mirror of
https://github.com/langgenius/dify.git
synced 2026-07-21 02:28:30 +08:00
chore: show package installed status in marketplace page (#39146)
This commit is contained in:
parent
e9c0e9de9e
commit
f4ad6ce978
@ -13,7 +13,11 @@ import {
|
||||
useInvalidDataSourceListAuth,
|
||||
} from '@/service/use-datasource'
|
||||
import { useInvalidDataSourceList } from '@/service/use-pipeline'
|
||||
import { useInstalledPluginList, useInvalidateInstalledPluginList } from '@/service/use-plugins'
|
||||
import {
|
||||
useCheckInstalled,
|
||||
useInstalledPluginList,
|
||||
useInvalidateInstalledPluginList,
|
||||
} from '@/service/use-plugins'
|
||||
import { useDataSourceAuthUpdate, useMarketplaceAllPlugins } from '../hooks'
|
||||
import DataSourcePage from '../index'
|
||||
|
||||
@ -42,6 +46,7 @@ vi.mock('@/service/use-pipeline', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-plugins', () => ({
|
||||
useCheckInstalled: vi.fn(),
|
||||
useInstalledPluginList: vi.fn(),
|
||||
useInvalidateInstalledPluginList: vi.fn(),
|
||||
}))
|
||||
@ -186,6 +191,11 @@ describe('DataSourcePage Component', () => {
|
||||
vi.mocked(useInstalledPluginList).mockReturnValue({
|
||||
data: { plugins: [], total: 0 },
|
||||
} as unknown as ReturnType<typeof useInstalledPluginList>)
|
||||
vi.mocked(useCheckInstalled).mockReturnValue({
|
||||
data: { plugins: [] },
|
||||
isLoading: false,
|
||||
error: null,
|
||||
} as unknown as ReturnType<typeof useCheckInstalled>)
|
||||
vi.mocked(usePluginsWithLatestVersion).mockImplementation(
|
||||
(plugins = []) => plugins as PluginDetail[],
|
||||
)
|
||||
|
||||
@ -4,6 +4,7 @@ import { PluginCategoryEnum } from '../../../types'
|
||||
|
||||
// Mock invalidation / refresh functions
|
||||
const mockInvalidateInstalledPluginList = vi.fn()
|
||||
const mockInvalidateCheckInstalled = vi.fn()
|
||||
const mockRefetchLLMModelList = vi.fn()
|
||||
const mockRefetchEmbeddingModelList = vi.fn()
|
||||
const mockRefetchRerankModelList = vi.fn()
|
||||
@ -20,6 +21,7 @@ const mockInvalidateAllTriggerPlugins = vi.fn()
|
||||
const mockInvalidateRAGRecommendedPlugins = vi.fn()
|
||||
|
||||
vi.mock('@/service/use-plugins', () => ({
|
||||
useInvalidateCheckInstalled: () => mockInvalidateCheckInstalled,
|
||||
useInvalidateInstalledPluginList: () => mockInvalidateInstalledPluginList,
|
||||
}))
|
||||
|
||||
@ -80,12 +82,13 @@ describe('useRefreshPluginList', () => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should always invalidate installed plugin list', () => {
|
||||
it('should always invalidate installed plugin list and installation checks', () => {
|
||||
const { result } = renderHook(() => useRefreshPluginList())
|
||||
|
||||
result.current.refreshPluginList()
|
||||
|
||||
expect(mockInvalidateInstalledPluginList).toHaveBeenCalledTimes(1)
|
||||
expect(mockInvalidateCheckInstalled).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should refresh tool providers for tool category manifest', () => {
|
||||
|
||||
@ -7,7 +7,10 @@ import {
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { useInvalidDataSourceListAuth } from '@/service/use-datasource'
|
||||
import { useInvalidDataSourceList } from '@/service/use-pipeline'
|
||||
import { useInvalidateInstalledPluginList } from '@/service/use-plugins'
|
||||
import {
|
||||
useInvalidateCheckInstalled,
|
||||
useInvalidateInstalledPluginList,
|
||||
} from '@/service/use-plugins'
|
||||
import { useInvalidateStrategyProviders } from '@/service/use-strategy'
|
||||
import {
|
||||
useInvalidateAllBuiltInTools,
|
||||
@ -31,6 +34,7 @@ const SYSTEM_MODEL_TYPES = [
|
||||
|
||||
const useRefreshPluginList = () => {
|
||||
const invalidateInstalledPluginList = useInvalidateInstalledPluginList()
|
||||
const invalidateCheckInstalled = useInvalidateCheckInstalled()
|
||||
const { mutate: refetchLLMModelList } = useModelList(ModelTypeEnum.textGeneration)
|
||||
const { mutate: refetchEmbeddingModelList } = useModelList(ModelTypeEnum.textEmbedding)
|
||||
const { mutate: refetchRerankModelList } = useModelList(ModelTypeEnum.rerank)
|
||||
@ -57,6 +61,7 @@ const useRefreshPluginList = () => {
|
||||
) => {
|
||||
// installed list
|
||||
invalidateInstalledPluginList()
|
||||
invalidateCheckInstalled()
|
||||
|
||||
// tool page, tool select
|
||||
if ((manifest && PluginCategoryEnum.tool.includes(manifest.category)) || refreshAllType) {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import type { ComponentProps } from 'react'
|
||||
import type { Plugin } from '@/app/components/plugins/types'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { ThemeProvider } from 'next-themes'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { PluginCategoryEnum } from '@/app/components/plugins/types'
|
||||
@ -104,6 +105,17 @@ describe('CardWrapper', () => {
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows a disabled installed action and prevents another installation', async () => {
|
||||
const user = userEvent.setup()
|
||||
renderCardWrapper({ showInstallButton: true, isInstalled: true })
|
||||
|
||||
const installedButton = screen.getByRole('button', { name: 'plugin.task.installed' })
|
||||
expect(installedButton).toBeDisabled()
|
||||
|
||||
await user.click(installedButton)
|
||||
expect(screen.queryByTestId('install-modal')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('opens marketplace detail from the detail action', () => {
|
||||
const openSpy = vi.spyOn(window, 'open').mockImplementation(() => null)
|
||||
|
||||
|
||||
@ -24,6 +24,7 @@ vi.mock('#i18n', async () => {
|
||||
'plugin.marketplace.noPluginFound': 'No plugins found',
|
||||
'plugin.detailPanel.operation.install': 'Install',
|
||||
'plugin.detailPanel.operation.detail': 'Detail',
|
||||
'plugin.task.installed': 'Installed',
|
||||
}
|
||||
return translations[fullKey] || key
|
||||
}),
|
||||
@ -50,6 +51,19 @@ vi.mock('../../state', () => ({
|
||||
useMarketplaceData: () => mockMarketplaceData,
|
||||
}))
|
||||
|
||||
const mockUseCheckInstalled = vi.hoisted(() => vi.fn())
|
||||
vi.mock('@/app/components/plugins/install-plugin/hooks/use-check-installed', () => ({
|
||||
default: mockUseCheckInstalled,
|
||||
}))
|
||||
|
||||
beforeEach(() => {
|
||||
mockUseCheckInstalled.mockReturnValue({
|
||||
installedInfo: undefined,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
})
|
||||
})
|
||||
|
||||
vi.mock('../../atoms', () => ({
|
||||
useMarketplaceMoreClick: () => mockMoreClick,
|
||||
}))
|
||||
@ -991,6 +1005,64 @@ describe('CardWrapper (via List integration)', () => {
|
||||
expect(screen.getByText('Detail')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render installed state for every card with an installed plugin id', () => {
|
||||
const installedPlugin = createMockPlugin({
|
||||
name: 'installed-plugin',
|
||||
plugin_id: 'plugin-a',
|
||||
})
|
||||
const duplicateInstalledPlugin = createMockPlugin({
|
||||
name: 'installed-plugin-duplicate',
|
||||
plugin_id: 'plugin-a',
|
||||
})
|
||||
const uninstalledPlugin = createMockPlugin({
|
||||
name: 'uninstalled-plugin',
|
||||
plugin_id: 'plugin-b',
|
||||
})
|
||||
mockUseCheckInstalled.mockReturnValue({
|
||||
installedInfo: {
|
||||
'plugin-a': {
|
||||
installedId: 'installation-a',
|
||||
installedVersion: '1.0.0',
|
||||
uniqueIdentifier: 'plugin-a@1.0.0',
|
||||
},
|
||||
},
|
||||
isLoading: false,
|
||||
error: null,
|
||||
})
|
||||
|
||||
render(
|
||||
<List
|
||||
marketplaceCollections={[]}
|
||||
marketplaceCollectionPluginsMap={{}}
|
||||
plugins={[installedPlugin, duplicateInstalledPlugin, uninstalledPlugin]}
|
||||
showInstallButton={true}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getAllByRole('button', { name: 'Installed' })).toHaveLength(2)
|
||||
expect(screen.getByRole('button', { name: 'Install' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should keep install available while installation status is pending', () => {
|
||||
const plugin = createMockPlugin({ name: 'pending-status-plugin' })
|
||||
mockUseCheckInstalled.mockReturnValue({
|
||||
installedInfo: undefined,
|
||||
isLoading: true,
|
||||
error: null,
|
||||
})
|
||||
|
||||
render(
|
||||
<List
|
||||
marketplaceCollections={[]}
|
||||
marketplaceCollectionPluginsMap={{}}
|
||||
plugins={[plugin]}
|
||||
showInstallButton={true}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Install' })).not.toHaveAttribute('aria-disabled')
|
||||
})
|
||||
|
||||
it('should call showInstallFromMarketplace when install button is clicked', () => {
|
||||
const plugin = createMockPlugin({ name: 'click-test-plugin' })
|
||||
|
||||
|
||||
@ -16,8 +16,13 @@ import { getPluginLinkInMarketplace } from '../utils'
|
||||
type CardWrapperProps = {
|
||||
plugin: Plugin
|
||||
showInstallButton?: boolean
|
||||
isInstalled?: boolean
|
||||
}
|
||||
const CardWrapperComponent = ({ plugin, showInstallButton }: CardWrapperProps) => {
|
||||
const CardWrapperComponent = ({
|
||||
plugin,
|
||||
showInstallButton,
|
||||
isInstalled = false,
|
||||
}: CardWrapperProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { theme } = useTheme()
|
||||
const [
|
||||
@ -68,11 +73,14 @@ const CardWrapperComponent = ({ plugin, showInstallButton }: CardWrapperProps) =
|
||||
/>
|
||||
<div className="pointer-events-none absolute right-[-0.5px] bottom-[-0.5px] left-[-0.5px] z-10 flex items-center gap-2 rounded-b-xl bg-linear-to-t from-components-panel-on-panel-item-bg-hover from-[60%] to-background-gradient-mask-transparent px-4 pt-8 pb-4 opacity-0 transition-opacity group-hover:pointer-events-auto group-hover:opacity-100">
|
||||
<Button
|
||||
variant="primary"
|
||||
variant={isInstalled ? 'secondary' : 'primary'}
|
||||
className="min-w-0 flex-1 shadow-md"
|
||||
onClick={showInstallFromMarketplace}
|
||||
disabled={isInstalled}
|
||||
onClick={isInstalled ? undefined : showInstallFromMarketplace}
|
||||
>
|
||||
{t(($) => $['detailPanel.operation.install'], { ns: 'plugin' })}
|
||||
{isInstalled
|
||||
? t(($) => $['task.installed'], { ns: 'plugin' })
|
||||
: t(($) => $['detailPanel.operation.install'], { ns: 'plugin' })}
|
||||
</Button>
|
||||
<Button
|
||||
className="min-w-0 flex-1 gap-0.5 shadow-xs backdrop-blur-[5px]"
|
||||
|
||||
@ -2,7 +2,10 @@
|
||||
import type { MarketplaceCollection, SearchParamsFromCollection } from '@dify/contracts/marketplace'
|
||||
import type { Plugin } from '../../types'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { useMemo } from 'react'
|
||||
import { PluginInstallPermissionProviderGuard } from '@/app/components/plugins/install-plugin/components/plugin-install-permission-provider'
|
||||
import useCheckInstalled from '@/app/components/plugins/install-plugin/hooks/use-check-installed'
|
||||
import { useOptionalPluginInstallPermission } from '@/app/components/plugins/install-plugin/hooks/use-plugin-install-permission'
|
||||
import Empty from '../empty'
|
||||
import CardWrapper from './card-wrapper'
|
||||
import ListWithCollection from './list-with-collection'
|
||||
@ -27,6 +30,31 @@ const List = ({
|
||||
emptyClassName,
|
||||
onCollectionMoreClick,
|
||||
}: ListProps) => {
|
||||
const { canInstallPlugin } = useOptionalPluginInstallPermission()
|
||||
const pluginIds = useMemo(() => {
|
||||
const ids = new Set<string>()
|
||||
const addPluginId = (plugin: Plugin) => ids.add(plugin.plugin_id)
|
||||
|
||||
if (plugins) plugins.forEach(addPluginId)
|
||||
else
|
||||
Object.values(marketplaceCollectionPluginsMap).forEach((collectionPlugins) => {
|
||||
collectionPlugins.forEach(addPluginId)
|
||||
})
|
||||
|
||||
return [...ids].sort()
|
||||
}, [marketplaceCollectionPluginsMap, plugins])
|
||||
|
||||
const shouldCheckInstalled =
|
||||
!!showInstallButton && canInstallPlugin && !cardRender && pluginIds.length > 0
|
||||
const { installedInfo } = useCheckInstalled({
|
||||
pluginIds,
|
||||
enabled: shouldCheckInstalled,
|
||||
})
|
||||
const installedPluginIds = useMemo(
|
||||
() => new Set(Object.keys(installedInfo ?? {})),
|
||||
[installedInfo],
|
||||
)
|
||||
|
||||
return (
|
||||
<PluginInstallPermissionProviderGuard canInstallPlugin={!!showInstallButton}>
|
||||
{!plugins && (
|
||||
@ -37,6 +65,7 @@ const List = ({
|
||||
cardContainerClassName={cardContainerClassName}
|
||||
cardRender={cardRender}
|
||||
onCollectionMoreClick={onCollectionMoreClick}
|
||||
installedPluginIds={installedPluginIds}
|
||||
/>
|
||||
)}
|
||||
{plugins && !!plugins.length && (
|
||||
@ -49,6 +78,7 @@ const List = ({
|
||||
key={`${plugin.org}/${plugin.name}`}
|
||||
plugin={plugin}
|
||||
showInstallButton={showInstallButton}
|
||||
isInstalled={installedPluginIds.has(plugin.plugin_id)}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
|
||||
@ -38,18 +38,22 @@ type ListWithCollectionProps = {
|
||||
cardContainerClassName?: string
|
||||
cardRender?: (plugin: Plugin) => React.JSX.Element | null
|
||||
onCollectionMoreClick?: (searchParams?: SearchParamsFromCollection) => void
|
||||
installedPluginIds?: ReadonlySet<string>
|
||||
}
|
||||
|
||||
type PluginCardProps = {
|
||||
plugin: Plugin
|
||||
showInstallButton?: boolean
|
||||
cardRender?: (plugin: Plugin) => React.JSX.Element | null
|
||||
isInstalled?: boolean
|
||||
}
|
||||
|
||||
const PluginCard = ({ plugin, showInstallButton, cardRender }: PluginCardProps) => {
|
||||
const PluginCard = ({ plugin, showInstallButton, cardRender, isInstalled }: PluginCardProps) => {
|
||||
if (cardRender) return cardRender(plugin)
|
||||
|
||||
return <CardWrapper plugin={plugin} showInstallButton={showInstallButton} />
|
||||
return (
|
||||
<CardWrapper plugin={plugin} showInstallButton={showInstallButton} isInstalled={isInstalled} />
|
||||
)
|
||||
}
|
||||
|
||||
const ListWithCollection = ({
|
||||
@ -59,6 +63,7 @@ const ListWithCollection = ({
|
||||
cardContainerClassName,
|
||||
cardRender,
|
||||
onCollectionMoreClick,
|
||||
installedPluginIds,
|
||||
}: ListWithCollectionProps) => {
|
||||
const { t } = useTranslation()
|
||||
const locale = useLocale()
|
||||
@ -143,6 +148,7 @@ const ListWithCollection = ({
|
||||
plugin={plugin}
|
||||
showInstallButton={showInstallButton}
|
||||
cardRender={cardRender}
|
||||
isInstalled={installedPluginIds?.has(plugin.plugin_id)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
@ -158,6 +164,7 @@ const ListWithCollection = ({
|
||||
plugin={plugin}
|
||||
showInstallButton={showInstallButton}
|
||||
cardRender={cardRender}
|
||||
isInstalled={installedPluginIds?.has(plugin.plugin_id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user