diff --git a/web/app/components/workflow/block-selector/__tests__/all-start-blocks.spec.tsx b/web/app/components/workflow/block-selector/__tests__/all-start-blocks.spec.tsx index 3559ad12f2a..98a4868765d 100644 --- a/web/app/components/workflow/block-selector/__tests__/all-start-blocks.spec.tsx +++ b/web/app/components/workflow/block-selector/__tests__/all-start-blocks.spec.tsx @@ -287,6 +287,7 @@ describe('AllStartBlocks', () => { createMarketplacePluginsMock([ createPlugin({ name: 'start-marketplace', + plugin_id: 'plugin-2', label: { en_US: 'Start Marketplace', zh_Hans: 'Start Marketplace' }, }), ]), @@ -311,6 +312,28 @@ describe('AllStartBlocks', () => { expect(screen.getAllByRole('link', { name: /plugin\.searchInMarketplace/i })).toHaveLength(1) }) + it('shows installed triggers once and keeps other marketplace results available', async () => { + enableMarketplaceForRender = true + mockUseMarketplacePlugins.mockReturnValue( + createMarketplacePluginsMock([ + createPlugin({ plugin_id: 'plugin-1', label: { en_US: 'Installed Marketplace' } }), + createPlugin({ plugin_id: 'plugin-2', label: { en_US: 'Available Marketplace' } }), + ]), + ) + + render( + , + ) + + expect(await screen.findByText('Provider One')).toBeInTheDocument() + expect(screen.queryByText('Installed Marketplace')).not.toBeInTheDocument() + expect(screen.getByText('Available Marketplace')).toBeInTheDocument() + }) + it('should show the user input conflict state without allowing another start selection', () => { const onSelect = vi.fn() enableMarketplaceForRender = true diff --git a/web/app/components/workflow/block-selector/__tests__/data-sources.spec.tsx b/web/app/components/workflow/block-selector/__tests__/data-sources.spec.tsx index b22f3788c51..a8c5f16d50d 100644 --- a/web/app/components/workflow/block-selector/__tests__/data-sources.spec.tsx +++ b/web/app/components/workflow/block-selector/__tests__/data-sources.spec.tsx @@ -8,11 +8,15 @@ import { renderWithConsoleQuery } from '@/test/console/query-data' import { BlockEnum } from '../../types' import { DEFAULT_FILE_EXTENSIONS_IN_LOCAL_FILE_DATA_SOURCE } from '../constants' import DataSources from '../data-sources' +import { createPlugin } from './factories' const { marketplaceQuery, language, trackEvent } = vi.hoisted(() => ({ - marketplaceQuery: vi.fn((_params?: Parameters[0]) => ({ - data: undefined, - })), + marketplaceQuery: vi.fn( + ( + _params?: Parameters[0], + ): ReturnType => + ({ data: undefined }) as ReturnType, + ), language: { value: 'en_US' }, trackEvent: vi.fn(), })) @@ -210,6 +214,32 @@ it('debounces marketplace search while datasource filtering remains immediate', ) }) +it('shows installed datasources once and keeps other marketplace results available', async () => { + const provider = namedProvider('source') + marketplaceQuery.mockReturnValue({ + data: { + pages: [ + { + plugins: [ + createPlugin({ plugin_id: provider.plugin_id, label: { en_US: 'Installed Source' } }), + createPlugin({ plugin_id: 'dify/other', label: { en_US: 'Other Source' } }), + ], + page: 1, + page_size: 40, + total: 2, + }, + ], + pageParams: [1], + }, + } as ReturnType) + + render(, true) + + expect(await screen.findByRole('button', { name: 'source' })).toBeInTheDocument() + expect(screen.queryByText('Installed Source')).not.toBeInTheDocument() + expect(screen.getByText('Other Source')).toBeInTheDocument() +}) + it('keeps an expanded datasource open when the language changes its sort letter', async () => { const user = userEvent.setup() const provider = createDatasourceProvider() diff --git a/web/app/components/workflow/block-selector/all-start-blocks.tsx b/web/app/components/workflow/block-selector/all-start-blocks.tsx index 3dfb8be0346..7a476e69ddd 100644 --- a/web/app/components/workflow/block-selector/all-start-blocks.tsx +++ b/web/app/components/workflow/block-selector/all-start-blocks.tsx @@ -117,9 +117,12 @@ function AllStartBlocks({ ) const { data: marketplacePluginsData, isFetching: isMarketplaceFetching } = useMarketplacePlugins(marketplaceSearchParams) - const marketplacePlugins = useMemo( - () => marketplacePluginsData?.pages.flatMap((page) => page.plugins) ?? [], - [marketplacePluginsData?.pages], + const notInstalledPlugins = useMemo( + () => + marketplacePluginsData?.pages.flatMap((page) => + page.plugins.filter((plugin) => !providerMap.has(plugin.plugin_id)), + ) ?? [], + [marketplacePluginsData?.pages, providerMap], ) const shouldShowFeatured = enableTriggerPlugin && enable_marketplace && !hasFilter @@ -136,7 +139,7 @@ function AllStartBlocks({ const hasInstalledPluginContent = enableTriggerPlugin && hasPluginContent const hasMarketplaceContent = - enableTriggerPlugin && enable_marketplace && marketplacePlugins.length > 0 + enableTriggerPlugin && enable_marketplace && notInstalledPlugins.length > 0 const hasAnyContent = hasStartBlocksContent || hasInstalledPluginContent || @@ -231,7 +234,7 @@ function AllStartBlocks({ } - list={marketplacePlugins} + list={notInstalledPlugins} searchText={trimmedSearchText} category={PluginCategoryEnum.trigger} tags={tags} diff --git a/web/app/components/workflow/block-selector/data-sources.tsx b/web/app/components/workflow/block-selector/data-sources.tsx index d0483bddcee..d80819a99e5 100644 --- a/web/app/components/workflow/block-selector/data-sources.tsx +++ b/web/app/components/workflow/block-selector/data-sources.tsx @@ -113,10 +113,14 @@ function DataSources({ ], ) const { data: marketplacePluginsData } = useMarketplacePlugins(marketplaceSearchParams) - const notInstalledPlugins = useMemo( - () => marketplacePluginsData?.pages.flatMap((page) => page.plugins) ?? [], - [marketplacePluginsData?.pages], - ) + const notInstalledPlugins = useMemo(() => { + const installedPluginIds = new Set(dataSources.map((provider) => provider.plugin_id)) + return ( + marketplacePluginsData?.pages.flatMap((page) => + page.plugins.filter((plugin) => !installedPluginIds.has(plugin.plugin_id)), + ) ?? [] + ) + }, [dataSources, marketplacePluginsData?.pages]) return (
diff --git a/web/app/components/workflow/nodes/_base/components/__tests__/agent-strategy-selector.spec.tsx b/web/app/components/workflow/nodes/_base/components/__tests__/agent-strategy-selector.spec.tsx index 89b3078a55f..3aefde257d2 100644 --- a/web/app/components/workflow/nodes/_base/components/__tests__/agent-strategy-selector.spec.tsx +++ b/web/app/components/workflow/nodes/_base/components/__tests__/agent-strategy-selector.spec.tsx @@ -207,6 +207,28 @@ it('keeps marketplace search and its category link alongside the installed catal expect(screen.getByText('Marketplace strategy')).toBeInTheDocument() }) +it('shows installed strategies once and keeps other marketplace results available', async () => { + const user = userEvent.setup() + const provider = createProvider() + marketplaceResults.mockReturnValue([ + createPlugin({ plugin_id: provider.plugin_id, label: { en_US: 'Installed Marketplace' } }), + createPlugin({ plugin_id: 'org/available', label: { en_US: 'Available Marketplace' } }), + ]) + request.mockImplementation(() => Promise.resolve(Response.json([provider]))) + render(, { + systemFeatures: { enable_marketplace: true }, + }) + + await user.click(screen.getByText('workflowAgent.nodes.agent.strategy.selectTip')) + expect(await screen.findByRole('button', { name: 'Agent provider' })).toBeInTheDocument() + await user.type( + screen.getByPlaceholderText('workflowAgent.nodes.agent.strategy.searchPlaceholder'), + 'agent', + ) + expect(screen.queryByText('Installed Marketplace')).not.toBeInTheDocument() + expect(await screen.findByText('Available Marketplace')).toBeInTheDocument() +}) + it('preserves an older saved selection with no plugin identifier until the user chooses a replacement', async () => { const user = userEvent.setup() const onChange = vi.fn() diff --git a/web/app/components/workflow/nodes/_base/components/agent-strategy-selector.tsx b/web/app/components/workflow/nodes/_base/components/agent-strategy-selector.tsx index 6949402e6c8..61edce260e3 100644 --- a/web/app/components/workflow/nodes/_base/components/agent-strategy-selector.tsx +++ b/web/app/components/workflow/nodes/_base/components/agent-strategy-selector.tsx @@ -116,8 +116,12 @@ export const AgentStrategySelector = memo((props: AgentStrategySelectorProps) => const wrapElemRef = useRef(null) - const { queryPluginsWithDebounced: fetchPlugins, plugins: notInstalledPlugins = [] } = + const { queryPluginsWithDebounced: fetchPlugins, plugins: marketplacePlugins = [] } = useMarketplacePlugins() + const notInstalledPlugins = useMemo(() => { + const installedPluginIds = new Set((providers.data ?? []).map((provider) => provider.plugin_id)) + return marketplacePlugins.filter((plugin) => !installedPluginIds.has(plugin.plugin_id)) + }, [marketplacePlugins, providers.data]) useEffect(() => { if (!enable_marketplace) return