fix(web): invalidate plugin checkInstalled cache after version updates

This commit is contained in:
yyh
2026-03-04 22:33:17 +08:00
parent 0f7ed6f67e
commit 22a4100dd7
3 changed files with 59 additions and 7 deletions

View File

@ -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)
})

View File

@ -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,
])

View File

@ -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',