fix: align tools and mcp provider behavior

This commit is contained in:
Jingyi-Dify
2026-05-12 19:35:03 -07:00
parent 4ef2e952bd
commit 6cb97e9201
14 changed files with 266 additions and 65 deletions

View File

@ -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', () => {

View File

@ -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,
}))
})

View File

@ -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>
)

View File

@ -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', () => {

View File

@ -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', () => {

View File

@ -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', () => {

View File

@ -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 && (

View File

@ -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', () => {

View File

@ -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

View File

@ -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',
)}

View File

@ -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">

View File

@ -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}
/>
)}
</>
)
}

View File

@ -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

View File

@ -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">