fix(web): hide installed plugins from workflow marketplace selectors (#43191)

Co-authored-by: CodingOnStar <hanxujiang@dify.com>
This commit is contained in:
Coding On StarandCodingOnStar authored and GitHub committed 2026-09-29 10:18:11 +00:00
1 parent f79ed842fa
commit bfd5636bf0
6 files changed
+99 -13

No files matched your search

@@ -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(
<AllStartBlocks
searchText="provider"
onSelect={vi.fn()}
availableBlocksTypes={[BlockEnum.TriggerPlugin]}
/>,
)
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
@@ -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<typeof useMarketplacePlugins>[0]) => ({
data: undefined,
})),
marketplaceQuery: vi.fn(
(
_params?: Parameters<typeof useMarketplacePlugins>[0],
): ReturnType<typeof useMarketplacePlugins> =>
({ data: undefined }) as ReturnType<typeof useMarketplacePlugins>,
),
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<typeof useMarketplacePlugins>)
render(<DataSources searchText="source" onSelect={vi.fn()} dataSources={[provider]} />, 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()
@@ -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({
<PluginList
ref={pluginRef}
wrapElemRef={wrapElemRef as RefObject<HTMLElement>}
list={marketplacePlugins}
list={notInstalledPlugins}
searchText={trimmedSearchText}
category={PluginCategoryEnum.trigger}
tags={tags}
@@ -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 (
<div className={cn('w-100 max-w-full min-w-0', className)}>
@@ -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(<AgentStrategySelector onChange={vi.fn()} />, {
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()
@@ -116,8 +116,12 @@ export const AgentStrategySelector = memo((props: AgentStrategySelectorProps) =>
const wrapElemRef = useRef<HTMLDivElement>(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