diff --git a/web/app/components/plugins/install-plugin/hooks/__tests__/use-refresh-plugin-list.spec.ts b/web/app/components/plugins/install-plugin/hooks/__tests__/use-refresh-plugin-list.spec.ts index ce228d923f8..44400aa9cc7 100644 --- a/web/app/components/plugins/install-plugin/hooks/__tests__/use-refresh-plugin-list.spec.ts +++ b/web/app/components/plugins/install-plugin/hooks/__tests__/use-refresh-plugin-list.spec.ts @@ -7,6 +7,9 @@ const mockInvalidateInstalledPluginList = vi.fn() const mockRefetchLLMModelList = vi.fn() const mockRefetchEmbeddingModelList = vi.fn() const mockRefetchRerankModelList = vi.fn() +const mockRefetchSpeech2textModelList = vi.fn() +const mockRefetchTTSModelList = vi.fn() +const mockInvalidateDefaultModel = vi.fn() const mockRefreshModelProviders = vi.fn() const mockInvalidateAllToolProviders = vi.fn() const mockInvalidateAllBuiltInTools = vi.fn() @@ -21,18 +24,21 @@ vi.mock('@/service/use-plugins', () => ({ })) vi.mock('@/app/components/header/account-setting/model-provider-page/declarations', () => ({ - ModelTypeEnum: { textGeneration: 'text-generation', textEmbedding: 'text-embedding', rerank: 'rerank' }, + ModelTypeEnum: { textGeneration: 'llm', textEmbedding: 'text-embedding', rerank: 'rerank', speech2text: 'speech2text', tts: 'tts' }, })) vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({ useModelList: (type: string) => { const map: Record }> = { - 'text-generation': { mutate: mockRefetchLLMModelList }, + 'llm': { mutate: mockRefetchLLMModelList }, 'text-embedding': { mutate: mockRefetchEmbeddingModelList }, 'rerank': { mutate: mockRefetchRerankModelList }, + 'speech2text': { mutate: mockRefetchSpeech2textModelList }, + 'tts': { mutate: mockRefetchTTSModelList }, } return map[type] ?? { mutate: vi.fn() } }, + useInvalidateDefaultModel: () => mockInvalidateDefaultModel, })) vi.mock('@/context/provider-context', () => ({ @@ -95,6 +101,14 @@ describe('useRefreshPluginList', () => { expect(mockRefetchLLMModelList).toHaveBeenCalledTimes(1) expect(mockRefetchEmbeddingModelList).toHaveBeenCalledTimes(1) expect(mockRefetchRerankModelList).toHaveBeenCalledTimes(1) + expect(mockRefetchSpeech2textModelList).toHaveBeenCalledTimes(1) + expect(mockRefetchTTSModelList).toHaveBeenCalledTimes(1) + expect(mockInvalidateDefaultModel).toHaveBeenCalledTimes(5) + expect(mockInvalidateDefaultModel).toHaveBeenCalledWith('llm') + expect(mockInvalidateDefaultModel).toHaveBeenCalledWith('text-embedding') + expect(mockInvalidateDefaultModel).toHaveBeenCalledWith('rerank') + expect(mockInvalidateDefaultModel).toHaveBeenCalledWith('speech2text') + expect(mockInvalidateDefaultModel).toHaveBeenCalledWith('tts') }) it('should refresh datasource lists for datasource category manifest', () => { @@ -138,6 +152,9 @@ describe('useRefreshPluginList', () => { expect(mockRefetchLLMModelList).toHaveBeenCalledTimes(1) expect(mockRefetchEmbeddingModelList).toHaveBeenCalledTimes(1) expect(mockRefetchRerankModelList).toHaveBeenCalledTimes(1) + expect(mockRefetchSpeech2textModelList).toHaveBeenCalledTimes(1) + expect(mockRefetchTTSModelList).toHaveBeenCalledTimes(1) + expect(mockInvalidateDefaultModel).toHaveBeenCalledTimes(5) expect(mockInvalidateStrategyProviders).toHaveBeenCalledTimes(1) }) diff --git a/web/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list.tsx b/web/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list.tsx index 083503211e9..f02dcf7e819 100644 --- a/web/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list.tsx +++ b/web/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list.tsx @@ -1,6 +1,6 @@ import type { Plugin, PluginDeclaration, PluginManifestInMarket } from '../../types' import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' -import { useModelList } from '@/app/components/header/account-setting/model-provider-page/hooks' +import { useInvalidateDefaultModel, useModelList } from '@/app/components/header/account-setting/model-provider-page/hooks' import { useProviderContext } from '@/context/provider-context' import { useInvalidDataSourceListAuth } from '@/service/use-datasource' import { useInvalidDataSourceList } from '@/service/use-pipeline' @@ -14,11 +14,22 @@ type PluginCategoryPayload = { category: PluginCategoryEnum | string } +const SYSTEM_MODEL_TYPES = [ + ModelTypeEnum.textGeneration, + ModelTypeEnum.textEmbedding, + ModelTypeEnum.rerank, + ModelTypeEnum.speech2text, + ModelTypeEnum.tts, +] + const useRefreshPluginList = () => { const invalidateInstalledPluginList = useInvalidateInstalledPluginList() const { mutate: refetchLLMModelList } = useModelList(ModelTypeEnum.textGeneration) const { mutate: refetchEmbeddingModelList } = useModelList(ModelTypeEnum.textEmbedding) const { mutate: refetchRerankModelList } = useModelList(ModelTypeEnum.rerank) + const { mutate: refetchSpeech2textModelList } = useModelList(ModelTypeEnum.speech2text) + const { mutate: refetchTTSModelList } = useModelList(ModelTypeEnum.tts) + const invalidateDefaultModel = useInvalidateDefaultModel() const { refreshModelProviders } = useProviderContext() const invalidateAllToolProviders = useInvalidateAllToolProviders() @@ -59,6 +70,9 @@ const useRefreshPluginList = () => { refetchLLMModelList() refetchEmbeddingModelList() refetchRerankModelList() + refetchSpeech2textModelList() + refetchTTSModelList() + SYSTEM_MODEL_TYPES.forEach(type => invalidateDefaultModel(type)) } // agent select diff --git a/web/app/components/plugins/plugin-detail-panel/__tests__/operation-dropdown.spec.tsx b/web/app/components/plugins/plugin-detail-panel/__tests__/operation-dropdown.spec.tsx index 5a870c6c826..db2e7f1bdda 100644 --- a/web/app/components/plugins/plugin-detail-panel/__tests__/operation-dropdown.spec.tsx +++ b/web/app/components/plugins/plugin-detail-panel/__tests__/operation-dropdown.spec.tsx @@ -20,15 +20,15 @@ vi.mock('@langgenius/dify-ui/dropdown-menu', () => ({ DropdownMenuTrigger: ({ children, className }: { children: ReactNode, className?: string }) => ( ), - DropdownMenuContent: ({ children }: { children: ReactNode }) => ( -
{children}
+ DropdownMenuContent: ({ children, popupClassName }: { children: ReactNode, popupClassName?: string }) => ( +
{children}
), - DropdownMenuItem: ({ children, onClick, render, destructive }: { children: ReactNode, onClick?: () => void, render?: ReactElement, destructive?: boolean }) => { + DropdownMenuItem: ({ children, className, onClick, render, variant }: { children: ReactNode, className?: string, onClick?: () => void, render?: ReactElement, variant?: string }) => { if (render) - return cloneElement(render, { onClick, 'data-destructive': destructive } as Record, children) - return
{children}
+ return cloneElement(render, { className, onClick, 'data-variant': variant } as Record, children) + return
{children}
}, - DropdownMenuSeparator: () =>
, + DropdownMenuSeparator: ({ className }: { className?: string }) =>
, })) describe('OperationDropdown', () => { @@ -60,6 +60,15 @@ describe('OperationDropdown', () => { expect(screen.getByTestId('dropdown-content')).toBeInTheDocument() }) + it('should render figma-aligned dropdown surface and rows', () => { + render() + + expect(screen.getByTestId('dropdown-content')).toHaveClass('w-[192px]', 'py-1') + expect(screen.getByText('plugin.detailPanel.operation.viewDetail').closest('a')).toHaveClass('px-2', 'py-1', 'system-md-regular') + expect(screen.getByText('plugin.detailPanel.operation.remove').closest('[data-testid="dropdown-item"]')).toHaveClass('px-2', 'py-1', 'system-md-regular') + expect(screen.getByTestId('dropdown-separator')).toHaveClass('my-0') + }) + it('should render info option for github source', () => { render() @@ -90,6 +99,12 @@ describe('OperationDropdown', () => { expect(screen.getByText('plugin.detailPanel.operation.remove')).toBeInTheDocument() }) + it('should render remove option with normal text color', () => { + render() + + expect(screen.getByText('plugin.detailPanel.operation.remove').closest('[data-testid="dropdown-item"]')).not.toHaveAttribute('data-variant', 'destructive') + }) + it('should not render info option for marketplace source', () => { render() diff --git a/web/app/components/plugins/plugin-detail-panel/detail-header/hooks/__tests__/use-plugin-operations.spec.ts b/web/app/components/plugins/plugin-detail-panel/detail-header/hooks/__tests__/use-plugin-operations.spec.ts index 855530bf186..ddc50120023 100644 --- a/web/app/components/plugins/plugin-detail-panel/detail-header/hooks/__tests__/use-plugin-operations.spec.ts +++ b/web/app/components/plugins/plugin-detail-panel/detail-header/hooks/__tests__/use-plugin-operations.spec.ts @@ -13,9 +13,8 @@ type VersionPickerMock = { const { mockSetShowUpdatePluginModal, - mockRefreshModelProviders, mockInvalidateCheckInstalled, - mockInvalidateAllToolProviders, + mockRefreshPluginList, mockUninstallPlugin, mockFetchReleases, mockCheckForUpdates, @@ -23,9 +22,8 @@ const { } = vi.hoisted(() => { return { mockSetShowUpdatePluginModal: vi.fn(), - mockRefreshModelProviders: vi.fn(), mockInvalidateCheckInstalled: vi.fn(), - mockInvalidateAllToolProviders: vi.fn(), + mockRefreshPluginList: vi.fn(), mockUninstallPlugin: vi.fn(() => Promise.resolve({ success: true })), mockFetchReleases: vi.fn(() => Promise.resolve([{ tag_name: 'v2.0.0' }])), mockCheckForUpdates: vi.fn(() => ({ needUpdate: true, toastProps: { type: 'success', message: 'Update available' } })), @@ -54,9 +52,9 @@ vi.mock('@/context/modal-context', () => ({ }), })) -vi.mock('@/context/provider-context', () => ({ - useProviderContext: () => ({ - refreshModelProviders: mockRefreshModelProviders, +vi.mock('@/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list', () => ({ + default: () => ({ + refreshPluginList: mockRefreshPluginList, }), })) @@ -68,10 +66,6 @@ vi.mock('@/service/use-plugins', () => ({ useInvalidateCheckInstalled: () => mockInvalidateCheckInstalled, })) -vi.mock('@/service/use-tools', () => ({ - useInvalidateAllToolProviders: () => mockInvalidateAllToolProviders, -})) - vi.mock('../../../../install-plugin/hooks', async (importOriginal) => { const actual = await importOriginal() return { @@ -461,7 +455,7 @@ describe('usePluginOperations', () => { expect(modalStates.hideDeleteConfirm).toHaveBeenCalled() }) - it('should refresh model providers when deleting model plugin', async () => { + it('should refresh plugin list when deleting model plugin', async () => { const detail = createPluginDetail({ declaration: { author: 'test-author', @@ -487,10 +481,10 @@ describe('usePluginOperations', () => { await result.current.handleDelete() }) - expect(mockRefreshModelProviders).toHaveBeenCalled() + expect(mockRefreshPluginList).toHaveBeenCalledWith({ category: 'model' }) }) - it('should invalidate tool providers when deleting tool plugin', async () => { + it('should refresh plugin list when deleting tool plugin', async () => { const detail = createPluginDetail({ declaration: { author: 'test-author', @@ -516,7 +510,7 @@ describe('usePluginOperations', () => { await result.current.handleDelete() }) - expect(mockInvalidateAllToolProviders).toHaveBeenCalled() + expect(mockRefreshPluginList).toHaveBeenCalledWith({ category: 'tool' }) }) it('should track plugin uninstalled event', async () => { diff --git a/web/app/components/plugins/plugin-detail-panel/detail-header/hooks/use-plugin-operations.ts b/web/app/components/plugins/plugin-detail-panel/detail-header/hooks/use-plugin-operations.ts index 62c762a35d1..2931c4e7051 100644 --- a/web/app/components/plugins/plugin-detail-panel/detail-header/hooks/use-plugin-operations.ts +++ b/web/app/components/plugins/plugin-detail-panel/detail-header/hooks/use-plugin-operations.ts @@ -6,13 +6,12 @@ import { toast } from '@langgenius/dify-ui/toast' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' import { trackEvent } from '@/app/components/base/amplitude' +import useRefreshPluginList from '@/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list' import { useModalContext } from '@/context/modal-context' -import { useProviderContext } from '@/context/provider-context' import { uninstallPlugin } from '@/service/plugins' import { useInvalidateCheckInstalled } from '@/service/use-plugins' -import { useInvalidateAllToolProviders } from '@/service/use-tools' import { checkForUpdates, fetchReleases } from '../../../install-plugin/hooks' -import { PluginCategoryEnum, PluginSource } from '../../../types' +import { PluginSource } from '../../../types' type UsePluginOperationsParams = { detail: PluginDetail @@ -40,9 +39,8 @@ export const usePluginOperations = ({ }: UsePluginOperationsParams): UsePluginOperationsReturn => { const { t } = useTranslation() const { setShowUpdatePluginModal } = useModalContext() - const { refreshModelProviders } = useProviderContext() + const { refreshPluginList } = useRefreshPluginList() const invalidateCheckInstalled = useInvalidateCheckInstalled() - const invalidateAllToolProviders = useInvalidateAllToolProviders() const { id, meta, plugin_id } = detail const { author, category, name } = detail.declaration || detail @@ -120,12 +118,7 @@ export const usePluginOperations = ({ modalStates.hideDeleteConfirm() toast.success(t('action.deleteSuccess', { ns: 'plugin' })) handlePluginUpdated(true) - - if (PluginCategoryEnum.model.includes(category)) - refreshModelProviders() - - if (PluginCategoryEnum.tool.includes(category)) - invalidateAllToolProviders() + refreshPluginList({ category }) trackEvent('plugin_uninstalled', { plugin_id, plugin_name: name }) } @@ -136,8 +129,7 @@ export const usePluginOperations = ({ name, modalStates, handlePluginUpdated, - refreshModelProviders, - invalidateAllToolProviders, + refreshPluginList, ]) return { diff --git a/web/app/components/plugins/plugin-detail-panel/operation-dropdown.tsx b/web/app/components/plugins/plugin-detail-panel/operation-dropdown.tsx index 0b6f9879442..1332c1ec091 100644 --- a/web/app/components/plugins/plugin-detail-panel/operation-dropdown.tsx +++ b/web/app/components/plugins/plugin-detail-panel/operation-dropdown.tsx @@ -28,6 +28,10 @@ type Props = { triggerSize?: 'm' | 'xs' } +const operationMenuPopupClassName = 'w-[192px] py-1' +const operationMenuItemClassName = 'px-2 py-1 text-text-secondary system-md-regular' +const operationMenuLabelClassName = 'min-w-0 grow truncate px-1 py-0.5' + const OperationDropdown: FC = ({ source, detailUrl, @@ -57,29 +61,29 @@ const OperationDropdown: FC = ({ placement={placement} sideOffset={sideOffset} alignOffset={alignOffset} - popupClassName={cn('w-auto min-w-[160px]', popupClassName)} + popupClassName={cn(operationMenuPopupClassName, popupClassName)} > {source === PluginSource.github && ( - - {t('detailPanel.operation.info', { ns: 'plugin' })} + + {t('detailPanel.operation.info', { ns: 'plugin' })} )} {source === PluginSource.github && ( - - {t('detailPanel.operation.checkUpdate', { ns: 'plugin' })} + + {t('detailPanel.operation.checkUpdate', { ns: 'plugin' })} )} {(source === PluginSource.marketplace || source === PluginSource.github) && enable_marketplace && ( - }> - {t('detailPanel.operation.viewDetail', { ns: 'plugin' })} + }> + {t('detailPanel.operation.viewDetail', { ns: 'plugin' })} )} {(source === PluginSource.marketplace || source === PluginSource.github) && enable_marketplace && ( - + )} - - {t('detailPanel.operation.remove', { ns: 'plugin' })} + + {t('detailPanel.operation.remove', { ns: 'plugin' })} diff --git a/web/app/components/plugins/update-plugin/__tests__/plugin-version-picker.spec.tsx b/web/app/components/plugins/update-plugin/__tests__/plugin-version-picker.spec.tsx index b65c6a6e42c..556b64a620c 100644 --- a/web/app/components/plugins/update-plugin/__tests__/plugin-version-picker.spec.tsx +++ b/web/app/components/plugins/update-plugin/__tests__/plugin-version-picker.spec.tsx @@ -61,6 +61,29 @@ describe('PluginVersionPicker', () => { expect(screen.getByText('CURRENT')).toBeInTheDocument() }) + it('renders figma-aligned version rows', () => { + render( + trigger} + onSelect={vi.fn()} + />, + ) + + const currentVersion = screen.getByText('2.0.0') + const currentBadge = screen.getByText('CURRENT') + const oldVersion = screen.getByText('1.0.0') + + expect(screen.getByText('plugin.detailPanel.switchVersion')).toHaveClass('px-3', 'pb-0.5', 'pt-1') + expect(currentVersion.closest('.cursor-default')).toHaveClass('px-2', 'py-1', 'opacity-30') + expect(oldVersion.closest('.cursor-pointer')).toHaveClass('px-2', 'py-1') + expect(currentVersion.parentElement).toHaveClass('min-h-5', 'gap-1', 'px-1') + expect(currentBadge).toHaveClass('bg-components-badge-bg-dimm') + }) + it('calls onSelect with downgrade metadata and closes the picker', () => { const onSelect = vi.fn() const onShowChange = vi.fn() diff --git a/web/app/components/plugins/update-plugin/plugin-version-picker.tsx b/web/app/components/plugins/update-plugin/plugin-version-picker.tsx index 458947da6bb..bc2b13bffa2 100644 --- a/web/app/components/plugins/update-plugin/plugin-version-picker.tsx +++ b/web/app/components/plugins/update-plugin/plugin-version-picker.tsx @@ -84,7 +84,7 @@ const PluginVersionPicker: FC = ({ placement={placement} sideOffset={sideOffset} alignOffset={alignOffset} - popupClassName="relative w-[209px] bg-components-panel-bg-blur p-1 backdrop-blur-xs" + popupClassName="relative w-[209px] bg-components-panel-bg-blur p-1 backdrop-blur-[5px]" >
{t('detailPanel.switchVersion', { ns: 'plugin' })} @@ -94,7 +94,7 @@ const PluginVersionPicker: FC = ({
handleSelect({ @@ -103,11 +103,11 @@ const PluginVersionPicker: FC = ({ isDowngrade: isEarlierThanVersion(version.version, currentVersion), })} > -
-
{version.version}
- {currentVersion === version.version && } +
+
{version.version}
+ {currentVersion === version.version && } +
{formatDate(version.created_at, format!)}
-
{formatDate(version.created_at, format!)}
))}