From 22a4100dd7b43f2fc05575b28a67c8d30ea745fb Mon Sep 17 00:00:00 2001 From: yyh Date: Wed, 4 Mar 2026 22:33:17 +0800 Subject: [PATCH] fix(web): invalidate plugin checkInstalled cache after version updates --- .../__tests__/use-plugin-operations.spec.ts | 34 +++++++++++++++++++ .../hooks/use-plugin-operations.ts | 18 ++++++---- web/service/use-plugins.ts | 14 +++++++- 3 files changed, 59 insertions(+), 7 deletions(-) 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 15397ab6fc..0fcec7f16b 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 @@ -15,6 +15,7 @@ type VersionPickerMock = { const { mockSetShowUpdatePluginModal, mockRefreshModelProviders, + mockInvalidateCheckInstalled, mockInvalidateAllToolProviders, mockUninstallPlugin, mockFetchReleases, @@ -23,6 +24,7 @@ const { return { mockSetShowUpdatePluginModal: vi.fn(), mockRefreshModelProviders: vi.fn(), + mockInvalidateCheckInstalled: vi.fn(), mockInvalidateAllToolProviders: vi.fn(), mockUninstallPlugin: vi.fn(() => Promise.resolve({ success: true })), mockFetchReleases: vi.fn(() => Promise.resolve([{ tag_name: 'v2.0.0' }])), @@ -46,6 +48,10 @@ vi.mock('@/service/plugins', () => ({ uninstallPlugin: mockUninstallPlugin, })) +vi.mock('@/service/use-plugins', () => ({ + useInvalidateCheckInstalled: () => mockInvalidateCheckInstalled, +})) + vi.mock('@/service/use-tools', () => ({ useInvalidateAllToolProviders: () => mockInvalidateAllToolProviders, })) @@ -178,6 +184,7 @@ describe('usePluginOperations', () => { result.current.handleUpdatedFromMarketplace() }) + expect(mockInvalidateCheckInstalled).toHaveBeenCalled() expect(mockOnUpdate).toHaveBeenCalled() expect(modalStates.hideUpdateModal).toHaveBeenCalled() }) @@ -251,6 +258,32 @@ describe('usePluginOperations', () => { expect(mockSetShowUpdatePluginModal).toHaveBeenCalled() }) + it('should invalidate checkInstalled when GitHub update save callback fires', async () => { + const detail = createPluginDetail({ + source: PluginSource.github, + meta: { repo: 'owner/repo', version: 'v1.0.0', package: 'pkg' }, + }) + const { result } = renderHook(() => + usePluginOperations({ + detail, + modalStates, + versionPicker, + isFromMarketplace: false, + onUpdate: mockOnUpdate, + }), + ) + + await act(async () => { + await result.current.handleUpdate() + }) + + const firstCall = mockSetShowUpdatePluginModal.mock.calls.at(0)?.[0] + firstCall?.onSaveCallback() + + expect(mockInvalidateCheckInstalled).toHaveBeenCalled() + expect(mockOnUpdate).toHaveBeenCalled() + }) + it('should not show modal when no releases found', async () => { mockFetchReleases.mockResolvedValueOnce([]) const detail = createPluginDetail({ @@ -388,6 +421,7 @@ describe('usePluginOperations', () => { await result.current.handleDelete() }) + expect(mockInvalidateCheckInstalled).toHaveBeenCalled() expect(mockOnUpdate).toHaveBeenCalledWith(true) }) 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 fd8ebfa24c..bf6bb4aae6 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 @@ -9,6 +9,7 @@ import Toast from '@/app/components/base/toast' 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 { useGitHubReleases } from '../../../install-plugin/hooks' import { PluginCategoryEnum, PluginSource } from '../../../types' @@ -41,10 +42,15 @@ export const usePluginOperations = ({ const { checkForUpdates, fetchReleases } = useGitHubReleases() const { setShowUpdatePluginModal } = useModalContext() const { refreshModelProviders } = useProviderContext() + const invalidateCheckInstalled = useInvalidateCheckInstalled() const invalidateAllToolProviders = useInvalidateAllToolProviders() const { id, meta, plugin_id } = detail const { author, category, name } = detail.declaration || detail + const handlePluginUpdated = useCallback((isDelete?: boolean) => { + invalidateCheckInstalled() + onUpdate?.(isDelete) + }, [invalidateCheckInstalled, onUpdate]) const handleUpdate = useCallback(async (isDowngrade?: boolean) => { if (isFromMarketplace) { @@ -73,7 +79,7 @@ export const usePluginOperations = ({ if (needUpdate) { setShowUpdatePluginModal({ onSaveCallback: () => { - onUpdate?.() + handlePluginUpdated() }, payload: { type: PluginSource.github, @@ -99,15 +105,15 @@ export const usePluginOperations = ({ checkForUpdates, setShowUpdatePluginModal, detail, - onUpdate, + handlePluginUpdated, modalStates, versionPicker, ]) const handleUpdatedFromMarketplace = useCallback(() => { - onUpdate?.() + handlePluginUpdated() modalStates.hideUpdateModal() - }, [onUpdate, modalStates]) + }, [handlePluginUpdated, modalStates]) const handleDelete = useCallback(async () => { modalStates.showDeleting() @@ -120,7 +126,7 @@ export const usePluginOperations = ({ type: 'success', message: t('action.deleteSuccess', { ns: 'plugin' }), }) - onUpdate?.(true) + handlePluginUpdated(true) if (PluginCategoryEnum.model.includes(category)) refreshModelProviders() @@ -136,7 +142,7 @@ export const usePluginOperations = ({ plugin_id, name, modalStates, - onUpdate, + handlePluginUpdated, refreshModelProviders, invalidateAllToolProviders, ]) diff --git a/web/service/use-plugins.ts b/web/service/use-plugins.ts index ea32cf8ab7..9a6c6a7e2f 100644 --- a/web/service/use-plugins.ts +++ b/web/service/use-plugins.ts @@ -47,6 +47,7 @@ import { useInvalidateAllBuiltInTools } from './use-tools' const NAME_SPACE = 'plugins' const useInstalledPluginListKey = [NAME_SPACE, 'installedPluginList'] +const useCheckInstalledKey = [NAME_SPACE, 'checkInstalled'] as const export const useCheckInstalled = ({ pluginIds, enabled, @@ -55,7 +56,7 @@ export const useCheckInstalled = ({ enabled: boolean }) => { return useQuery<{ plugins: PluginDetail[] }>({ - queryKey: [NAME_SPACE, 'checkInstalled', pluginIds], + queryKey: [...useCheckInstalledKey, pluginIds], queryFn: () => post<{ plugins: PluginDetail[] }>('/workspaces/current/plugin/list/installations/ids', { body: { plugin_ids: pluginIds, @@ -66,6 +67,17 @@ export const useCheckInstalled = ({ }) } +export const useInvalidateCheckInstalled = () => { + const queryClient = useQueryClient() + return () => { + queryClient.invalidateQueries( + { + queryKey: useCheckInstalledKey, + }, + ) + } +} + const useRecommendedMarketplacePluginsKey = [NAME_SPACE, 'recommendedMarketplacePlugins'] export const useRecommendedMarketplacePlugins = ({ collection = '__recommended-plugins-tools',