mirror of
https://github.com/langgenius/dify.git
synced 2026-05-28 12:53:23 +08:00
fix: align tools and mcp provider behavior
This commit is contained in:
@ -104,6 +104,39 @@ vi.mock('@/service/use-plugins', () => ({
|
||||
useInvalidateInstalledPluginList: () => mockInvalidateInstalledPluginList,
|
||||
}))
|
||||
|
||||
const {
|
||||
mockCanSetPermissions,
|
||||
mockReferenceSetting,
|
||||
mockSetReferenceSettings,
|
||||
} = vi.hoisted(() => ({
|
||||
mockCanSetPermissions: vi.fn(() => true),
|
||||
mockReferenceSetting: vi.fn(() => ({
|
||||
permission: {
|
||||
install_permission: 'everyone',
|
||||
debug_permission: 'admins',
|
||||
},
|
||||
auto_upgrade: {},
|
||||
})),
|
||||
mockSetReferenceSettings: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/plugins/plugin-page/use-reference-setting', () => ({
|
||||
default: () => ({
|
||||
referenceSetting: mockReferenceSetting(),
|
||||
canSetPermissions: mockCanSetPermissions(),
|
||||
setReferenceSettings: mockSetReferenceSettings,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/plugins/reference-setting-modal', () => ({
|
||||
__esModule: true,
|
||||
default: ({ onHide }: { onHide: () => void }) => (
|
||||
<div data-testid="reference-setting-modal">
|
||||
<button type="button" onClick={onHide}>Close modal</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/plugins/card', () => ({
|
||||
default: ({ payload, className }: { payload: { name: string }, className?: string }) => (
|
||||
<div data-testid={`card-${payload.name}`} className={className}>{payload.name}</div>
|
||||
@ -227,6 +260,14 @@ describe('ProviderList', () => {
|
||||
mockEnableMarketplace = false
|
||||
mockCollectionData = createDefaultCollections()
|
||||
mockCheckedInstalledData = null
|
||||
mockCanSetPermissions.mockReturnValue(true)
|
||||
mockReferenceSetting.mockReturnValue({
|
||||
permission: {
|
||||
install_permission: 'everyone',
|
||||
debug_permission: 'admins',
|
||||
},
|
||||
auto_upgrade: {},
|
||||
})
|
||||
Element.prototype.scrollTo = vi.fn()
|
||||
})
|
||||
|
||||
@ -280,20 +321,27 @@ describe('ProviderList', () => {
|
||||
it('uses default content inset outside compact integrations layout', () => {
|
||||
const { container } = renderProviderList()
|
||||
|
||||
expect(container.querySelector('.sticky')).toHaveClass('px-12')
|
||||
expect(container.querySelector('.sticky')).not.toHaveClass('max-w-[1600px]')
|
||||
expect(screen.getByTestId('card-google-search').closest('.grid')).toHaveClass('px-12')
|
||||
expect(screen.getByTestId('card-google-search').closest('.grid')).not.toHaveClass('max-w-[1600px]')
|
||||
expect(container.querySelector('.sticky')).toHaveClass('px-12', 'pt-2', 'pb-0')
|
||||
expect(container.querySelector('.sticky')).toHaveClass('max-w-[1600px]')
|
||||
expect(screen.getByTestId('card-google-search').closest('.grid')).toHaveClass('px-12', 'gap-2', 'pt-2')
|
||||
expect(screen.getByTestId('card-google-search').closest('.grid')).toHaveClass('max-w-[1600px]')
|
||||
})
|
||||
|
||||
it('uses compact content inset when rendered by integrations layout', () => {
|
||||
const { container } = renderProviderList(undefined, 'builtin', 'compact')
|
||||
|
||||
expect(container.querySelector('.sticky')).toHaveClass('px-6')
|
||||
expect(container.querySelector('.sticky')).toHaveClass('px-6', 'pt-2', 'pb-0')
|
||||
expect(container.querySelector('.sticky')).toHaveClass('max-w-[1600px]')
|
||||
expect(screen.getByTestId('card-google-search').closest('.grid')).toHaveClass('px-6')
|
||||
expect(screen.getByTestId('card-google-search').closest('.grid')).toHaveClass('px-6', 'gap-2', 'pt-2')
|
||||
expect(screen.getByTestId('card-google-search').closest('.grid')).toHaveClass('max-w-[1600px]')
|
||||
})
|
||||
|
||||
it('keeps tool cards at three columns on desktop and wider screens', () => {
|
||||
renderProviderList(undefined, 'builtin', 'compact')
|
||||
|
||||
expect(screen.getByTestId('card-google-search').closest('.grid')).toHaveClass('grid-cols-1', 'sm:grid-cols-2', 'md:grid-cols-3')
|
||||
expect(screen.getByTestId('card-google-search').closest('.grid')).not.toHaveClass('lg:grid-cols-4')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Filtering', () => {
|
||||
@ -344,6 +392,16 @@ describe('ProviderList', () => {
|
||||
expect(screen.getByTestId('card-weather-tool')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('does not apply hidden tag filters on non-tools tabs', () => {
|
||||
renderProviderList()
|
||||
|
||||
fireEvent.click(screen.getByTestId('add-filter'))
|
||||
fireEvent.click(screen.getByText('tools.type.custom'))
|
||||
|
||||
expect(screen.queryByTestId('label-filter')).not.toBeInTheDocument()
|
||||
expect(screen.getByTestId('card-my-api')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('clears search with clear button', () => {
|
||||
renderProviderList()
|
||||
const input = screen.getByRole('textbox')
|
||||
@ -353,13 +411,17 @@ describe('ProviderList', () => {
|
||||
expect(screen.getByTestId('card-weather-tool')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows label filter for non-MCP tabs', () => {
|
||||
it('shows label filter for the built-in tools page', () => {
|
||||
renderProviderList()
|
||||
expect(screen.getByTestId('label-filter')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('hides label filter for MCP tab', () => {
|
||||
renderProviderList({ category: 'mcp' })
|
||||
it.each([
|
||||
['api'],
|
||||
['workflow'],
|
||||
['mcp'],
|
||||
] as const)('hides label filter for the %s tool page', (category) => {
|
||||
renderProviderList({ category })
|
||||
expect(screen.queryByTestId('label-filter')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
@ -367,6 +429,28 @@ describe('ProviderList', () => {
|
||||
renderProviderList()
|
||||
expect(screen.getByRole('textbox')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('opens plugin update settings from the tools toolbar', () => {
|
||||
renderProviderList(undefined, 'builtin')
|
||||
|
||||
expect(screen.getByText('common.modelProvider.updateSetting')).toBeInTheDocument()
|
||||
expect(screen.getByText('plugin.autoUpdate.strategy.latest.name')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByText('common.modelProvider.updateSetting'))
|
||||
|
||||
expect(screen.getByTestId('reference-setting-modal')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it.each([
|
||||
['mcp'],
|
||||
['api'],
|
||||
['workflow'],
|
||||
] as const)('does not show plugin update settings on the %s tool page', (category) => {
|
||||
renderProviderList({ category })
|
||||
|
||||
expect(screen.queryByText('common.modelProvider.updateSetting')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('plugin.autoUpdate.strategy.latest.name')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Custom Tab', () => {
|
||||
|
||||
@ -15,6 +15,7 @@ vi.mock('@/app/components/plugins/marketplace/list', () => ({
|
||||
default: (props: {
|
||||
marketplaceCollections: unknown[]
|
||||
marketplaceCollectionPluginsMap: Record<string, unknown[]>
|
||||
cardContainerClassName?: string
|
||||
plugins?: unknown[]
|
||||
showInstallButton?: boolean
|
||||
}) => {
|
||||
@ -136,6 +137,7 @@ describe('Marketplace', () => {
|
||||
// Assert
|
||||
expect(screen.getByTestId('marketplace-list')).toBeInTheDocument()
|
||||
expect(listRenderSpy).toHaveBeenCalledWith(expect.objectContaining({
|
||||
cardContainerClassName: 'grid-cols-1 sm:grid-cols-2 md:grid-cols-3',
|
||||
showInstallButton: true,
|
||||
}))
|
||||
})
|
||||
|
||||
@ -11,7 +11,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import List from '@/app/components/plugins/marketplace/list'
|
||||
import { getMarketplaceUrl } from '@/utils/var'
|
||||
import { toolsContentFrameClassNames, toolsContentInsetClassNames } from '../content-inset'
|
||||
import { toolsContentInsetClassNames, toolsUnifiedContentFrameClassName } from '../content-inset'
|
||||
|
||||
type MarketplaceProps = {
|
||||
searchPluginText: string
|
||||
@ -40,7 +40,8 @@ const Marketplace = ({
|
||||
page,
|
||||
} = marketplaceContext
|
||||
const contentPaddingClassName = toolsContentInsetClassNames[contentInset]
|
||||
const contentFrameClassName = cn(toolsContentFrameClassNames[contentInset], contentPaddingClassName)
|
||||
const marketplaceFrameClassName = cn(contentPaddingClassName, toolsUnifiedContentFrameClassName)
|
||||
const cardContainerClassName = 'grid-cols-1 sm:grid-cols-2 md:grid-cols-3'
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -51,7 +52,7 @@ const Marketplace = ({
|
||||
onClick={showMarketplacePanel}
|
||||
/>
|
||||
)}
|
||||
<div className={cn('pt-4 pb-3', contentFrameClassName)}>
|
||||
<div className={cn('pt-4 pb-3', marketplaceFrameClassName)}>
|
||||
<div className="bg-linear-to-r from-[rgba(11,165,236,0.95)] to-[rgba(21,90,239,0.95)] bg-clip-text title-2xl-semi-bold text-transparent">
|
||||
{t('marketplace.moreFrom', { ns: 'plugin' })}
|
||||
</div>
|
||||
@ -106,12 +107,13 @@ const Marketplace = ({
|
||||
}
|
||||
{
|
||||
(!isLoading || page > 1) && (
|
||||
<div className={contentFrameClassName}>
|
||||
<div className={marketplaceFrameClassName}>
|
||||
<List
|
||||
marketplaceCollections={marketplaceCollections || []}
|
||||
marketplaceCollectionPluginsMap={marketplaceCollectionPluginsMap || {}}
|
||||
plugins={plugins}
|
||||
showInstallButton
|
||||
cardContainerClassName={cardContainerClassName}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -149,11 +149,25 @@ describe('NewMCPCard', () => {
|
||||
})
|
||||
|
||||
describe('Styling', () => {
|
||||
it('should have correct card structure', () => {
|
||||
it('should match the Figma card shell and section sizing', () => {
|
||||
render(<NewMCPCard {...defaultProps} />, { wrapper: createWrapper() })
|
||||
|
||||
const card = document.querySelector('.rounded-xl')
|
||||
expect(card).toBeInTheDocument()
|
||||
const card = screen.getByText('tools.mcp.create.cardTitle').closest('.col-span-1')
|
||||
expect(card).toHaveClass(
|
||||
'h-[120px]',
|
||||
'overflow-hidden',
|
||||
'rounded-xl',
|
||||
'border-[0.5px]',
|
||||
'border-components-panel-border',
|
||||
'bg-components-panel-on-panel-item-bg',
|
||||
'shadow-md',
|
||||
)
|
||||
|
||||
const header = screen.getByRole('button', { name: 'tools.mcp.create.cardTitle' })
|
||||
expect(header).toHaveClass('h-[84px]', 'gap-3', 'p-4')
|
||||
|
||||
const docLink = screen.getByText('tools.mcp.create.cardLink').closest('a')
|
||||
expect(docLink).toHaveClass('h-8', 'border-t', 'border-divider-subtle', 'px-3', 'py-2')
|
||||
})
|
||||
|
||||
it('should have clickable cursor style', () => {
|
||||
|
||||
@ -167,6 +167,7 @@ describe('MCPList', () => {
|
||||
mockProviders = [
|
||||
{ id: '1', name: { 'en-US': 'Search Tool' }, type: 'mcp' },
|
||||
{ id: '2', name: { 'en-US': 'Another Provider' }, type: 'mcp' },
|
||||
{ id: '3', name: { 'en-US': 'Search API Tool' }, type: 'api' },
|
||||
]
|
||||
})
|
||||
|
||||
@ -175,6 +176,7 @@ describe('MCPList', () => {
|
||||
|
||||
expect(screen.getByTestId('provider-card-1')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('provider-card-2')).not.toBeInTheDocument()
|
||||
expect(screen.queryByTestId('provider-card-3')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should filter case-insensitively', () => {
|
||||
@ -316,13 +318,14 @@ describe('MCPList', () => {
|
||||
})
|
||||
|
||||
describe('Grid Layout', () => {
|
||||
it('should have responsive grid layout', () => {
|
||||
it('should keep MCP cards to three columns at desktop width and above', () => {
|
||||
render(<MCPList searchText="" />)
|
||||
|
||||
const grid = document.querySelector('.grid')
|
||||
expect(grid).toHaveClass('grid-cols-1')
|
||||
expect(grid).toHaveClass('md:grid-cols-2')
|
||||
expect(grid).toHaveClass('xl:grid-cols-4')
|
||||
expect(grid).toHaveClass('grid-cols-1', 'sm:grid-cols-2', 'md:grid-cols-3')
|
||||
expect(grid).not.toHaveClass('xl:grid-cols-4')
|
||||
expect(grid).not.toHaveClass('2xl:grid-cols-5')
|
||||
expect(grid).not.toHaveClass('2k:grid-cols-6')
|
||||
})
|
||||
|
||||
it('should have overflow hidden when list is empty', () => {
|
||||
|
||||
@ -183,6 +183,20 @@ describe('MCPCard', () => {
|
||||
render(<MCPCard {...defaultProps} />, { wrapper: createWrapper() })
|
||||
expect(screen.getByText(/tools.mcp.updateTime/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should use the Figma card shell', () => {
|
||||
render(<MCPCard {...defaultProps} />, { wrapper: createWrapper() })
|
||||
|
||||
const card = screen.getByText('Test MCP Server').closest('.group')
|
||||
expect(card).toHaveClass(
|
||||
'overflow-hidden',
|
||||
'rounded-xl',
|
||||
'border-[0.5px]',
|
||||
'border-components-panel-border',
|
||||
'bg-components-panel-on-panel-item-bg',
|
||||
'shadow-md',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('No Tools State', () => {
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
'use client'
|
||||
import type { ToolWithProvider } from '@/app/components/workflow/types'
|
||||
import {
|
||||
RiAddCircleFill,
|
||||
RiAddLine,
|
||||
RiArrowRightUpLine,
|
||||
RiBookOpenLine,
|
||||
} from '@remixicon/react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -35,22 +34,34 @@ const NewMCPCard = ({ handleCreate }: Props) => {
|
||||
return (
|
||||
<>
|
||||
{isCurrentWorkspaceManager && (
|
||||
<div className="col-span-1 flex min-h-[108px] cursor-pointer flex-col rounded-xl bg-background-default-dimmed transition-all duration-200 ease-in-out">
|
||||
<div className="group grow rounded-t-xl" onClick={() => setShowModal(true)}>
|
||||
<div className="flex shrink-0 items-center p-4 pb-3">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-lg border border-dashed border-divider-deep group-hover:border-solid group-hover:border-state-accent-hover-alt group-hover:bg-state-accent-hover">
|
||||
<RiAddCircleFill className="h-4 w-4 text-text-quaternary group-hover:text-text-accent" />
|
||||
<div className="col-span-1 flex h-[120px] flex-col overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg shadow-md">
|
||||
<button
|
||||
type="button"
|
||||
className="group flex h-[84px] w-full cursor-pointer items-center gap-3 p-4 text-left outline-hidden hover:bg-components-panel-on-panel-item-bg-hover focus-visible:ring-1 focus-visible:ring-components-input-border-hover"
|
||||
onClick={() => setShowModal(true)}
|
||||
>
|
||||
<div className="flex size-10 shrink-0 items-center justify-center">
|
||||
<div className="flex size-10 items-center justify-center rounded-lg border-[0.5px] border-dashed border-divider-regular bg-background-body">
|
||||
<RiAddLine className="size-4 text-text-quaternary group-hover:text-text-accent" />
|
||||
</div>
|
||||
<div className="ml-3 system-md-semibold text-text-secondary group-hover:text-text-accent">{t('mcp.create.cardTitle', { ns: 'tools' })}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-b-xl border-t-[0.5px] border-divider-subtle px-4 py-3 text-text-tertiary hover:text-text-accent">
|
||||
<a href={linkUrl} target="_blank" rel="noopener noreferrer" className="flex items-center space-x-1">
|
||||
<RiBookOpenLine className="h-3 w-3 shrink-0" />
|
||||
<div className="grow truncate system-xs-regular" title={t('mcp.create.cardLink', { ns: 'tools' }) || ''}>{t('mcp.create.cardLink', { ns: 'tools' })}</div>
|
||||
<RiArrowRightUpLine className="h-3 w-3 shrink-0" />
|
||||
</a>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1 py-px">
|
||||
<div className="truncate system-md-semibold text-text-primary group-hover:text-text-accent" title={t('mcp.create.cardTitle', { ns: 'tools' }) || ''}>
|
||||
{t('mcp.create.cardTitle', { ns: 'tools' })}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<a
|
||||
href={linkUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex h-8 items-center gap-0.5 border-t border-divider-subtle px-3 py-2 text-components-button-secondary-text outline-hidden hover:bg-components-panel-on-panel-item-bg-hover hover:text-text-accent focus-visible:ring-1 focus-visible:ring-components-input-border-hover"
|
||||
>
|
||||
<div className="min-w-0 flex-1 px-0.5">
|
||||
<div className="truncate system-sm-medium" title={t('mcp.create.cardLink', { ns: 'tools' }) || ''}>{t('mcp.create.cardLink', { ns: 'tools' })}</div>
|
||||
</div>
|
||||
<RiArrowRightUpLine className="size-4 shrink-0" />
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
{showModal && (
|
||||
|
||||
@ -62,7 +62,16 @@ describe('MCPDetailPanel', () => {
|
||||
<MCPDetailPanel {...defaultProps} detail={detail} />,
|
||||
{ wrapper: createWrapper() },
|
||||
)
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument()
|
||||
const dialog = screen.getByRole('dialog')
|
||||
|
||||
expect(dialog).toBeInTheDocument()
|
||||
expect(dialog).toHaveClass(
|
||||
'data-[swipe-direction=right]:top-2',
|
||||
'data-[swipe-direction=right]:bottom-2',
|
||||
'data-[swipe-direction=right]:h-[calc(100dvh-16px)]',
|
||||
'data-[swipe-direction=right]:w-[400px]',
|
||||
'data-[swipe-direction=right]:max-w-[calc(100vw-1rem)]',
|
||||
)
|
||||
})
|
||||
|
||||
it('should render content when detail is provided', () => {
|
||||
|
||||
@ -50,7 +50,7 @@ const MCPDetailPanel: FC<Props> = ({
|
||||
<DrawerPortal>
|
||||
<DrawerBackdrop className="bg-transparent" />
|
||||
<DrawerViewport>
|
||||
<DrawerPopup className={cn('justify-start bg-components-panel-bg! p-0! shadow-xl data-[swipe-direction=right]:top-16 data-[swipe-direction=right]:right-2 data-[swipe-direction=right]:bottom-2 data-[swipe-direction=right]:h-auto data-[swipe-direction=right]:w-[420px] data-[swipe-direction=right]:max-w-[420px] data-[swipe-direction=right]:rounded-2xl data-[swipe-direction=right]:border-[0.5px] data-[swipe-direction=right]:border-components-panel-border')}>
|
||||
<DrawerPopup className={cn('justify-start bg-components-panel-bg! p-0! shadow-xl data-[swipe-direction=right]:top-2 data-[swipe-direction=right]:right-2 data-[swipe-direction=right]:bottom-2 data-[swipe-direction=right]:h-[calc(100dvh-16px)] data-[swipe-direction=right]:w-[400px] data-[swipe-direction=right]:max-w-[calc(100vw-1rem)] data-[swipe-direction=right]:rounded-2xl data-[swipe-direction=right]:border-[0.5px] data-[swipe-direction=right]:border-components-panel-border')}>
|
||||
<DrawerContent className="flex min-h-0 flex-1 flex-col p-0 pb-0">
|
||||
{detail && (
|
||||
<MCPDetailContent
|
||||
|
||||
@ -6,7 +6,7 @@ import { useMemo, useState } from 'react'
|
||||
import {
|
||||
useAllToolProviders,
|
||||
} from '@/service/use-tools'
|
||||
import { toolsContentFrameClassNames, toolsContentInsetClassNames } from '../content-inset'
|
||||
import { toolsContentInsetClassNames, toolsUnifiedContentFrameClassName } from '../content-inset'
|
||||
import NewMCPCard from './create-card'
|
||||
import MCPDetailPanel from './detail/provider-detail'
|
||||
import MCPCard from './provider-card'
|
||||
@ -44,9 +44,11 @@ const MCPList = ({
|
||||
|
||||
const filteredList = useMemo(() => {
|
||||
return list.filter((collection) => {
|
||||
if (collection.type !== 'mcp')
|
||||
return false
|
||||
if (searchText)
|
||||
return Object.values(collection.name).some(value => (value as string).toLowerCase().includes(searchText.toLowerCase()))
|
||||
return collection.type === 'mcp'
|
||||
return true
|
||||
}) as ToolWithProvider[]
|
||||
}, [list, searchText])
|
||||
|
||||
@ -68,12 +70,12 @@ const MCPList = ({
|
||||
setIsTriggerAuthorize(true)
|
||||
}
|
||||
const contentPaddingClassName = toolsContentInsetClassNames[contentInset]
|
||||
const contentFrameClassName = cn(toolsContentFrameClassNames[contentInset], contentPaddingClassName)
|
||||
const contentFrameClassName = cn(contentPaddingClassName, toolsUnifiedContentFrameClassName)
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={cn(
|
||||
'relative grid shrink-0 grid-cols-1 content-start gap-4 pt-2 pb-4 2k:grid-cols-6 sm:grid-cols-1 md:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-5',
|
||||
'relative grid shrink-0 grid-cols-1 content-start gap-4 pt-2 pb-4 sm:grid-cols-2 md:grid-cols-3',
|
||||
contentFrameClassName,
|
||||
!list.length && 'h-[calc(100vh-136px)] overflow-hidden',
|
||||
)}
|
||||
|
||||
@ -85,8 +85,8 @@ const MCPCard = ({
|
||||
<div
|
||||
onClick={() => handleSelect(data.id)}
|
||||
className={cn(
|
||||
'group relative flex cursor-pointer flex-col rounded-xl border-[1.5px] border-transparent bg-components-card-bg shadow-xs hover:bg-components-card-bg-alt hover:shadow-md',
|
||||
currentProvider?.id === data.id && 'border-components-option-card-option-selected-border bg-components-card-bg-alt',
|
||||
'group relative flex cursor-pointer flex-col overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg shadow-md hover:bg-components-panel-on-panel-item-bg-hover',
|
||||
currentProvider?.id === data.id && 'border-components-option-card-option-selected-border bg-components-panel-on-panel-item-bg-hover',
|
||||
)}
|
||||
>
|
||||
<div className="flex grow items-center gap-3 rounded-t-xl p-4">
|
||||
|
||||
@ -3,6 +3,7 @@ import type { ToolsContentInset } from './content-inset'
|
||||
import type { Collection } from './types'
|
||||
import type { Plugin } from '@/app/components/plugins/types'
|
||||
import type { ToolCategory } from '@/app/components/tools/integration-routes'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { parseAsStringLiteral, useQueryState } from 'nuqs'
|
||||
@ -15,6 +16,8 @@ import CardMoreInfo from '@/app/components/plugins/card/card-more-info'
|
||||
import { useTags } from '@/app/components/plugins/hooks'
|
||||
import Empty from '@/app/components/plugins/marketplace/empty'
|
||||
import PluginDetailPanel from '@/app/components/plugins/plugin-detail-panel'
|
||||
import useReferenceSetting from '@/app/components/plugins/plugin-page/use-reference-setting'
|
||||
import ReferenceSettingModal from '@/app/components/plugins/reference-setting-modal'
|
||||
import { TOOL_CATEGORY_VALUES } from '@/app/components/tools/integration-routes'
|
||||
import LabelFilter from '@/app/components/tools/labels/filter'
|
||||
import CustomCreateCard from '@/app/components/tools/provider/custom-create-card'
|
||||
@ -23,7 +26,7 @@ import WorkflowToolEmpty from '@/app/components/tools/provider/empty'
|
||||
import { systemFeaturesQueryOptions } from '@/service/system-features'
|
||||
import { useCheckInstalled, useInvalidateInstalledPluginList } from '@/service/use-plugins'
|
||||
import { useAllToolProviders } from '@/service/use-tools'
|
||||
import { toolsContentFrameClassNames, toolsContentInsetClassNames } from './content-inset'
|
||||
import { toolsContentInsetClassNames, toolsUnifiedContentFrameClassName } from './content-inset'
|
||||
import Marketplace from './marketplace'
|
||||
import { useMarketplace } from './marketplace/hooks'
|
||||
import MCPList from './mcp'
|
||||
@ -51,6 +54,11 @@ const ProviderList = ({
|
||||
// searchParams.get('category') === 'workflow'
|
||||
const { t } = useTranslation()
|
||||
const { getTagLabel } = useTags()
|
||||
const {
|
||||
referenceSetting,
|
||||
canSetPermissions,
|
||||
setReferenceSettings,
|
||||
} = useReferenceSetting()
|
||||
const { data: enable_marketplace } = useSuspenseQuery({
|
||||
...systemFeaturesQueryOptions(),
|
||||
select: s => s.enable_marketplace,
|
||||
@ -61,7 +69,9 @@ const ProviderList = ({
|
||||
const activeTab = category ?? categoryParam
|
||||
const isRouteCategory = !!category
|
||||
const contentPaddingClassName = toolsContentInsetClassNames[contentInset]
|
||||
const contentFrameClassName = cn(toolsContentFrameClassNames[contentInset], contentPaddingClassName)
|
||||
const toolListFrameClassName = cn(contentPaddingClassName, toolsUnifiedContentFrameClassName)
|
||||
const showToolsUpdateSetting = activeTab === 'builtin' && canSetPermissions && !!referenceSetting
|
||||
const showLabelFilter = activeTab === 'builtin'
|
||||
const options = [
|
||||
{ value: 'builtin', text: t('type.builtIn', { ns: 'tools' }) },
|
||||
{ value: 'api', text: t('type.custom', { ns: 'tools' }) },
|
||||
@ -69,6 +79,7 @@ const ProviderList = ({
|
||||
{ value: 'mcp', text: 'MCP' },
|
||||
]
|
||||
const [tagFilterValue, setTagFilterValue] = useState<string[]>([])
|
||||
const [showPluginSettingModal, setShowPluginSettingModal] = useState(false)
|
||||
const handleTagsChange = (value: string[]) => {
|
||||
setTagFilterValue(value)
|
||||
}
|
||||
@ -81,13 +92,13 @@ const ProviderList = ({
|
||||
return collectionList.filter((collection) => {
|
||||
if (collection.type !== activeTab)
|
||||
return false
|
||||
if (tagFilterValue.length > 0 && (!collection.labels || collection.labels.every(label => !tagFilterValue.includes(label))))
|
||||
if (showLabelFilter && tagFilterValue.length > 0 && (!collection.labels || collection.labels.every(label => !tagFilterValue.includes(label))))
|
||||
return false
|
||||
if (keywords)
|
||||
return Object.values(collection.label).some(value => value.toLowerCase().includes(keywords.toLowerCase()))
|
||||
return true
|
||||
})
|
||||
}, [activeTab, tagFilterValue, keywords, collectionList])
|
||||
}, [activeTab, showLabelFilter, tagFilterValue, keywords, collectionList])
|
||||
|
||||
const [currentProviderId, setCurrentProviderId] = useState<string | undefined>()
|
||||
const currentProvider = useMemo<Collection | undefined>(() => {
|
||||
@ -146,8 +157,8 @@ const ProviderList = ({
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'sticky top-0 z-10 flex flex-wrap items-center justify-start gap-x-4 gap-y-2 bg-background-body pt-4 pb-2',
|
||||
contentFrameClassName,
|
||||
'sticky top-0 z-10 flex flex-wrap items-center justify-start gap-x-2 gap-y-2 bg-background-body pt-2 pb-0',
|
||||
toolListFrameClassName,
|
||||
currentProviderId && 'pr-6',
|
||||
)}
|
||||
>
|
||||
@ -165,25 +176,41 @@ const ProviderList = ({
|
||||
options={options}
|
||||
/>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
{activeTab !== 'mcp' && (
|
||||
<LabelFilter value={tagFilterValue} onChange={handleTagsChange} />
|
||||
<div className="flex min-w-[200px] flex-1 items-center justify-between gap-2">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
{showLabelFilter && (
|
||||
<LabelFilter value={tagFilterValue} onChange={handleTagsChange} />
|
||||
)}
|
||||
<Input
|
||||
showLeftIcon
|
||||
showClearIcon
|
||||
wrapperClassName="w-[200px]"
|
||||
value={keywords}
|
||||
onChange={e => handleKeywordsChange(e.target.value)}
|
||||
onClear={() => handleKeywordsChange('')}
|
||||
/>
|
||||
</div>
|
||||
{showToolsUpdateSetting && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="h-8 shrink-0 gap-0.5 px-3 system-sm-medium"
|
||||
onClick={() => setShowPluginSettingModal(true)}
|
||||
>
|
||||
<span aria-hidden className="i-ri-flashlight-line size-4" />
|
||||
<span className="px-0.5">{t('modelProvider.updateSetting', { ns: 'common' })}</span>
|
||||
<span className="flex min-w-4 items-center justify-center rounded-[5px] border border-divider-deep bg-components-badge-bg-dimm px-1 py-0.5 system-2xs-medium-uppercase text-text-tertiary">
|
||||
{t('autoUpdate.strategy.latest.name', { ns: 'plugin' })}
|
||||
</span>
|
||||
<span aria-hidden className="i-ri-arrow-down-s-line size-4" />
|
||||
</Button>
|
||||
)}
|
||||
<Input
|
||||
showLeftIcon
|
||||
showClearIcon
|
||||
wrapperClassName="w-[200px]"
|
||||
value={keywords}
|
||||
onChange={e => handleKeywordsChange(e.target.value)}
|
||||
onClear={() => handleKeywordsChange('')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{activeTab !== 'mcp' && (
|
||||
<div
|
||||
className={cn(
|
||||
'relative grid shrink-0 grid-cols-1 content-start gap-4 pt-2 pb-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4',
|
||||
contentFrameClassName,
|
||||
'relative grid shrink-0 grid-cols-1 content-start gap-2 pt-2 pb-4 sm:grid-cols-2 md:grid-cols-3',
|
||||
toolListFrameClassName,
|
||||
!filteredCollectionList.length && activeTab === 'workflow' && 'grow',
|
||||
)}
|
||||
>
|
||||
@ -217,7 +244,7 @@ const ProviderList = ({
|
||||
</div>
|
||||
)}
|
||||
{!filteredCollectionList.length && activeTab === 'builtin' && (
|
||||
<Empty lightCard text={t('noTools', { ns: 'tools' })} className={cn('h-[224px] shrink-0', contentFrameClassName)} />
|
||||
<Empty lightCard text={t('noTools', { ns: 'tools' })} className={cn('h-[224px] shrink-0', toolListFrameClassName)} />
|
||||
)}
|
||||
<div ref={toolListTailRef} />
|
||||
{enable_marketplace && activeTab === 'builtin' && (
|
||||
@ -247,6 +274,13 @@ const ProviderList = ({
|
||||
onUpdate={() => invalidateInstalledPluginList()}
|
||||
onHide={() => setCurrentProviderId(undefined)}
|
||||
/>
|
||||
{showPluginSettingModal && referenceSetting && (
|
||||
<ReferenceSettingModal
|
||||
payload={referenceSetting}
|
||||
onHide={() => setShowPluginSettingModal(false)}
|
||||
onSave={setReferenceSettings}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -174,6 +174,32 @@ describe('ProviderDetail', () => {
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('uses the full-height right drawer layout from the design', () => {
|
||||
render(
|
||||
<ProviderDetail
|
||||
collection={createMockCollection()}
|
||||
onHide={mockOnHide}
|
||||
onRefreshData={mockOnRefreshData}
|
||||
/>,
|
||||
)
|
||||
|
||||
const dialog = screen.getByRole('dialog')
|
||||
|
||||
expect(dialog).toHaveClass(
|
||||
'data-[swipe-direction=right]:top-2',
|
||||
'data-[swipe-direction=right]:right-2',
|
||||
'data-[swipe-direction=right]:bottom-2',
|
||||
'data-[swipe-direction=right]:h-[calc(100dvh-16px)]',
|
||||
'data-[swipe-direction=right]:w-[400px]',
|
||||
'data-[swipe-direction=right]:max-w-[calc(100vw-1rem)]',
|
||||
)
|
||||
expect(dialog).not.toHaveClass(
|
||||
'data-[swipe-direction=right]:top-16',
|
||||
'data-[swipe-direction=right]:w-[420px]',
|
||||
'data-[swipe-direction=right]:max-w-[420px]',
|
||||
)
|
||||
})
|
||||
|
||||
it('renders title, org info and description for a builtIn collection', async () => {
|
||||
render(
|
||||
<ProviderDetail
|
||||
|
||||
@ -248,7 +248,7 @@ const ProviderDetail = ({
|
||||
<DrawerPortal>
|
||||
<DrawerBackdrop className="bg-transparent" />
|
||||
<DrawerViewport>
|
||||
<DrawerPopup className={cn('justify-start bg-components-panel-bg! p-0! shadow-xl data-[swipe-direction=right]:top-16 data-[swipe-direction=right]:right-2 data-[swipe-direction=right]:bottom-2 data-[swipe-direction=right]:h-auto data-[swipe-direction=right]:w-[420px] data-[swipe-direction=right]:max-w-[420px] data-[swipe-direction=right]:rounded-2xl data-[swipe-direction=right]:border-[0.5px] data-[swipe-direction=right]:border-components-panel-border')}>
|
||||
<DrawerPopup className={cn('justify-start bg-components-panel-bg! p-0! shadow-xl data-[swipe-direction=right]:top-2 data-[swipe-direction=right]:right-2 data-[swipe-direction=right]:bottom-2 data-[swipe-direction=right]:h-[calc(100dvh-16px)] data-[swipe-direction=right]:w-[400px] data-[swipe-direction=right]:max-w-[calc(100vw-1rem)] data-[swipe-direction=right]:rounded-2xl data-[swipe-direction=right]:border-[0.5px] data-[swipe-direction=right]:border-components-panel-border')}>
|
||||
<DrawerContent className="flex min-h-0 flex-1 flex-col p-0 pb-0">
|
||||
<div className="flex h-full flex-col p-4">
|
||||
<div className="shrink-0">
|
||||
|
||||
Reference in New Issue
Block a user