@@ -106,12 +107,13 @@ const Marketplace = ({
}
{
(!isLoading || page > 1) && (
-
+
)
diff --git a/web/app/components/tools/mcp/__tests__/create-card.spec.tsx b/web/app/components/tools/mcp/__tests__/create-card.spec.tsx
index 6e5b4038f4..d343be23be 100644
--- a/web/app/components/tools/mcp/__tests__/create-card.spec.tsx
+++ b/web/app/components/tools/mcp/__tests__/create-card.spec.tsx
@@ -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(
, { 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', () => {
diff --git a/web/app/components/tools/mcp/__tests__/index.spec.tsx b/web/app/components/tools/mcp/__tests__/index.spec.tsx
index 4a07d3f82a..f5b26af2c2 100644
--- a/web/app/components/tools/mcp/__tests__/index.spec.tsx
+++ b/web/app/components/tools/mcp/__tests__/index.spec.tsx
@@ -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(
)
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', () => {
diff --git a/web/app/components/tools/mcp/__tests__/provider-card.spec.tsx b/web/app/components/tools/mcp/__tests__/provider-card.spec.tsx
index fc4c07275a..8b66bb1c6e 100644
--- a/web/app/components/tools/mcp/__tests__/provider-card.spec.tsx
+++ b/web/app/components/tools/mcp/__tests__/provider-card.spec.tsx
@@ -183,6 +183,20 @@ describe('MCPCard', () => {
render(
, { wrapper: createWrapper() })
expect(screen.getByText(/tools.mcp.updateTime/)).toBeInTheDocument()
})
+
+ it('should use the Figma card shell', () => {
+ render(
, { 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', () => {
diff --git a/web/app/components/tools/mcp/create-card.tsx b/web/app/components/tools/mcp/create-card.tsx
index d96365b2db..b2fb040da8 100644
--- a/web/app/components/tools/mcp/create-card.tsx
+++ b/web/app/components/tools/mcp/create-card.tsx
@@ -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 && (
-
-
setShowModal(true)}>
-
-
)}
{showModal && (
diff --git a/web/app/components/tools/mcp/detail/__tests__/provider-detail.spec.tsx b/web/app/components/tools/mcp/detail/__tests__/provider-detail.spec.tsx
index 4d69d89516..c32398070b 100644
--- a/web/app/components/tools/mcp/detail/__tests__/provider-detail.spec.tsx
+++ b/web/app/components/tools/mcp/detail/__tests__/provider-detail.spec.tsx
@@ -62,7 +62,16 @@ describe('MCPDetailPanel', () => {
,
{ 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', () => {
diff --git a/web/app/components/tools/mcp/detail/provider-detail.tsx b/web/app/components/tools/mcp/detail/provider-detail.tsx
index e704e6d262..54906246f4 100644
--- a/web/app/components/tools/mcp/detail/provider-detail.tsx
+++ b/web/app/components/tools/mcp/detail/provider-detail.tsx
@@ -50,7 +50,7 @@ const MCPDetailPanel: FC
= ({
-
+
{detail && (
{
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 (
<>
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',
)}
>
diff --git a/web/app/components/tools/provider-list.tsx b/web/app/components/tools/provider-list.tsx
index 1dac25bc64..6c2551c2bc 100644
--- a/web/app/components/tools/provider-list.tsx
+++ b/web/app/components/tools/provider-list.tsx
@@ -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
([])
+ 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()
const currentProvider = useMemo(() => {
@@ -146,8 +157,8 @@ const ProviderList = ({
>
@@ -165,25 +176,41 @@ const ProviderList = ({
options={options}
/>
)}
-
- {activeTab !== 'mcp' && (
-
+
{activeTab !== 'mcp' && (
@@ -217,7 +244,7 @@ const ProviderList = ({
)}
{!filteredCollectionList.length && activeTab === 'builtin' && (
-
+
)}
{enable_marketplace && activeTab === 'builtin' && (
@@ -247,6 +274,13 @@ const ProviderList = ({
onUpdate={() => invalidateInstalledPluginList()}
onHide={() => setCurrentProviderId(undefined)}
/>
+ {showPluginSettingModal && referenceSetting && (
+
setShowPluginSettingModal(false)}
+ onSave={setReferenceSettings}
+ />
+ )}
>
)
}
diff --git a/web/app/components/tools/provider/__tests__/detail.spec.tsx b/web/app/components/tools/provider/__tests__/detail.spec.tsx
index 5a26589e11..bcaec02f34 100644
--- a/web/app/components/tools/provider/__tests__/detail.spec.tsx
+++ b/web/app/components/tools/provider/__tests__/detail.spec.tsx
@@ -174,6 +174,32 @@ describe('ProviderDetail', () => {
})
describe('Rendering', () => {
+ it('uses the full-height right drawer layout from the design', () => {
+ render(
+ ,
+ )
+
+ 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(
-
+