From 791829fdb0df560498d9f129d4de16e0f2d26abc Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Fri, 16 Oct 2026 06:49:00 +0800 Subject: [PATCH] refactor(web): use generated tool options directly Remove the forwarding hooks in service/use-tools. Consumers call the generated tool list, MCP provider, MCP server, app trigger, workflow tool detail and RAG recommendation queryOptions and mutationOptions directly, adapt responses to the tool UI types with select adapters in tools/utils/query-select, and invalidate generated keys inline. The provider-type to tool list key mapping lives in tools/utils/tool-list-query-key. Specs answer generated operations through mockConsoleOperationHooks and assert invalidation through cache state. Co-Authored-By: Claude Opus 5.5 --- oxlint-suppressions.json | 5 - .../access-point/__tests__/mcp-card.spec.tsx | 62 +-- .../__tests__/trigger-card.spec.tsx | 36 +- .../built-in-access-points/mcp-card.tsx | 34 +- .../built-in-access-points/trigger-card.tsx | 23 +- .../environment-deployment-flow.spec.tsx | 15 +- .../app-publisher/__tests__/index.spec.tsx | 17 +- .../agent-tools/__tests__/index.spec.tsx | 15 +- .../config/agent/agent-tools/index.tsx | 29 +- .../app/deploy/__tests__/index.spec.tsx | 39 +- .../built-in-environment-card/index.tsx | 7 +- .../__tests__/use-provider-icon.spec.tsx | 15 +- .../use-provider-icon.ts | 35 +- .../try-app/preview/basic-app-preview.tsx | 15 +- .../__tests__/tool-provider-list.spec.tsx | 21 +- .../integrations/tool-provider-list.tsx | 31 +- .../__tests__/use-refresh-plugin-list.spec.ts | 34 +- .../hooks/use-refresh-plugin-list.tsx | 24 +- .../__tests__/authorized-in-node.spec.tsx | 4 - .../__tests__/plugin-auth-in-agent.spec.tsx | 4 - .../hooks/__tests__/use-credential.spec.ts | 21 +- .../plugin-auth/hooks/use-credential.ts | 9 +- .../plugin-detail-panel/action-list.tsx | 22 +- .../detail-header/__tests__/index.spec.tsx | 25 +- .../detail-header/index.tsx | 11 +- .../__tests__/focus-restoration.spec.tsx | 21 +- .../__tests__/index.spec.tsx | 13 +- .../multiple-tool-selector/index.tsx | 8 +- .../hooks/__tests__/use-tool-selector.spec.ts | 165 ++++---- .../tool-selector/hooks/use-tool-selector.ts | 38 +- .../marketplace/__tests__/index.spec.tsx | 5 - .../tools/mcp/__tests__/create-card.spec.tsx | 19 +- .../tools/mcp/__tests__/index.spec.tsx | 47 ++- .../mcp/__tests__/mcp-server-modal.spec.tsx | 24 +- web/app/components/tools/mcp/create-card.tsx | 17 +- .../mcp/detail/__tests__/content.spec.tsx | 60 +-- .../components/tools/mcp/detail/content.tsx | 55 ++- web/app/components/tools/mcp/index.tsx | 26 +- .../components/tools/mcp/mcp-server-modal.tsx | 56 +-- web/app/components/tools/mcp/modal.tsx | 3 +- .../tools/provider/__tests__/detail.spec.tsx | 4 - web/app/components/tools/provider/detail.tsx | 6 +- web/app/components/tools/types.ts | 12 + .../components/tools/utils/query-select.ts | 47 +++ .../tools/utils/tool-list-query-key.ts | 20 + .../__tests__/use-configure-button.spec.ts | 287 ++++++++------ .../hooks/use-configure-button.ts | 49 ++- .../workflow-app/__tests__/index.spec.tsx | 9 +- .../__tests__/provider-contract.spec.tsx | 11 +- .../__tests__/features-trigger.spec.tsx | 17 +- .../workflow-header/features-trigger.tsx | 9 +- web/app/components/workflow-app/index.tsx | 17 +- .../__tests__/service-mock-factory.ts | 17 +- .../__tests__/workflow-edge-events.spec.tsx | 15 +- .../block-selector/__tests__/index.spec.tsx | 16 +- .../block-selector/__tests__/tabs.spec.tsx | 28 +- .../__tests__/tool-picker.spec.tsx | 112 +++--- .../rag-tool-recommendations/index.tsx | 11 +- .../workflow/block-selector/tool-panel.tsx | 39 +- .../workflow/block-selector/tool-picker.tsx | 56 +-- .../__tests__/use-available-blocks.spec.ts | 7 +- .../hooks/__tests__/use-checklist.spec.ts | 18 +- .../use-fetch-workflow-inspect-vars.spec.ts | 7 +- .../use-inspect-vars-crud-common.spec.ts | 7 +- .../use-node-plugin-installation.spec.ts | 40 +- .../__tests__/use-nodes-meta-data.spec.tsx | 13 +- .../hooks/__tests__/use-tool-icon.spec.ts | 37 +- .../__tests__/use-workflow-search.spec.tsx | 31 +- .../__tests__/use-workflow-variables.spec.ts | 7 +- .../workflow/hooks/use-checklist.ts | 43 ++- .../hooks/use-fetch-workflow-inspect-vars.ts | 29 +- .../hooks/use-inspect-vars-crud-common.ts | 29 +- .../hooks/use-node-plugin-installation.ts | 46 ++- .../workflow/hooks/use-nodes-meta-data.ts | 20 +- .../workflow/hooks/use-tool-icon.ts | 49 ++- .../workflow/hooks/use-workflow-search.tsx | 29 +- .../workflow/hooks/use-workflow-variables.ts | 29 +- web/app/components/workflow/index.tsx | 33 +- .../__tests__/details.spec.tsx | 20 +- .../__tests__/index.spec.tsx | 13 +- .../use-node-actions-menu-model.ts | 10 +- .../__tests__/node-focus.browser.spec.tsx | 19 +- .../components/__tests__/node-handle.spec.tsx | 16 +- .../workflow-panel/__tests__/index.spec.tsx | 13 +- .../_base/components/workflow-panel/index.tsx | 11 +- .../hooks/__tests__/use-one-step-run.spec.ts | 15 +- .../nodes/_base/hooks/use-one-step-run.ts | 29 +- .../agent/__tests__/integration.spec.tsx | 18 +- .../components/__tests__/tool-icon.spec.tsx | 15 +- .../nodes/agent/components/tool-icon.tsx | 29 +- .../if-else/__tests__/integration.spec.tsx | 15 +- .../condition-list/condition-item.tsx | 29 +- .../iteration/__tests__/use-config.spec.ts | 15 +- .../workflow/nodes/iteration/use-config.ts | 29 +- .../nodes/loop/__tests__/use-config.spec.tsx | 15 +- .../workflow/nodes/loop/use-config.ts | 29 +- .../__tests__/import-from-tool.spec.tsx | 16 +- .../extract-parameter/import-from-tool.tsx | 20 +- .../tool/hooks/__tests__/use-config.spec.tsx | 30 +- .../use-current-tool-collection.spec.tsx | 15 +- .../workflow/nodes/tool/hooks/use-config.ts | 10 +- .../tool/hooks/use-current-tool-collection.ts | 29 +- .../operator/__tests__/add-block.spec.tsx | 16 +- .../workflow-canvas.browser.spec.tsx | 7 +- .../use-agent-configure-sync.spec.tsx | 90 +++-- .../__tests__/agent-prompt-editor.spec.tsx | 15 +- .../orchestrate/prompt-editor/hooks.ts | 38 +- .../orchestrate/prompt-editor/slash.tsx | 29 +- .../tools/__tests__/index.spec.tsx | 29 +- .../components/orchestrate/tools/index.tsx | 46 ++- .../configure/tool-provider-catalog.ts | 29 +- web/service/__tests__/use-tools.spec.tsx | 85 ----- web/service/use-plugins.ts | 6 +- web/service/use-tools.ts | 360 ------------------ 114 files changed, 1934 insertions(+), 1642 deletions(-) create mode 100644 web/app/components/tools/utils/query-select.ts create mode 100644 web/app/components/tools/utils/tool-list-query-key.ts delete mode 100644 web/service/__tests__/use-tools.spec.tsx delete mode 100644 web/service/use-tools.ts diff --git a/oxlint-suppressions.json b/oxlint-suppressions.json index e958edee52f..861e3b1b48b 100644 --- a/oxlint-suppressions.json +++ b/oxlint-suppressions.json @@ -4176,11 +4176,6 @@ "count": 1 } }, - "web/service/use-tools.ts": { - "@tanstack/query/prefer-query-options": { - "count": 15 - } - }, "web/service/use-triggers.ts": { "@tanstack/query/prefer-query-options": { "count": 6 diff --git a/web/app/components/app/access-point/__tests__/mcp-card.spec.tsx b/web/app/components/app/access-point/__tests__/mcp-card.spec.tsx index 4fd5ec63630..7d3b06a713f 100644 --- a/web/app/components/app/access-point/__tests__/mcp-card.spec.tsx +++ b/web/app/components/app/access-point/__tests__/mcp-card.spec.tsx @@ -12,7 +12,6 @@ import { AppModeEnum } from '@/types/app' import { MCPAccessPointCard } from '../built-in-access-points/mcp-card' const mocks = vi.hoisted(() => ({ - invalidateServerDetail: vi.fn(), serverDetail: { data: undefined as undefined | { id: string; server_code: string; status: string }, isPending: false, @@ -29,31 +28,38 @@ vi.mock('@/app/notifications', () => ({ }, })) -vi.mock('@/service/console', () => ({ - consoleQuery: { - apps: { - byAppId: { - server: { - put: { - mutationOptions: (options = {}) => ({ - mutationFn: mocks.updateServer, - ...options, - }), +vi.mock('@/service/console', async () => { + const { consoleOperationStub } = await import('@/test/console/operation-hooks') + return { + consoleQuery: { + apps: { + byAppId: { + server: { + get: consoleOperationStub('apps.byAppId.server.get'), + refresh: { post: consoleOperationStub('apps.byAppId.server.refresh.post') }, + put: { + mutationOptions: (options = {}) => ({ + mutationFn: mocks.updateServer, + ...options, + }), + }, }, }, }, }, - }, -})) + } +}) -vi.mock('@/service/use-tools', () => ({ - useInvalidateMCPServerDetail: () => mocks.invalidateServerDetail, - useMCPServerDetail: () => mocks.serverDetail, - useRefreshMCPServerCode: () => ({ - isPending: false, - mutateAsync: mocks.refreshServerCode, - }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'apps.byAppId.server.get': () => mocks.serverDetail, + 'apps.byAppId.server.refresh.post': () => ({ + isPending: false, + mutateAsync: mocks.refreshServerCode, + }), + }) +}) vi.mock('@/app/components/tools/mcp/mcp-server-modal', () => ({ default: (props: Record) => { @@ -112,8 +118,9 @@ function createDeferredPromise() { function renderCard(cardAppInfo: AppDetailWithSite = appInfo, workflow?: PublishedWorkflow) { const queryClient = createTestQueryClient() + const invalidateQueries = vi.spyOn(queryClient, 'invalidateQueries') - return render( + const view = render( , ) + return { ...view, invalidateQueries } } describe('MCPAccessPointCard', () => { @@ -228,7 +236,7 @@ describe('MCPAccessPointCard', () => { mocks.updateServer .mockReturnValueOnce(firstToggle.promise) .mockReturnValueOnce(secondToggle.promise) - renderCard() + const { invalidateQueries } = renderCard() const accessSwitch = screen.getByRole('switch') await user.click(accessSwitch) @@ -250,7 +258,13 @@ describe('MCPAccessPointCard', () => { secondToggle.resolve() await waitFor(() => { - expect(mocks.invalidateServerDetail).toHaveBeenCalledTimes(2) + expect(invalidateQueries).toHaveBeenCalledTimes(2) + expect(invalidateQueries).toHaveBeenLastCalledWith({ + queryKey: [ + ['console', 'apps', 'byAppId', 'server', 'get'], + { type: 'query', input: { params: { app_id: 'app-1' } } }, + ], + }) }) expect(accessSwitch).toHaveAttribute('aria-checked', 'true') }) diff --git a/web/app/components/app/access-point/__tests__/trigger-card.spec.tsx b/web/app/components/app/access-point/__tests__/trigger-card.spec.tsx index a0d9de65e82..7819855913e 100644 --- a/web/app/components/app/access-point/__tests__/trigger-card.spec.tsx +++ b/web/app/components/app/access-point/__tests__/trigger-card.spec.tsx @@ -1,4 +1,4 @@ -import type { AppTrigger } from '@/service/use-tools' +import type { AppTrigger } from '@/app/components/tools/types' import { QueryClientProvider } from '@tanstack/react-query' import { screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' @@ -10,7 +10,6 @@ import { AppModeEnum } from '@/types/app' import { TriggerAccessPointCard } from '../built-in-access-points/trigger-card' const mocks = vi.hoisted(() => ({ - invalidateTriggers: vi.fn(), setTriggerStatuses: vi.fn(), triggerQuery: { data: undefined as { data: AppTrigger[] } | undefined, @@ -39,10 +38,15 @@ vi.mock('@/context/i18n', () => ({ useDocLink: () => (path: string) => `https://docs.example.test/en${path}`, })) -vi.mock('@/service/console', () => ({ +vi.mock('@/service/console', async () => ({ consoleQuery: { apps: { byAppId: { + triggers: { + get: (await import('@/test/console/operation-hooks')).consoleOperationStub( + 'apps.byAppId.triggers.get', + ), + }, triggerEnable: { post: { mutationOptions: (options = {}) => ({ @@ -56,10 +60,12 @@ vi.mock('@/service/console', () => ({ }, })) -vi.mock('@/service/use-tools', () => ({ - useAppTriggers: () => mocks.triggerQuery, - useInvalidateAppTriggers: () => mocks.invalidateTriggers, -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'apps.byAppId.triggers.get': () => mocks.triggerQuery, + }) +}) vi.mock('@/service/use-triggers', () => ({ useAllTriggerPlugins: () => ({ data: [] }), @@ -88,14 +94,21 @@ function createTrigger(id: string, status: AppTrigger['status']): AppTrigger { } } +const triggersQueryKey = [ + ['console', 'apps', 'byAppId', 'triggers', 'get'], + { type: 'query', input: { params: { app_id: 'app-1' } } }, +] + function renderCard(availability: 'available' | 'loading' | 'unavailable') { const queryClient = createTestQueryClient() + queryClient.setQueryData(triggersQueryKey, { data: [] }) render( , ) + return queryClient } function createDeferredPromise() { @@ -187,7 +200,7 @@ describe('TriggerAccessPointCard', () => { mocks.triggerQuery.data = { data: [createTrigger('disabled', 'disabled')], } - renderCard('available') + const queryClient = renderCard('available') await user.click(screen.getByRole('switch', { name: 'Trigger disabled' })) @@ -204,7 +217,7 @@ describe('TriggerAccessPointCard', () => { }, expect.any(Object), ) - expect(mocks.invalidateTriggers).toHaveBeenCalledWith('app-1') + expect(queryClient.getQueryState(triggersQueryKey)?.isInvalidated).toBe(true) }) expect(toast.success).not.toHaveBeenCalled() }) @@ -235,7 +248,8 @@ describe('TriggerAccessPointCard', () => { mocks.updateTriggerStatus .mockReturnValueOnce(firstToggle.promise) .mockReturnValueOnce(secondToggle.promise) - renderCard('available') + const queryClient = renderCard('available') + const invalidateQueries = vi.spyOn(queryClient, 'invalidateQueries') const triggerSwitch = screen.getByRole('switch', { name: 'Trigger enabled' }) await user.click(triggerSwitch) @@ -257,7 +271,7 @@ describe('TriggerAccessPointCard', () => { secondToggle.resolve() await waitFor(() => { - expect(mocks.invalidateTriggers).toHaveBeenCalledTimes(2) + expect(invalidateQueries).toHaveBeenCalledTimes(2) }) expect(triggerSwitch).toHaveAttribute('aria-checked', 'true') }) diff --git a/web/app/components/app/access-point/built-in-access-points/mcp-card.tsx b/web/app/components/app/access-point/built-in-access-points/mcp-card.tsx index 5e2c4f5ae08..3545909fd84 100644 --- a/web/app/components/app/access-point/built-in-access-points/mcp-card.tsx +++ b/web/app/components/app/access-point/built-in-access-points/mcp-card.tsx @@ -12,20 +12,16 @@ import { AlertDialogTitle, } from '@langgenius/dify-ui/alert-dialog' import { Button } from '@langgenius/dify-ui/button' -import { useMutation } from '@tanstack/react-query' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { AccessPointCard } from '@/app/components/base/access-point/card' import { AccessPointUrl } from '@/app/components/base/access-point/url' import MCPServerModal from '@/app/components/tools/mcp/mcp-server-modal' +import { selectMCPServerDetail } from '@/app/components/tools/utils/query-select' import { BlockEnum } from '@/app/components/workflow/types' import { toast } from '@/app/notifications' import { consoleQuery } from '@/service/console' -import { - useInvalidateMCPServerDetail, - useMCPServerDetail, - useRefreshMCPServerCode, -} from '@/service/use-tools' import { AppModeEnum } from '@/types/app' import { useAccessPointStatusLabel } from '../shared/use-access-point-status-label' import { getPublishedWorkflowNodes, isAdvancedApp } from '../shared/utils' @@ -55,23 +51,29 @@ export function MCPAccessPointCard({ const [showRegenerate, setShowRegenerate] = useState(false) const basicConfig = appInfo.model_config const basicAppInputForm = basicConfig?.user_input_form - const { data: detail, isPending: serverDetailLoading } = useMCPServerDetail( - appInfo.id, - Boolean(appInfo.id), - ) - const invalidateServerDetail = useInvalidateMCPServerDetail() + const queryClient = useQueryClient() + const serverDetailQueryOptions = consoleQuery.apps.byAppId.server.get.queryOptions({ + input: { params: { app_id: appInfo.id } }, + enabled: Boolean(appInfo.id), + select: selectMCPServerDetail, + }) + const { data: detail, isPending: serverDetailLoading } = useQuery(serverDetailQueryOptions) + const invalidateServerDetail = () => + queryClient.invalidateQueries({ queryKey: serverDetailQueryOptions.queryKey }) const updateServerMutation = useMutation( consoleQuery.apps.byAppId.server.put.mutationOptions({ scope: { id: `app-mcp-toggle:${appInfo.id}`, }, - onSuccess: () => invalidateServerDetail(appInfo.id), + onSuccess: () => invalidateServerDetail(), onError: () => { toast.error(t(($) => $['actionMsg.modifiedUnsuccessfully'], { ns: 'common' })) }, }), ) - const { mutateAsync: refreshServerCode, isPending: regenerating } = useRefreshMCPServerCode() + const { mutateAsync: refreshServerCode, isPending: regenerating } = useMutation( + consoleQuery.apps.byAppId.server.refresh.post.mutationOptions(), + ) const serverPublished = Boolean(detail?.id) const serverActivated = detail?.status === 'active' @@ -137,8 +139,8 @@ export function MCPAccessPointCard({ const handleRegenerate = async () => { if (!canManageAccessPoint || !detail?.id) return - await refreshServerCode(appInfo.id) - invalidateServerDetail(appInfo.id) + await refreshServerCode({ params: { app_id: appInfo.id } }) + invalidateServerDetail() setShowRegenerate(false) } @@ -207,7 +209,7 @@ export function MCPAccessPointCard({ latestParams={latestParams} onHide={() => { setShowServerModal(false) - invalidateServerDetail(appInfo.id) + invalidateServerDetail() }} appInfo={appInfo} /> diff --git a/web/app/components/app/access-point/built-in-access-points/trigger-card.tsx b/web/app/components/app/access-point/built-in-access-points/trigger-card.tsx index abf8c9bf080..cf3f086de7a 100644 --- a/web/app/components/app/access-point/built-in-access-points/trigger-card.tsx +++ b/web/app/components/app/access-point/built-in-access-points/trigger-card.tsx @@ -1,14 +1,15 @@ 'use client' import type { AppDetailWithSite } from '@dify/contracts/api/console/apps/types.gen' +import type { AppTrigger } from '@/app/components/tools/types' import type { TriggerWithProvider } from '@/app/components/workflow/block-selector/types' -import type { AppTrigger } from '@/service/use-tools' import { StatusDot } from '@langgenius/dify-ui/status-dot' import { Switch } from '@langgenius/dify-ui/switch' -import { useMutation } from '@tanstack/react-query' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useEffect, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { AccessPointCard, AccessPointEmptyContent } from '@/app/components/base/access-point/card' +import { selectAppTriggers } from '@/app/components/tools/utils/query-select' import BlockIcon from '@/app/components/workflow/block-icon' import { useTriggerStatusStore } from '@/app/components/workflow/store/trigger-status' import { BlockEnum } from '@/app/components/workflow/types' @@ -16,7 +17,6 @@ import { toast } from '@/app/notifications' import { useDocLink } from '@/context/i18n' import Link from '@/next/link' import { consoleQuery } from '@/service/console' -import { useAppTriggers, useInvalidateAppTriggers } from '@/service/use-tools' import { useAllTriggerPlugins } from '@/service/use-triggers' import { canFindTool } from '@/utils' import { useAccessPointStatusLabel } from '../shared/use-access-point-status-label' @@ -64,13 +64,18 @@ function TriggerAccessPointItem({ triggerPlugins: TriggerWithProvider[] }) { const { t } = useTranslation(['agentV2', 'appOverview', 'common']) - const invalidateTriggers = useInvalidateAppTriggers() + const queryClient = useQueryClient() const updateTriggerMutation = useMutation( consoleQuery.apps.byAppId.triggerEnable.post.mutationOptions({ scope: { id: `app-trigger-toggle:${appId}:${trigger.id}`, }, - onSuccess: () => invalidateTriggers(appId), + onSuccess: () => + queryClient.invalidateQueries({ + queryKey: consoleQuery.apps.byAppId.triggers.get.queryKey({ + input: { params: { app_id: appId } }, + }), + }), onError: () => { toast.error(t(($) => $['actionMsg.modifiedUnsuccessfully'], { ns: 'common' })) }, @@ -145,7 +150,13 @@ export function TriggerAccessPointCard({ }: TriggerAccessPointCardProps) { const { t } = useTranslation(['appOverview', 'deployments', 'navigation']) const docLink = useDocLink() - const { data: response, isLoading } = useAppTriggers(appInfo.id) + const { data: response, isLoading } = useQuery( + consoleQuery.apps.byAppId.triggers.get.queryOptions({ + input: { params: { app_id: appInfo.id } }, + enabled: !!appInfo.id, + select: selectAppTriggers, + }), + ) const { data: triggerPlugins = [] } = useAllTriggerPlugins() const setTriggerStatuses = useTriggerStatusStore((state) => state.setTriggerStatuses) const triggers = useMemo(() => response?.data ?? [], [response?.data]) diff --git a/web/app/components/app/app-publisher/__tests__/environment-deployment-flow.spec.tsx b/web/app/components/app/app-publisher/__tests__/environment-deployment-flow.spec.tsx index 4529251fdd6..c983f3fa2a8 100644 --- a/web/app/components/app/app-publisher/__tests__/environment-deployment-flow.spec.tsx +++ b/web/app/components/app/app-publisher/__tests__/environment-deployment-flow.spec.tsx @@ -67,12 +67,15 @@ vi.mock('@/hooks/use-format-time-from-now', () => ({ }), })) -vi.mock('@/service/use-tools', () => ({ - useAllBuiltInTools: () => ({ data: [] }), - useAllCustomTools: () => ({ data: [] }), - useAllMCPTools: () => ({ data: [] }), - useAllWorkflowTools: () => ({ data: [] }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.tools.builtin.get': () => ({ data: [] }), + 'workspaces.current.tools.api.get': () => ({ data: [] }), + 'workspaces.current.tools.mcp.get': () => ({ data: [] }), + 'workspaces.current.tools.workflow.get': () => ({ data: [] }), + }) +}) function publishedWorkflowVersion({ id, diff --git a/web/app/components/app/app-publisher/__tests__/index.spec.tsx b/web/app/components/app/app-publisher/__tests__/index.spec.tsx index a74e51ebbf6..8cc6f1af40e 100644 --- a/web/app/components/app/app-publisher/__tests__/index.spec.tsx +++ b/web/app/components/app/app-publisher/__tests__/index.spec.tsx @@ -132,14 +132,15 @@ vi.mock('@/app/components/workflow/collaboration/core/collaboration-manager', () }, })) -vi.mock('@/service/use-tools', () => ({ - useWorkflowToolDetailByAppID: () => ({ - data: undefined, - isLoading: false, - }), - useInvalidateAllWorkflowTools: () => vi.fn(), - useInvalidateWorkflowToolDetailByAppID: () => vi.fn(), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.toolProvider.workflow.get.get': () => ({ + data: undefined, + isLoading: false, + }), + }) +}) vi.mock('@/context/workspace-state', async () => { const { createWorkspaceStateModuleMock } = await import('@/test/console/state-fixture') diff --git a/web/app/components/app/configuration/config/agent/agent-tools/__tests__/index.spec.tsx b/web/app/components/app/configuration/config/agent/agent-tools/__tests__/index.spec.tsx index af5bcc2af18..c9a7cb32858 100644 --- a/web/app/components/app/configuration/config/agent/agent-tools/__tests__/index.spec.tsx +++ b/web/app/components/app/configuration/config/agent/agent-tools/__tests__/index.spec.tsx @@ -31,12 +31,15 @@ let builtInTools: ToolWithProvider[] = [] let customTools: ToolWithProvider[] = [] let workflowTools: ToolWithProvider[] = [] let mcpTools: ToolWithProvider[] = [] -vi.mock('@/service/use-tools', () => ({ - useAllBuiltInTools: () => ({ data: builtInTools }), - useAllCustomTools: () => ({ data: customTools }), - useAllWorkflowTools: () => ({ data: workflowTools }), - useAllMCPTools: () => ({ data: mcpTools }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.tools.builtin.get': () => ({ data: builtInTools }), + 'workspaces.current.tools.api.get': () => ({ data: customTools }), + 'workspaces.current.tools.workflow.get': () => ({ data: workflowTools }), + 'workspaces.current.tools.mcp.get': () => ({ data: mcpTools }), + }) +}) type ToolPickerProps = React.ComponentProps let singleToolSelection: ToolDefaultValue | null = null diff --git a/web/app/components/app/configuration/config/agent/agent-tools/index.tsx b/web/app/components/app/configuration/config/agent/agent-tools/index.tsx index 7ad92410dd0..2dc637af297 100644 --- a/web/app/components/app/configuration/config/agent/agent-tools/index.tsx +++ b/web/app/components/app/configuration/config/agent/agent-tools/index.tsx @@ -12,6 +12,7 @@ import { StatusDot } from '@langgenius/dify-ui/status-dot' import { Switch } from '@langgenius/dify-ui/switch' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { RiDeleteBinLine, RiEqualizer2Line } from '@remixicon/react' +import { useQuery } from '@tanstack/react-query' import { produce } from 'immer' import * as React from 'react' import { useCallback, useMemo, useState } from 'react' @@ -22,6 +23,7 @@ import { OperationButton } from '@/app/components/app/configuration/base/operati import AppIcon from '@/app/components/base/app-icon' import { parseToolProviderType } from '@/app/components/tools/provider-type' import { CollectionType } from '@/app/components/tools/types' +import { selectToolsWithProvider } from '@/app/components/tools/utils/query-select' import { addDefaultValue, toolParametersToFormSchemas, @@ -29,12 +31,7 @@ import { import ToolPicker from '@/app/components/workflow/block-selector/tool-picker' import { MAX_TOOLS_NUM } from '@/config' import ConfigContext from '@/context/debug-configuration' -import { - useAllBuiltInTools, - useAllCustomTools, - useAllMCPTools, - useAllWorkflowTools, -} from '@/service/use-tools' +import { consoleQuery } from '@/service/console' import { writeTextToClipboard } from '@/utils/clipboard' import { matchesProviderReference } from '@/utils/provider-reference' import { useFormattingChangedDispatcher } from '../../../debug/hooks' @@ -48,10 +45,22 @@ const AgentTools: FC = () => { const { t } = useTranslation(['appDebug', 'common', 'tools']) const [isShowChooseTool, setIsShowChooseTool] = useState(false) const { readonly, modelConfig, setModelConfig } = useContext(ConfigContext) - const { data: buildInTools } = useAllBuiltInTools() - const { data: customTools } = useAllCustomTools() - const { data: workflowTools } = useAllWorkflowTools() - const { data: mcpTools } = useAllMCPTools() + const { data: buildInTools } = useQuery( + consoleQuery.workspaces.current.tools.builtin.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: customTools } = useQuery( + consoleQuery.workspaces.current.tools.api.get.queryOptions({ select: selectToolsWithProvider }), + ) + const { data: workflowTools } = useQuery( + consoleQuery.workspaces.current.tools.workflow.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: mcpTools } = useQuery( + consoleQuery.workspaces.current.tools.mcp.get.queryOptions({ select: selectToolsWithProvider }), + ) const collectionList = useMemo(() => { const allTools = [ ...(buildInTools || []), diff --git a/web/app/components/app/deploy/__tests__/index.spec.tsx b/web/app/components/app/deploy/__tests__/index.spec.tsx index 37b925cfc41..647471d7934 100644 --- a/web/app/components/app/deploy/__tests__/index.spec.tsx +++ b/web/app/components/app/deploy/__tests__/index.spec.tsx @@ -823,24 +823,27 @@ vi.mock('@/hooks/use-timestamp', () => ({ }), })) -vi.mock('@/service/use-tools', () => ({ - useAllBuiltInTools: () => ({ data: [] }), - useAllCustomTools: () => ({ data: [] }), - useAllMCPTools: () => ({ - data: [ - { - icon: '/notion-mcp.svg', - id: 'notion', - name: 'Notion', - plugin_id: 'langgenius/notion', - }, - ], - }), - useAllWorkflowTools: () => ({ data: [] }), - useMCPServerDetail: () => ({ - data: mockBuiltInEnvironment.mcpServerDetail, - }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.tools.builtin.get': () => ({ data: [] }), + 'workspaces.current.tools.api.get': () => ({ data: [] }), + 'workspaces.current.tools.mcp.get': () => ({ + data: [ + { + icon: '/notion-mcp.svg', + id: 'notion', + name: 'Notion', + plugin_id: 'langgenius/notion', + }, + ], + }), + 'workspaces.current.tools.workflow.get': () => ({ data: [] }), + 'apps.byAppId.server.get': () => ({ + data: mockBuiltInEnvironment.mcpServerDetail, + }), + }) +}) vi.mock('@/next/navigation', async (importOriginal) => ({ ...(await importOriginal()), diff --git a/web/app/components/app/deploy/built-in-environment-card/index.tsx b/web/app/components/app/deploy/built-in-environment-card/index.tsx index c576f94b043..ca83bbecce1 100644 --- a/web/app/components/app/deploy/built-in-environment-card/index.tsx +++ b/web/app/components/app/deploy/built-in-environment-card/index.tsx @@ -10,7 +10,6 @@ import { BlockEnum, isTriggerNode } from '@/app/components/workflow/types' import useTimestamp from '@/hooks/use-timestamp' import { useParams } from '@/next/navigation' import { consoleQuery } from '@/service/console' -import { useMCPServerDetail } from '@/service/use-tools' import { appWorkflowQueryOptions } from '@/service/workflow-queries' import { AccessPointIcon } from '../shared/access-point-icon' import { RuntimeStateIndicator } from '../shared/runtime-state' @@ -30,7 +29,11 @@ export const BuiltInEnvironmentCard = memo( consoleQuery.apps.byAppId.get.queryOptions({ input: { params: { app_id: appId } } }), ) const { data: publishedWorkflow } = useQuery(appWorkflowQueryOptions(appId)) - const { data: mcpServerDetail } = useMCPServerDetail(appId) + const { data: mcpServerDetail } = useQuery( + consoleQuery.apps.byAppId.server.get.queryOptions({ + input: { params: { app_id: appId } }, + }), + ) const publishedNodes = Array.isArray(publishedWorkflow?.graph.nodes) ? (publishedWorkflow.graph.nodes as Node[]) : [] diff --git a/web/app/components/app/deploy/shared/deployment-configuration/__tests__/use-provider-icon.spec.tsx b/web/app/components/app/deploy/shared/deployment-configuration/__tests__/use-provider-icon.spec.tsx index d698f9f945d..b3975a43479 100644 --- a/web/app/components/app/deploy/shared/deployment-configuration/__tests__/use-provider-icon.spec.tsx +++ b/web/app/components/app/deploy/shared/deployment-configuration/__tests__/use-provider-icon.spec.tsx @@ -10,12 +10,15 @@ import { useGetProviderIcon } from '../use-provider-icon' const request = vi.hoisted(() => vi.fn<(url: string) => Promise>()) vi.mock('@/service/base', () => ({ request })) -vi.mock('@/service/use-tools', () => ({ - useAllBuiltInTools: () => ({}), - useAllCustomTools: () => ({}), - useAllWorkflowTools: () => ({}), - useAllMCPTools: () => ({}), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.tools.builtin.get': () => ({}), + 'workspaces.current.tools.api.get': () => ({}), + 'workspaces.current.tools.workflow.get': () => ({}), + 'workspaces.current.tools.mcp.get': () => ({}), + }) +}) vi.mock('@/service/use-triggers', () => ({ useAllTriggerPlugins: () => ({}) })) vi.mock('@/hooks/use-theme', () => ({ default: () => ({ theme: 'light' }) })) vi.mock('@/utils/var', async (importOriginal) => ({ diff --git a/web/app/components/app/deploy/shared/deployment-configuration/use-provider-icon.ts b/web/app/components/app/deploy/shared/deployment-configuration/use-provider-icon.ts index efc796759d2..3ec0579b15a 100644 --- a/web/app/components/app/deploy/shared/deployment-configuration/use-provider-icon.ts +++ b/web/app/components/app/deploy/shared/deployment-configuration/use-provider-icon.ts @@ -2,16 +2,11 @@ import type { UnsupportedNode } from '@dify/contracts/enterprise-app-deploy/type import type { Node } from '@/app/components/workflow/types' import { useQuery } from '@tanstack/react-query' import { useCallback } from 'react' +import { selectToolsWithProvider } from '@/app/components/tools/utils/query-select' import { findNodeIcon } from '@/app/components/workflow/hooks/use-tool-icon' import { BlockEnum } from '@/app/components/workflow/types' import useTheme from '@/hooks/use-theme' import { consoleQuery } from '@/service/console' -import { - useAllBuiltInTools, - useAllCustomTools, - useAllMCPTools, - useAllWorkflowTools, -} from '@/service/use-tools' import { useAllTriggerPlugins } from '@/service/use-triggers' const PROVIDER_ICON_BLOCK_TYPES = new Set([ @@ -35,10 +30,30 @@ export const useGetProviderIcon = (nodes: UnsupportedNode[]) => { const hasToolNode = nodes.some((node) => node.type === BlockEnum.Tool) const hasDataSourceNode = nodes.some((node) => node.type === BlockEnum.DataSource) const hasTriggerPluginNode = nodes.some((node) => node.type === BlockEnum.TriggerPlugin) - const { data: buildInTools } = useAllBuiltInTools(hasToolNode) - const { data: customTools } = useAllCustomTools(hasToolNode) - const { data: workflowTools } = useAllWorkflowTools(hasToolNode) - const { data: mcpTools } = useAllMCPTools(hasToolNode) + const { data: buildInTools } = useQuery( + consoleQuery.workspaces.current.tools.builtin.get.queryOptions({ + enabled: hasToolNode, + select: selectToolsWithProvider, + }), + ) + const { data: customTools } = useQuery( + consoleQuery.workspaces.current.tools.api.get.queryOptions({ + enabled: hasToolNode, + select: selectToolsWithProvider, + }), + ) + const { data: workflowTools } = useQuery( + consoleQuery.workspaces.current.tools.workflow.get.queryOptions({ + enabled: hasToolNode, + select: selectToolsWithProvider, + }), + ) + const { data: mcpTools } = useQuery( + consoleQuery.workspaces.current.tools.mcp.get.queryOptions({ + enabled: hasToolNode, + select: selectToolsWithProvider, + }), + ) const { data: dataSourceList } = useQuery( consoleQuery.rag.pipelines.datasourcePlugins.get.queryOptions({ enabled: hasDataSourceNode, diff --git a/web/app/components/explore/try-app/preview/basic-app-preview.tsx b/web/app/components/explore/try-app/preview/basic-app-preview.tsx index 86ceddfef54..f1ad3320e10 100644 --- a/web/app/components/explore/try-app/preview/basic-app-preview.tsx +++ b/web/app/components/explore/try-app/preview/basic-app-preview.tsx @@ -3,8 +3,10 @@ import type { TrialAppDetailResponse } from '@dify/contracts/api/console/trial-a import type { FC } from 'react' import type { Features as FeaturesData, FileUpload } from '@/app/components/base/features/types' import type { FormValue } from '@/app/components/header/account-setting/model-provider-page/declarations' +import type { Collection } from '@/app/components/tools/types' import type { ModelConfig } from '@/models/debug' import type { PromptVariable } from '@/types/app' +import { useQuery } from '@tanstack/react-query' import { noop } from 'es-toolkit/function' import { clone } from 'es-toolkit/object' import * as React from 'react' @@ -16,6 +18,7 @@ import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder' import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants' import { ModelFeatureEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { CollectionType } from '@/app/components/tools/types' +import { selectToolCollections } from '@/app/components/tools/utils/query-select' import { SupportUploadFileTypes } from '@/app/components/workflow/types' import { ANNOTATION_DEFAULT, @@ -26,7 +29,7 @@ import { import ConfigContext from '@/context/debug-configuration' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import { PromptMode } from '@/models/debug' -import { useAllToolProviders } from '@/service/use-tools' +import { consoleQuery } from '@/service/console' import { useGetTryAppDataSets } from '@/service/use-try-app' import { AgentStrategy, ModelModeType, Resolution, TransferMethod, TtsAutoPlay } from '@/types/app' import { correctModelProvider, correctToolProvider } from '@/utils' @@ -295,7 +298,7 @@ const normalizeExternalDataToolFormItem = (item: Record) => { const normalizeAgentTool = ( tool: Record, deletedTools: TrialAppDetailResponse['deleted_tools'] | undefined, - collectionList: ReturnType['data'] | undefined, + collectionList: Collection[] | undefined, ): AgentToolItem => { const providerId = getString(tool.provider_id) const providerName = getString(tool.provider_name) @@ -330,8 +333,12 @@ const BasicAppPreview: FC = ({ appId, appDetail }) => { const media = useBreakpoints() const isMobile = media === MediaType.mobile - const { data: collectionListFromServer, isLoading: isLoadingToolProviders } = - useAllToolProviders() + const { data: collectionListFromServer, isLoading: isLoadingToolProviders } = useQuery( + consoleQuery.workspaces.current.toolProviders.get.queryOptions({ + input: { query: {} }, + select: selectToolCollections, + }), + ) const collectionList = collectionListFromServer?.map((item) => { return { ...item, diff --git a/web/app/components/integrations/__tests__/tool-provider-list.spec.tsx b/web/app/components/integrations/__tests__/tool-provider-list.spec.tsx index 2f1830cd2d2..a2aa4bfec22 100644 --- a/web/app/components/integrations/__tests__/tool-provider-list.spec.tsx +++ b/web/app/components/integrations/__tests__/tool-provider-list.spec.tsx @@ -96,11 +96,22 @@ const mockRefetch = vi.fn() const mockUseAllToolProviders = vi.hoisted(() => vi.fn()) const mockUseAllCustomTools = vi.hoisted(() => vi.fn()) const mockUseAllWorkflowTools = vi.hoisted(() => vi.fn()) -vi.mock('@/service/use-tools', () => ({ - useAllToolProviders: (enabled?: boolean, type?: string) => mockUseAllToolProviders(enabled, type), - useAllCustomTools: (enabled?: boolean) => mockUseAllCustomTools(enabled), - useAllWorkflowTools: (enabled?: boolean) => mockUseAllWorkflowTools(enabled), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.toolProviders.get': ({ + input, + enabled, + }: { + input: { query: { type?: string } } + enabled?: boolean + }) => mockUseAllToolProviders(enabled, input.query.type), + 'workspaces.current.tools.api.get': ({ enabled }: { enabled?: boolean }) => + mockUseAllCustomTools(enabled), + 'workspaces.current.tools.workflow.get': ({ enabled }: { enabled?: boolean }) => + mockUseAllWorkflowTools(enabled), + }) +}) const mockConsoleState = vi.hoisted(() => ({ workspacePermissionKeys: ['tool.manage', 'mcp.manage'] as string[], diff --git a/web/app/components/integrations/tool-provider-list.tsx b/web/app/components/integrations/tool-provider-list.tsx index e7ef086a3f4..10906bda1d0 100644 --- a/web/app/components/integrations/tool-provider-list.tsx +++ b/web/app/components/integrations/tool-provider-list.tsx @@ -11,7 +11,7 @@ import { ScrollAreaThumb, ScrollAreaViewport, } from '@langgenius/dify-ui/scroll-area' -import { useSuspenseQuery } from '@tanstack/react-query' +import { useQuery, useSuspenseQuery } from '@tanstack/react-query' import { useCallback, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { isSearchResultEmpty } from '@/app/components/base/search-input/search-state' @@ -32,9 +32,13 @@ import { BuiltinMarketplacePanel } from '@/app/components/tools/marketplace/buil import MCPList from '@/app/components/tools/mcp' import ProviderDetail from '@/app/components/tools/provider/detail' import { ToolProviderGrid } from '@/app/components/tools/tool-provider-grid' +import { + selectToolCollections, + selectToolsWithProvider, +} from '@/app/components/tools/utils/query-select' import { systemFeaturesQueryOptions } from '@/features/system-features/client' +import { consoleQuery } from '@/service/console' import { useCheckInstalled, useInvalidateInstalledPluginList } from '@/service/use-plugins' -import { useAllCustomTools, useAllToolProviders, useAllWorkflowTools } from '@/service/use-tools' import { useToolProviderCategory } from './hooks/use-tool-provider-category' import ToolProviderCreateAction from './tool-provider-create-action' import { ToolProviderToolbar } from './tool-provider-toolbar' @@ -85,12 +89,25 @@ const ProviderList = ({ category, contentInset = 'default', layout }: ProviderLi const handleCreatedMCPProviderHandled = useCallback(() => { setCreatedMCPProviderId(undefined) }, []) - const allToolProvidersQuery = useAllToolProviders( - activeTab === 'builtin' || activeTab === 'mcp', - activeTab === 'mcp' ? 'mcp' : undefined, + const allToolProvidersQuery = useQuery( + consoleQuery.workspaces.current.toolProviders.get.queryOptions({ + input: { query: activeTab === 'mcp' ? { type: 'mcp' } : {} }, + enabled: activeTab === 'builtin' || activeTab === 'mcp', + select: selectToolCollections, + }), + ) + const customToolsQuery = useQuery( + consoleQuery.workspaces.current.tools.api.get.queryOptions({ + enabled: activeTab === 'api', + select: selectToolsWithProvider, + }), + ) + const workflowToolsQuery = useQuery( + consoleQuery.workspaces.current.tools.workflow.get.queryOptions({ + enabled: activeTab === 'workflow', + select: selectToolsWithProvider, + }), ) - const customToolsQuery = useAllCustomTools(activeTab === 'api') - const workflowToolsQuery = useAllWorkflowTools(activeTab === 'workflow') const { refetch: refetchMcpTools } = allToolProvidersQuery const activeToolsQuery = activeTab === 'api' 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 ec5a86d3120..0fd9b191a55 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 @@ -12,10 +12,7 @@ const mocks = vi.hoisted(() => ({ installed: vi.fn(), checkInstalled: vi.fn(), defaultModel: vi.fn(), - toolProviders: vi.fn(), - builtInTools: vi.fn(), triggers: vi.fn(), - recommended: vi.fn(), })) vi.mock('@/service/base', () => ({ request: mocks.request })) @@ -26,11 +23,6 @@ vi.mock('@/service/use-plugins', () => ({ vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({ useInvalidateDefaultModel: () => mocks.defaultModel, })) -vi.mock('@/service/use-tools', () => ({ - useInvalidateAllToolProviders: () => mocks.toolProviders, - useInvalidateAllBuiltInTools: () => mocks.builtInTools, - useInvalidateRAGRecommendedPlugins: () => mocks.recommended, -})) vi.mock('@/service/use-triggers', () => ({ useInvalidateAllTriggerPlugins: () => mocks.triggers, })) @@ -51,6 +43,17 @@ const providerKeys = [ ] const datasourceKey = consoleQuery.rag.pipelines.datasourcePlugins.get.queryKey() const dataSourceAuthKey = consoleQuery.auth.plugin.datasource.list.get.queryKey() +const toolKeys = [ + current.toolProviders.get.queryKey({ input: { query: {} } }), + current.toolProviders.get.queryKey({ input: { query: { type: 'mcp' } } }), + current.tools.builtin.get.queryKey(), + consoleQuery.rag.pipelines.recommendedPlugins.get.queryKey({ + input: { query: { type: 'tool' } }, + }), +] +const otherRecommendedKey = consoleQuery.rag.pipelines.recommendedPlugins.get.queryKey({ + input: { query: { type: 'datasource' } }, +}) const unrelatedKey = consoleQuery.account.profile.get.key() const modelTypes = ['llm', 'text-embedding', 'rerank', 'speech2text', 'tts'] @@ -69,6 +72,7 @@ describe('plugin installation refresh', () => { client.setQueryData(key, { cached: true }) client.setQueryData(datasourceKey, []) client.setQueryData(dataSourceAuthKey, { result: [] }) + for (const key of [...toolKeys, otherRecommendedKey]) client.setQueryData(key, []) for (const model_type of modelTypes) { client.setQueryData( current.models.modelTypes.byModelType.get.queryKey({ input: { params: { model_type } } }), @@ -105,11 +109,9 @@ describe('plugin installation refresh', () => { expect(client.getQueryState(datasourceKey)?.isInvalidated).toBe( category === PluginCategoryEnum.datasource, ) - if (category === PluginCategoryEnum.tool) { - expect(mocks.toolProviders).toHaveBeenCalledOnce() - expect(mocks.builtInTools).toHaveBeenCalledOnce() - expect(mocks.recommended).toHaveBeenCalledWith('tool') - } + for (const key of toolKeys) + expect(client.getQueryState(key)?.isInvalidated).toBe(category === PluginCategoryEnum.tool) + expect(client.getQueryState(otherRecommendedKey)?.isInvalidated).toBe(false) if (category === PluginCategoryEnum.datasource) { expect(client.getQueryState(datasourceKey)?.isInvalidated).toBe(true) expect(client.getQueryState(dataSourceAuthKey)?.isInvalidated).toBe(true) @@ -136,12 +138,10 @@ describe('plugin installation refresh', () => { expect(client.getQueryState(datasourceKey)?.isInvalidated).toBe(all) if (all) { expect(mocks.installed).toHaveBeenCalledWith() - expect(mocks.toolProviders).toHaveBeenCalledOnce() - expect(mocks.builtInTools).toHaveBeenCalledOnce() + for (const key of toolKeys) expect(client.getQueryState(key)?.isInvalidated).toBe(true) expect(client.getQueryState(datasourceKey)?.isInvalidated).toBe(true) expect(client.getQueryState(dataSourceAuthKey)?.isInvalidated).toBe(true) expect(mocks.triggers).toHaveBeenCalledOnce() - expect(mocks.recommended).toHaveBeenCalledWith('tool') } }) @@ -156,7 +156,7 @@ describe('plugin installation refresh', () => { expect(mocks.checkInstalled).toHaveBeenCalledOnce() for (const key of [...strategyKeys, ...providerKeys]) expect(client.getQueryState(key)?.isInvalidated).toBe(false) - expect(mocks.toolProviders).not.toHaveBeenCalled() + for (const key of toolKeys) expect(client.getQueryState(key)?.isInvalidated).toBe(false) expect(client.getQueryState(datasourceKey)?.isInvalidated).toBe(false) expect(mocks.triggers).not.toHaveBeenCalled() expect(mocks.request).not.toHaveBeenCalled() 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 61a6d1428f7..e8ae6fecbe3 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 @@ -7,11 +7,6 @@ import { useInvalidateCheckInstalled, useInvalidateInstalledPluginList, } from '@/service/use-plugins' -import { - useInvalidateAllBuiltInTools, - useInvalidateAllToolProviders, - useInvalidateRAGRecommendedPlugins, -} from '@/service/use-tools' import { useInvalidateAllTriggerPlugins } from '@/service/use-triggers' import { PluginCategoryEnum } from '../../types' @@ -58,12 +53,7 @@ const useRefreshPluginList = () => { const invalidateDefaultModel = useInvalidateDefaultModel() const queryClient = useQueryClient() - const invalidateAllToolProviders = useInvalidateAllToolProviders() - const invalidateAllBuiltInTools = useInvalidateAllBuiltInTools() - const invalidateAllTriggerPlugins = useInvalidateAllTriggerPlugins() - - const invalidateRAGRecommendedPlugins = useInvalidateRAGRecommendedPlugins() return { refreshPluginList: ( manifest?: PluginManifestInMarket | Plugin | PluginDeclaration | PluginCategoryPayload | null, @@ -76,9 +66,17 @@ const useRefreshPluginList = () => { // tool page, tool select if ((manifest && PluginCategoryEnum.tool.includes(manifest.category)) || refreshAllType) { - invalidateAllToolProviders() - invalidateAllBuiltInTools() - invalidateRAGRecommendedPlugins('tool') + queryClient.invalidateQueries({ + queryKey: consoleQuery.workspaces.current.toolProviders.get.key(), + }) + queryClient.invalidateQueries({ + queryKey: consoleQuery.workspaces.current.tools.builtin.get.queryKey(), + }) + queryClient.invalidateQueries({ + queryKey: consoleQuery.rag.pipelines.recommendedPlugins.get.queryKey({ + input: { query: { type: 'tool' } }, + }), + }) // TODO: update suggested tools. It's a function in hook useMarketplacePlugins,handleUpdatePlugins } diff --git a/web/app/components/plugins/plugin-auth/__tests__/authorized-in-node.spec.tsx b/web/app/components/plugins/plugin-auth/__tests__/authorized-in-node.spec.tsx index b0c8297af63..8f1356f844c 100644 --- a/web/app/components/plugins/plugin-auth/__tests__/authorized-in-node.spec.tsx +++ b/web/app/components/plugins/plugin-auth/__tests__/authorized-in-node.spec.tsx @@ -40,10 +40,6 @@ vi.mock('@/service/use-plugins-auth', () => ({ useGetPluginCredentialSchema: () => ({ data: undefined, isLoading: false }), })) -vi.mock('@/service/use-tools', () => ({ - useInvalidToolsByType: () => vi.fn(), -})) - const mockIsCurrentWorkspaceManager = vi.fn() vi.mock('@/hooks/use-oauth', () => ({ diff --git a/web/app/components/plugins/plugin-auth/__tests__/plugin-auth-in-agent.spec.tsx b/web/app/components/plugins/plugin-auth/__tests__/plugin-auth-in-agent.spec.tsx index c2c9e3cbf6f..93dc802bc81 100644 --- a/web/app/components/plugins/plugin-auth/__tests__/plugin-auth-in-agent.spec.tsx +++ b/web/app/components/plugins/plugin-auth/__tests__/plugin-auth-in-agent.spec.tsx @@ -33,10 +33,6 @@ vi.mock('@/service/use-plugins-auth', () => ({ useGetPluginCredentialSchema: () => ({ data: undefined, isLoading: false }), })) -vi.mock('@/service/use-tools', () => ({ - useInvalidToolsByType: () => vi.fn(), -})) - const mockIsCurrentWorkspaceManager = vi.fn() const mockUserProfile = { id: 'test-user', diff --git a/web/app/components/plugins/plugin-auth/hooks/__tests__/use-credential.spec.ts b/web/app/components/plugins/plugin-auth/hooks/__tests__/use-credential.spec.ts index f7f65d5a753..ee4942f5389 100644 --- a/web/app/components/plugins/plugin-auth/hooks/__tests__/use-credential.spec.ts +++ b/web/app/components/plugins/plugin-auth/hooks/__tests__/use-credential.spec.ts @@ -1,6 +1,9 @@ import type { PluginPayload } from '../../types' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { renderHook } from '@testing-library/react' +import { createElement } from 'react' import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' +import { consoleQuery } from '@/service/console' import { AuthCategory, CredentialTypeEnum } from '../../types' import { useAddPluginCredentialHook, @@ -30,7 +33,6 @@ const mockUseGetPluginOAuthClientSchema = vi.fn().mockReturnValue({ data: null, const mockUseInvalidPluginOAuthClientSchema = vi.fn().mockReturnValue(vi.fn()) const mockUseSetPluginOAuthCustomClient = vi.fn().mockReturnValue({ mutateAsync: vi.fn() }) const mockUseDeletePluginOAuthCustomClient = vi.fn().mockReturnValue({ mutateAsync: vi.fn() }) -const mockInvalidToolsByType = vi.fn() vi.mock('@/service/use-plugins-auth', () => ({ useGetPluginCredentialInfo: (...args: unknown[]) => mockUseGetPluginCredentialInfo(...args), @@ -50,10 +52,6 @@ vi.mock('@/service/use-plugins-auth', () => ({ mockUseDeletePluginOAuthCustomClient(...args), })) -vi.mock('@/service/use-tools', () => ({ - useInvalidToolsByType: () => mockInvalidToolsByType, -})) - const toolPayload = { category: AuthCategory.tool, provider: 'test-provider', @@ -90,13 +88,22 @@ describe('use-credential hooks', () => { describe('useInvalidPluginCredentialInfoHook', () => { it('should return a function that invalidates both credential info and tools', () => { - const { result } = renderHook(() => useInvalidPluginCredentialInfoHook(toolPayload)) + const queryClient = new QueryClient() + const builtInToolsKey = consoleQuery.workspaces.current.tools.builtin.get.queryKey() + const customToolsKey = consoleQuery.workspaces.current.tools.api.get.queryKey() + queryClient.setQueryData(builtInToolsKey, []) + queryClient.setQueryData(customToolsKey, []) + const { result } = renderHook(() => useInvalidPluginCredentialInfoHook(toolPayload), { + wrapper: ({ children }) => + createElement(QueryClientProvider, { client: queryClient }, children), + }) result.current() const invalidFn = mockUseInvalidPluginCredentialInfo.mock.results[0]!.value expect(invalidFn).toHaveBeenCalled() - expect(mockInvalidToolsByType).toHaveBeenCalled() + expect(queryClient.getQueryState(builtInToolsKey)?.isInvalidated).toBe(true) + expect(queryClient.getQueryState(customToolsKey)?.isInvalidated).toBe(false) }) }) diff --git a/web/app/components/plugins/plugin-auth/hooks/use-credential.ts b/web/app/components/plugins/plugin-auth/hooks/use-credential.ts index 80e7151ab21..a9150a50cb4 100644 --- a/web/app/components/plugins/plugin-auth/hooks/use-credential.ts +++ b/web/app/components/plugins/plugin-auth/hooks/use-credential.ts @@ -1,4 +1,6 @@ import type { CredentialTypeEnum, PluginPayload } from '../types' +import { useQueryClient } from '@tanstack/react-query' +import { getToolListQueryKey } from '@/app/components/tools/utils/tool-list-query-key' import { useAddPluginCredential, useDeletePluginCredential, @@ -13,7 +15,6 @@ import { useSetPluginOAuthCustomClient, useUpdatePluginCredential, } from '@/service/use-plugins-auth' -import { useInvalidToolsByType } from '@/service/use-tools' import { useGetApi } from './use-get-api' export const useGetPluginCredentialInfoHook = ( @@ -41,12 +42,12 @@ export const useDeletePluginCredentialHook = (pluginPayload: PluginPayload) => { export const useInvalidPluginCredentialInfoHook = (pluginPayload: PluginPayload) => { const apiMap = useGetApi(pluginPayload) const invalidPluginCredentialInfo = useInvalidPluginCredentialInfo(apiMap.getCredentialInfo) - const providerType = pluginPayload.providerType - const invalidToolsByType = useInvalidToolsByType(providerType) + const queryClient = useQueryClient() + const toolListQueryKey = getToolListQueryKey(pluginPayload.providerType) return () => { invalidPluginCredentialInfo() - invalidToolsByType() + if (toolListQueryKey) queryClient.invalidateQueries({ queryKey: toolListQueryKey }) } } diff --git a/web/app/components/plugins/plugin-detail-panel/action-list.tsx b/web/app/components/plugins/plugin-detail-panel/action-list.tsx index 892520f0e20..761e90a6e8c 100644 --- a/web/app/components/plugins/plugin-detail-panel/action-list.tsx +++ b/web/app/components/plugins/plugin-detail-panel/action-list.tsx @@ -1,9 +1,14 @@ import type { PluginDetail } from '@/app/components/plugins/types' +import { useQuery } from '@tanstack/react-query' import * as React from 'react' import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import ToolItem from '@/app/components/tools/provider/tool-item' -import { useAllToolProviders, useBuiltinTools } from '@/service/use-tools' +import { + selectBuiltinProviderTools, + selectToolCollections, +} from '@/app/components/tools/utils/query-select' +import { consoleQuery } from '@/service/console' type Props = Readonly<{ detail: PluginDetail @@ -13,11 +18,22 @@ const ActionList = ({ detail }: Props) => { const { t } = useTranslation(['plugin']) const providerBriefInfo = detail.declaration?.tool?.identity const providerKey = providerBriefInfo ? `${detail.plugin_id}/${providerBriefInfo.name}` : '' - const { data: collectionList = [] } = useAllToolProviders() + const { data: collectionList = [] } = useQuery( + consoleQuery.workspaces.current.toolProviders.get.queryOptions({ + input: { query: {} }, + select: selectToolCollections, + }), + ) const provider = useMemo(() => { return collectionList.find((collection) => collection.name === providerKey) }, [collectionList, providerKey]) - const { data } = useBuiltinTools(providerKey) + const { data } = useQuery( + consoleQuery.workspaces.current.toolProvider.builtin.byProvider.tools.get.queryOptions({ + input: { params: { provider: providerKey } }, + enabled: !!providerKey, + select: selectBuiltinProviderTools, + }), + ) if (!providerKey || !data || !provider) return null diff --git a/web/app/components/plugins/plugin-detail-panel/detail-header/__tests__/index.spec.tsx b/web/app/components/plugins/plugin-detail-panel/detail-header/__tests__/index.spec.tsx index 0ef3bb63d0c..3069b579325 100644 --- a/web/app/components/plugins/plugin-detail-panel/detail-header/__tests__/index.spec.tsx +++ b/web/app/components/plugins/plugin-detail-panel/detail-header/__tests__/index.spec.tsx @@ -24,17 +24,20 @@ vi.mock('@/hooks/use-theme', () => ({ default: () => ({ theme: 'light' }), })) -vi.mock('@/service/use-tools', () => ({ - useAllToolProviders: () => ({ - data: [ - { - name: 'tool-plugin/provider-a', - type: 'builtin', - allow_delete: true, - }, - ], - }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.toolProviders.get': () => ({ + data: [ + { + name: 'tool-plugin/provider-a', + type: 'builtin', + allow_delete: true, + }, + ], + }), + }) +}) vi.mock('@/service/use-plugins', () => ({ useVersionListOfPlugin: () => ({ data: { data: { versions: [] } } }), diff --git a/web/app/components/plugins/plugin-detail-panel/detail-header/index.tsx b/web/app/components/plugins/plugin-detail-panel/detail-header/index.tsx index f318f7864f2..a95283c041a 100644 --- a/web/app/components/plugins/plugin-detail-panel/detail-header/index.tsx +++ b/web/app/components/plugins/plugin-detail-panel/detail-header/index.tsx @@ -15,11 +15,12 @@ import { OperationDropdown } from '@/app/components/plugins/plugin-detail-panel/ import { BUILTIN_TOOLS_ARRAY } from '@/app/components/plugins/readme-panel/constants' import { useReadmePanelStore } from '@/app/components/plugins/readme-panel/store' import PluginVersionPicker from '@/app/components/plugins/update-plugin/plugin-version-picker' +import { selectToolCollections } from '@/app/components/tools/utils/query-select' import { API_PREFIX } from '@/config' import { useGetLanguage } from '@/context/i18n' import { userProfileQueryOptions } from '@/features/account-profile/client' import useTheme from '@/hooks/use-theme' -import { useAllToolProviders } from '@/service/use-tools' +import { consoleQuery } from '@/service/console' import { getMarketplaceUrl } from '@/utils/var' import Verified from '../../base/badges/verified' import DeprecationNotice from '../../base/deprecation-notice' @@ -134,7 +135,13 @@ const DetailHeader = ({ const isTool = category === PluginCategoryEnum.tool const providerBriefInfo = tool?.identity const providerKey = `${plugin_id}/${providerBriefInfo?.name}` - const { data: collectionList = [] } = useAllToolProviders(isTool) + const { data: collectionList = [] } = useQuery( + consoleQuery.workspaces.current.toolProviders.get.queryOptions({ + input: { query: {} }, + enabled: isTool, + select: selectToolCollections, + }), + ) const provider = useMemo(() => { return collectionList.find((collection) => collection.name === providerKey) }, [collectionList, providerKey]) diff --git a/web/app/components/plugins/plugin-detail-panel/multiple-tool-selector/__tests__/focus-restoration.spec.tsx b/web/app/components/plugins/plugin-detail-panel/multiple-tool-selector/__tests__/focus-restoration.spec.tsx index 97e0bc22ca0..b633fccbf85 100644 --- a/web/app/components/plugins/plugin-detail-panel/multiple-tool-selector/__tests__/focus-restoration.spec.tsx +++ b/web/app/components/plugins/plugin-detail-panel/multiple-tool-selector/__tests__/focus-restoration.spec.tsx @@ -1,23 +1,26 @@ import type { ToolValue } from '@/app/components/workflow/block-selector/types' import type { ToolWithProvider } from '@/app/components/workflow/types' -import { render, screen } from '@testing-library/react' +import { screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { useState } from 'react' import { CollectionType } from '@/app/components/tools/types' import { MCPToolAvailabilityProvider } from '@/app/components/workflow/nodes/_base/components/mcp-tool-availability' +import { renderWithConsoleQuery } from '@/test/console/query-data' import MultipleToolSelector from '../index' const mockTools = vi.hoisted(() => ({ builtIn: [] as ToolWithProvider[], })) -vi.mock('@/service/use-tools', () => ({ - useAllBuiltInTools: () => ({ data: mockTools.builtIn, isFetched: true }), - useAllCustomTools: () => ({ data: [], isFetched: true }), - useAllMCPTools: () => ({ data: [], isFetched: true }), - useAllWorkflowTools: () => ({ data: [], isFetched: true }), - useInvalidateAllBuiltInTools: () => vi.fn(), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.tools.builtin.get': () => ({ data: mockTools.builtIn, isFetched: true }), + 'workspaces.current.tools.api.get': () => ({ data: [], isFetched: true }), + 'workspaces.current.tools.mcp.get': () => ({ data: [], isFetched: true }), + 'workspaces.current.tools.workflow.get': () => ({ data: [], isFetched: true }), + }) +}) vi.mock('@/service/use-plugins', () => ({ useCheckInstalled: () => ({ data: undefined }), @@ -77,7 +80,7 @@ function renderSelector(initialValue: ToolValue[]) { ) } - return render() + return renderWithConsoleQuery() } describe('MultipleToolSelector focus restoration', () => { diff --git a/web/app/components/plugins/plugin-detail-panel/multiple-tool-selector/__tests__/index.spec.tsx b/web/app/components/plugins/plugin-detail-panel/multiple-tool-selector/__tests__/index.spec.tsx index f6efaa7dce1..b1461060777 100644 --- a/web/app/components/plugins/plugin-detail-panel/multiple-tool-selector/__tests__/index.spec.tsx +++ b/web/app/components/plugins/plugin-detail-panel/multiple-tool-selector/__tests__/index.spec.tsx @@ -14,11 +14,14 @@ import MultipleToolSelector from '../index' // Mock useAllMCPTools hook const mockMCPToolsData = vi.fn<() => ToolWithProvider[] | undefined>(() => undefined) -vi.mock('@/service/use-tools', () => ({ - useAllMCPTools: () => ({ - data: mockMCPToolsData(), - }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.tools.mcp.get': () => ({ + data: mockMCPToolsData(), + }), + }) +}) // Track edit tool index for unique test IDs let editToolIndex = 0 diff --git a/web/app/components/plugins/plugin-detail-panel/multiple-tool-selector/index.tsx b/web/app/components/plugins/plugin-detail-panel/multiple-tool-selector/index.tsx index f9678e93958..c1d9a13c04c 100644 --- a/web/app/components/plugins/plugin-detail-panel/multiple-tool-selector/index.tsx +++ b/web/app/components/plugins/plugin-detail-panel/multiple-tool-selector/index.tsx @@ -7,11 +7,13 @@ import { IconButton } from '@langgenius/dify-ui/icon-button' import { Infotip, InfotipContent, InfotipTrigger } from '@langgenius/dify-ui/infotip' import { Separator } from '@langgenius/dify-ui/separator' import { useRefWithInit } from '@langgenius/dify-ui/use-ref-with-init' +import { useQuery } from '@tanstack/react-query' import * as React from 'react' import { useTranslation } from 'react-i18next' import ToolSelector from '@/app/components/plugins/plugin-detail-panel/tool-selector' +import { selectToolsWithProvider } from '@/app/components/tools/utils/query-select' import { useMCPToolAvailability } from '@/app/components/workflow/nodes/_base/components/mcp-tool-availability' -import { useAllMCPTools } from '@/service/use-tools' +import { consoleQuery } from '@/service/console' type Props = Readonly<{ disabled?: boolean @@ -46,7 +48,9 @@ const MultipleToolSelector = ({ const { t } = useTranslation(['appDebug', 'plugin']) const { allowed: isMCPToolAllowed } = useMCPToolAvailability() - const { data: mcpTools } = useAllMCPTools() + const { data: mcpTools } = useQuery( + consoleQuery.workspaces.current.tools.mcp.get.queryOptions({ select: selectToolsWithProvider }), + ) const addToolButtonRef = React.useRef(null) const toolItemTriggerByKeyRef = useRefWithInit(() => new Map()) const pendingFocusTargetRef = React.useRef<{ toolKey?: string } | null>(null) diff --git a/web/app/components/plugins/plugin-detail-panel/tool-selector/hooks/__tests__/use-tool-selector.spec.ts b/web/app/components/plugins/plugin-detail-panel/tool-selector/hooks/__tests__/use-tool-selector.spec.ts index 4ea540d54b5..ca1c24c6b29 100644 --- a/web/app/components/plugins/plugin-detail-panel/tool-selector/hooks/__tests__/use-tool-selector.spec.ts +++ b/web/app/components/plugins/plugin-detail-panel/tool-selector/hooks/__tests__/use-tool-selector.spec.ts @@ -2,8 +2,13 @@ import type { ToolValue } from '@/app/components/workflow/block-selector/types' import { act, renderHook } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' import { CollectionType } from '@/app/components/tools/types' +import { createQueryClientWrapper } from '@/test/console/query-client' +import { createTestQueryClient } from '@/test/query-client' import { useToolSelector } from '../use-tool-selector' +const queryClient = createTestQueryClient() +const queryClientWrapper = createQueryClientWrapper(queryClient) + const mockToolParams = [ { name: 'param1', form: 'llm', type: 'string', required: true, label: { en_US: 'Param 1' } }, { name: 'param2', form: 'form', type: 'number', required: false, label: { en_US: 'Param 2' } }, @@ -27,13 +32,18 @@ const mockTools = [ ] let areToolQueriesFetched = true -vi.mock('@/service/use-tools', () => ({ - useAllBuiltInTools: () => ({ data: mockTools, isFetched: areToolQueriesFetched }), - useAllCustomTools: () => ({ data: [], isFetched: areToolQueriesFetched }), - useAllWorkflowTools: () => ({ data: [], isFetched: areToolQueriesFetched }), - useAllMCPTools: () => ({ data: [], isFetched: areToolQueriesFetched }), - useInvalidateAllBuiltInTools: () => vi.fn().mockResolvedValue(undefined), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.tools.builtin.get': () => ({ + data: mockTools, + isFetched: areToolQueriesFetched, + }), + 'workspaces.current.tools.api.get': () => ({ data: [], isFetched: areToolQueriesFetched }), + 'workspaces.current.tools.workflow.get': () => ({ data: [], isFetched: areToolQueriesFetched }), + 'workspaces.current.tools.mcp.get': () => ({ data: [], isFetched: areToolQueriesFetched }), + }) +}) vi.mock('@/service/use-plugins', () => ({ useInvalidateInstalledPluginList: () => vi.fn().mockResolvedValue(undefined), @@ -88,7 +98,9 @@ describe('useToolSelector', () => { }) it('should initialize with default panel states', () => { - const { result } = renderHook(() => useToolSelector({ onSelect: mockOnSelect })) + const { result } = renderHook(() => useToolSelector({ onSelect: mockOnSelect }), { + wrapper: queryClientWrapper, + }) expect(result.current.isShow).toBe(false) expect(result.current.isShowChooseTool).toBe(false) @@ -96,8 +108,9 @@ describe('useToolSelector', () => { }) it('should find current provider from tool value', () => { - const { result } = renderHook(() => - useToolSelector({ value: toolValue, onSelect: mockOnSelect }), + const { result } = renderHook( + () => useToolSelector({ value: toolValue, onSelect: mockOnSelect }), + { wrapper: queryClientWrapper }, ) expect(result.current.currentProvider).toBeDefined() @@ -105,8 +118,9 @@ describe('useToolSelector', () => { }) it('should find current tool from provider', () => { - const { result } = renderHook(() => - useToolSelector({ value: toolValue, onSelect: mockOnSelect }), + const { result } = renderHook( + () => useToolSelector({ value: toolValue, onSelect: mockOnSelect }), + { wrapper: queryClientWrapper }, ) expect(result.current.currentTool).toBeDefined() @@ -114,8 +128,9 @@ describe('useToolSelector', () => { }) it('should compute tool settings and params correctly', () => { - const { result } = renderHook(() => - useToolSelector({ value: toolValue, onSelect: mockOnSelect }), + const { result } = renderHook( + () => useToolSelector({ value: toolValue, onSelect: mockOnSelect }), + { wrapper: queryClientWrapper }, ) // param2 has form='form' (not 'llm'), so it goes to settings @@ -128,8 +143,9 @@ describe('useToolSelector', () => { }) it('should show tab slider when both settings and params exist', () => { - const { result } = renderHook(() => - useToolSelector({ value: toolValue, onSelect: mockOnSelect }), + const { result } = renderHook( + () => useToolSelector({ value: toolValue, onSelect: mockOnSelect }), + { wrapper: queryClientWrapper }, ) expect(result.current.showTabSlider).toBe(true) @@ -138,7 +154,9 @@ describe('useToolSelector', () => { }) it('should toggle panel visibility', () => { - const { result } = renderHook(() => useToolSelector({ onSelect: mockOnSelect })) + const { result } = renderHook(() => useToolSelector({ onSelect: mockOnSelect }), { + wrapper: queryClientWrapper, + }) act(() => { result.current.setIsShow(true) @@ -152,7 +170,9 @@ describe('useToolSelector', () => { }) it('should switch tab type', () => { - const { result } = renderHook(() => useToolSelector({ onSelect: mockOnSelect })) + const { result } = renderHook(() => useToolSelector({ onSelect: mockOnSelect }), { + wrapper: queryClientWrapper, + }) act(() => { result.current.setCurrType('params') @@ -161,8 +181,9 @@ describe('useToolSelector', () => { }) it('should handle description change', () => { - const { result } = renderHook(() => - useToolSelector({ value: toolValue, onSelect: mockOnSelect }), + const { result } = renderHook( + () => useToolSelector({ value: toolValue, onSelect: mockOnSelect }), + { wrapper: queryClientWrapper }, ) act(() => { @@ -177,8 +198,9 @@ describe('useToolSelector', () => { }) it('should handle enabled change', () => { - const { result } = renderHook(() => - useToolSelector({ value: toolValue, onSelect: mockOnSelect }), + const { result } = renderHook( + () => useToolSelector({ value: toolValue, onSelect: mockOnSelect }), + { wrapper: queryClientWrapper }, ) act(() => { @@ -193,8 +215,9 @@ describe('useToolSelector', () => { }) it('should handle authorization item click', () => { - const { result } = renderHook(() => - useToolSelector({ value: toolValue, onSelect: mockOnSelect }), + const { result } = renderHook( + () => useToolSelector({ value: toolValue, onSelect: mockOnSelect }), + { wrapper: queryClientWrapper }, ) act(() => { @@ -209,7 +232,9 @@ describe('useToolSelector', () => { }) it('should not call onSelect if value is undefined', () => { - const { result } = renderHook(() => useToolSelector({ onSelect: mockOnSelect })) + const { result } = renderHook(() => useToolSelector({ onSelect: mockOnSelect }), { + wrapper: queryClientWrapper, + }) act(() => { result.current.handleEnabledChange(true) @@ -218,11 +243,13 @@ describe('useToolSelector', () => { }) it('should return empty arrays when no provider matches', () => { - const { result } = renderHook(() => - useToolSelector({ - value: { ...toolValue, provider_name: 'nonexistent' }, - onSelect: mockOnSelect, - }), + const { result } = renderHook( + () => + useToolSelector({ + value: { ...toolValue, provider_name: 'nonexistent' }, + onSelect: mockOnSelect, + }), + { wrapper: queryClientWrapper }, ) expect(result.current.currentProvider).toBeUndefined() @@ -232,7 +259,9 @@ describe('useToolSelector', () => { }) it('should skip plugin checks after resolving the current provider and tool', () => { - renderHook(() => useToolSelector({ value: toolValue, onSelect: mockOnSelect })) + renderHook(() => useToolSelector({ value: toolValue, onSelect: mockOnSelect }), { + wrapper: queryClientWrapper, + }) expect(mockUsePluginInstalledCheck).toHaveBeenCalledWith({ providerPluginId: 'org/test-plugin', @@ -241,11 +270,13 @@ describe('useToolSelector', () => { }) it('should keep plugin checks enabled when the current tool cannot be resolved', () => { - renderHook(() => - useToolSelector({ - value: { ...toolValue, tool_name: 'missing-tool' }, - onSelect: mockOnSelect, - }), + renderHook( + () => + useToolSelector({ + value: { ...toolValue, tool_name: 'missing-tool' }, + onSelect: mockOnSelect, + }), + { wrapper: queryClientWrapper }, ) expect(mockUsePluginInstalledCheck).toHaveBeenCalledWith({ @@ -255,15 +286,17 @@ describe('useToolSelector', () => { }) it('should keep marketplace fallback enabled after tool queries settle without resolving the provider', () => { - renderHook(() => - useToolSelector({ - value: { - ...toolValue, - provider_name: 'org/market-plugin/search', - plugin_id: 'org/market-plugin', - }, - onSelect: mockOnSelect, - }), + renderHook( + () => + useToolSelector({ + value: { + ...toolValue, + provider_name: 'org/market-plugin/search', + plugin_id: 'org/market-plugin', + }, + onSelect: mockOnSelect, + }), + { wrapper: queryClientWrapper }, ) expect(mockUsePluginInstalledCheck).toHaveBeenCalledWith({ @@ -275,15 +308,17 @@ describe('useToolSelector', () => { it('should keep marketplace fallback enabled when legacy provider lists are still pending but plugin id is known', () => { areToolQueriesFetched = false - renderHook(() => - useToolSelector({ - value: { - ...toolValue, - provider_name: 'org/market-plugin/search', - plugin_id: 'org/market-plugin', - }, - onSelect: mockOnSelect, - }), + renderHook( + () => + useToolSelector({ + value: { + ...toolValue, + provider_name: 'org/market-plugin/search', + plugin_id: 'org/market-plugin', + }, + onSelect: mockOnSelect, + }), + { wrapper: queryClientWrapper }, ) expect(mockUsePluginInstalledCheck).toHaveBeenCalledWith({ @@ -293,16 +328,18 @@ describe('useToolSelector', () => { }) it('should skip marketplace checks for unresolved non-plugin workflow tools', () => { - renderHook(() => - useToolSelector({ - value: { - ...toolValue, - provider_name: 'author/tool-b', - plugin_id: undefined, - type: CollectionType.workflow, - }, - onSelect: mockOnSelect, - }), + renderHook( + () => + useToolSelector({ + value: { + ...toolValue, + provider_name: 'author/tool-b', + plugin_id: undefined, + type: CollectionType.workflow, + }, + onSelect: mockOnSelect, + }), + { wrapper: queryClientWrapper }, ) expect(mockUsePluginInstalledCheck).toHaveBeenCalledWith({ diff --git a/web/app/components/plugins/plugin-detail-panel/tool-selector/hooks/use-tool-selector.ts b/web/app/components/plugins/plugin-detail-panel/tool-selector/hooks/use-tool-selector.ts index 973c670a76f..dd8241f0336 100644 --- a/web/app/components/plugins/plugin-detail-panel/tool-selector/hooks/use-tool-selector.ts +++ b/web/app/components/plugins/plugin-detail-panel/tool-selector/hooks/use-tool-selector.ts @@ -3,23 +3,19 @@ import type { ReasoningConfigValue } from '../components/reasoning-config-form' import type { ToolParameter } from '@/app/components/tools/types' import type { ToolDefaultValue, ToolValue } from '@/app/components/workflow/block-selector/types' import type { ResourceVarInputs } from '@/app/components/workflow/nodes/_base/types' +import { useQuery, useQueryClient } from '@tanstack/react-query' import { useCallback, useMemo, useState } from 'react' import { PluginCategoryEnum } from '@/app/components/plugins/types' import { CollectionType } from '@/app/components/tools/types' +import { selectToolsWithProvider } from '@/app/components/tools/utils/query-select' import { generateFormValue, getPlainValue, getStructureValue, toolParametersToFormSchemas, } from '@/app/components/tools/utils/to-form-schema' +import { consoleQuery } from '@/service/console' import { useInvalidateInstalledPluginList } from '@/service/use-plugins' -import { - useAllBuiltInTools, - useAllCustomTools, - useAllMCPTools, - useAllWorkflowTools, - useInvalidateAllBuiltInTools, -} from '@/service/use-tools' import { getIconFromMarketPlace } from '@/utils/get-icon' import { matchesProviderReference } from '@/utils/provider-reference' import { usePluginInstalledCheck } from './use-plugin-installed-check' @@ -37,15 +33,27 @@ export function useToolSelector({ value, onSelect, onSelectMultiple }: UseToolSe const [isShowChooseTool, setIsShowChooseTool] = useState(false) const [currType, setCurrType] = useState('settings') - const buildInToolsQuery = useAllBuiltInTools() - const customToolsQuery = useAllCustomTools() - const workflowToolsQuery = useAllWorkflowTools() - const mcpToolsQuery = useAllMCPTools() + const buildInToolsQuery = useQuery( + consoleQuery.workspaces.current.tools.builtin.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const customToolsQuery = useQuery( + consoleQuery.workspaces.current.tools.api.get.queryOptions({ select: selectToolsWithProvider }), + ) + const workflowToolsQuery = useQuery( + consoleQuery.workspaces.current.tools.workflow.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const mcpToolsQuery = useQuery( + consoleQuery.workspaces.current.tools.mcp.get.queryOptions({ select: selectToolsWithProvider }), + ) const { data: buildInTools } = buildInToolsQuery const { data: customTools } = customToolsQuery const { data: workflowTools } = workflowToolsQuery const { data: mcpTools } = mcpToolsQuery - const invalidateAllBuiltinTools = useInvalidateAllBuiltInTools() + const queryClient = useQueryClient() const invalidateInstalledPluginList = useInvalidateInstalledPluginList() const currentProvider = useMemo(() => { @@ -245,7 +253,9 @@ export function useToolSelector({ value, onSelect, onSelectMultiple }: UseToolSe const handleInstall = useCallback(async () => { try { - await invalidateAllBuiltinTools() + await queryClient.invalidateQueries({ + queryKey: consoleQuery.workspaces.current.tools.builtin.get.queryKey(), + }) } catch (error) { console.error('Failed to invalidate built-in tools cache', error) } @@ -254,7 +264,7 @@ export function useToolSelector({ value, onSelect, onSelectMultiple }: UseToolSe } catch (error) { console.error('Failed to invalidate installed plugin list cache', error) } - }, [invalidateAllBuiltinTools, invalidateInstalledPluginList]) + }, [invalidateInstalledPluginList, queryClient]) const settingsValue = useMemo((): ResourceVarInputs => { return getPlainValue( diff --git a/web/app/components/tools/marketplace/__tests__/index.spec.tsx b/web/app/components/tools/marketplace/__tests__/index.spec.tsx index e49901b3012..71649cfa384 100644 --- a/web/app/components/tools/marketplace/__tests__/index.spec.tsx +++ b/web/app/components/tools/marketplace/__tests__/index.spec.tsx @@ -47,11 +47,6 @@ vi.mock('@/app/components/plugins/marketplace/hooks', () => ({ useMarketplacePlugins: (...args: unknown[]) => mockUseMarketplacePlugins(...args), })) -const mockUseAllToolProviders = vi.fn() -vi.mock('@/service/use-tools', () => ({ - useAllToolProviders: (...args: unknown[]) => mockUseAllToolProviders(...args), -})) - vi.mock('@/utils/var', () => ({ getMarketplaceUrl: vi.fn(() => 'https://marketplace.test/market'), })) diff --git a/web/app/components/tools/mcp/__tests__/create-card.spec.tsx b/web/app/components/tools/mcp/__tests__/create-card.spec.tsx index b8fc457486c..32926ef761e 100644 --- a/web/app/components/tools/mcp/__tests__/create-card.spec.tsx +++ b/web/app/components/tools/mcp/__tests__/create-card.spec.tsx @@ -12,11 +12,14 @@ import NewMCPCard, { NewMCPButton } from '../create-card' const mockCreateMCP = vi.fn().mockResolvedValue({ id: 'new-mcp-id', name: 'New MCP' }) // Mock the service -vi.mock('@/service/use-tools', () => ({ - useCreateMCP: () => ({ - mutateAsync: mockCreateMCP, - }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.toolProvider.mcp.post': () => ({ + mutateAsync: mockCreateMCP, + }), + }) +}) // Mock the MCP Modal type MockMCPModalProps = { @@ -196,8 +199,10 @@ describe('NewMCPCard', () => { await waitFor(() => { expect(mockCreateMCP).toHaveBeenCalledWith({ - name: 'Test MCP', - server_url: 'https://test.com', + body: { + name: 'Test MCP', + server_url: 'https://test.com', + }, }) expect(handleCreate).toHaveBeenCalled() }) diff --git a/web/app/components/tools/mcp/__tests__/index.spec.tsx b/web/app/components/tools/mcp/__tests__/index.spec.tsx index 4dd304794d2..d74d6a24a2b 100644 --- a/web/app/components/tools/mcp/__tests__/index.spec.tsx +++ b/web/app/components/tools/mcp/__tests__/index.spec.tsx @@ -23,23 +23,26 @@ const mockConsoleState = vi.hoisted(() => ({ workspacePermissionKeys: ['mcp.manage'] as string[], })) -vi.mock('@/service/use-tools', () => ({ - useAllMCPTools: (enabled?: boolean) => { - mockUseAllMCPTools(enabled) - return { - data: mockProviders, - isLoading: mockIsLoadingToolProviders, - refetch: mockRefetch, - } - }, - useUpdateMCP: () => ({ - mutateAsync: mockUpdateMCP, - }), - useDeleteMCP: () => ({ - mutateAsync: mockDeleteMCP, - isPending: false, - }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.tools.mcp.get': ({ enabled }: { enabled?: boolean }) => { + mockUseAllMCPTools(enabled) + return { + data: mockProviders, + isLoading: mockIsLoadingToolProviders, + refetch: mockRefetch, + } + }, + 'workspaces.current.toolProvider.mcp.put': () => ({ + mutateAsync: mockUpdateMCP, + }), + 'workspaces.current.toolProvider.mcp.delete': () => ({ + mutateAsync: mockDeleteMCP, + isPending: false, + }), + }) +}) vi.mock('@/context/permission-state', async () => { const { createPermissionStateModuleMock } = await import('@/test/console/state-fixture') @@ -475,9 +478,11 @@ describe('MCPList', () => { }) expect(mockUpdateMCP).toHaveBeenCalledWith({ - name: 'Updated MCP', - server_url: 'https://updated.com', - provider_id: '1', + body: { + name: 'Updated MCP', + server_url: 'https://updated.com', + provider_id: '1', + }, }) expect(mockRefetch).toHaveBeenCalled() expect(screen.getByTestId('detail-panel')).toBeInTheDocument() @@ -528,7 +533,7 @@ describe('MCPList', () => { await Promise.resolve() }) - expect(mockDeleteMCP).toHaveBeenCalledWith('1') + expect(mockDeleteMCP).toHaveBeenCalledWith({ body: { provider_id: '1' } }) expect(mockRefetch).toHaveBeenCalled() expect(screen.queryByTestId('detail-panel')).not.toBeInTheDocument() }) diff --git a/web/app/components/tools/mcp/__tests__/mcp-server-modal.spec.tsx b/web/app/components/tools/mcp/__tests__/mcp-server-modal.spec.tsx index 6f6ce929618..154899a1158 100644 --- a/web/app/components/tools/mcp/__tests__/mcp-server-modal.spec.tsx +++ b/web/app/components/tools/mcp/__tests__/mcp-server-modal.spec.tsx @@ -7,17 +7,19 @@ import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' import MCPServerModal from '../mcp-server-modal' // Mock the services -vi.mock('@/service/use-tools', () => ({ - useCreateMCPServer: () => ({ - mutateAsync: vi.fn().mockResolvedValue({ result: 'success' }), - isPending: false, - }), - useUpdateMCPServer: () => ({ - mutateAsync: vi.fn().mockResolvedValue({ result: 'success' }), - isPending: false, - }), - useInvalidateMCPServerDetail: () => vi.fn(), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'apps.byAppId.server.post': () => ({ + mutateAsync: vi.fn().mockResolvedValue({ result: 'success' }), + isPending: false, + }), + 'apps.byAppId.server.put': () => ({ + mutateAsync: vi.fn().mockResolvedValue({ result: 'success' }), + isPending: false, + }), + }) +}) describe('MCPServerModal', () => { const createWrapper = () => { diff --git a/web/app/components/tools/mcp/create-card.tsx b/web/app/components/tools/mcp/create-card.tsx index 03b9477f0af..066fa39e199 100644 --- a/web/app/components/tools/mcp/create-card.tsx +++ b/web/app/components/tools/mcp/create-card.tsx @@ -1,29 +1,36 @@ 'use client' +import type { ComponentProps } from 'react' import type { ToolWithProvider } from '@/app/components/workflow/types' import { Button } from '@langgenius/dify-ui/button' +import { useMutation } from '@tanstack/react-query' import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { STEP_BY_STEP_TOUR_TARGETS } from '@/app/components/step-by-step-tour/target-registry' import { useCanManageMCP } from '@/app/components/tools/hooks/use-tool-permissions' import { useDocLink } from '@/context/i18n' -import { useCreateMCP } from '@/service/use-tools' +import { consoleQuery } from '@/service/console' import CreateEntryCard from '../provider/create-entry-card' +import { selectToolProvider } from '../utils/query-select' import MCPModal from './modal' +type MCPModalConfirmPayload = Parameters['onConfirm']>[0] + type Props = Readonly<{ handleCreate: (provider: ToolWithProvider) => Promise | void }> function useMCPCreateAction({ handleCreate }: Props) { const canManageMCP = useCanManageMCP() - const { mutateAsync: createMCP } = useCreateMCP() + const { mutateAsync: createMCP } = useMutation( + consoleQuery.workspaces.current.toolProvider.mcp.post.mutationOptions(), + ) const [showModal, setShowModal] = useState(false) - const create = async (info: Parameters[0]) => { + const create = async (info: MCPModalConfirmPayload) => { if (!canManageMCP) return - const provider = await createMCP(info) - await handleCreate(provider) + const provider = await createMCP({ body: info }) + await handleCreate(selectToolProvider(provider)) } return { diff --git a/web/app/components/tools/mcp/detail/__tests__/content.spec.tsx b/web/app/components/tools/mcp/detail/__tests__/content.spec.tsx index 4359bd4c2cc..1ccbeed6af0 100644 --- a/web/app/components/tools/mcp/detail/__tests__/content.spec.tsx +++ b/web/app/components/tools/mcp/detail/__tests__/content.spec.tsx @@ -4,14 +4,19 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { fireEvent, screen, waitFor } from '@testing-library/react' import * as React from 'react' import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' +import { consoleQuery } from '@/service/console' import { render } from '@/test/console/render' import MCPDetailContent from '../content' // Mutable mock functions const mockUpdateTools = vi.fn().mockResolvedValue({}) const mockAuthorizeMcp = vi.fn().mockResolvedValue({ result: 'success' }) -const mockInvalidateMCPTools = vi.fn() -const mockInvalidateAllMCPTools = vi.fn() +const mockInvalidateQueries = vi.fn() +const mcpToolsKey = consoleQuery.workspaces.current.tools.mcp.get.queryKey() +const mcpProviderToolsKey = + consoleQuery.workspaces.current.toolProvider.mcp.tools.byProviderId.get.queryKey({ + input: { params: { provider_id: 'mcp-1' } }, + }) const mockOpenOAuthPopup = vi.fn() // Mutable mock state @@ -27,22 +32,23 @@ let mockIsUpdating = false let mockIsAuthorizing = false // Mock the services -vi.mock('@/service/use-tools', () => ({ - useMCPTools: () => ({ - data: mockToolsData, - isFetching: mockIsFetching, - }), - useInvalidateMCPTools: () => mockInvalidateMCPTools, - useInvalidateAllMCPTools: () => mockInvalidateAllMCPTools, - useUpdateMCPTools: () => ({ - mutateAsync: mockUpdateTools, - isPending: mockIsUpdating, - }), - useAuthorizeMCP: () => ({ - mutateAsync: mockAuthorizeMcp, - isPending: mockIsAuthorizing, - }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.toolProvider.mcp.tools.byProviderId.get': () => ({ + data: mockToolsData, + isFetching: mockIsFetching, + }), + 'workspaces.current.toolProvider.mcp.update.byProviderId.get': () => ({ + mutateAsync: mockUpdateTools, + isPending: mockIsUpdating, + }), + 'workspaces.current.toolProvider.mcp.auth.post': () => ({ + mutateAsync: mockAuthorizeMcp, + isPending: mockIsAuthorizing, + }), + }) +}) // Mock OAuth hook type OAuthArgs = readonly unknown[] @@ -118,6 +124,7 @@ describe('MCPDetailContent', () => { }, }, }) + vi.spyOn(queryClient, 'invalidateQueries').mockImplementation(mockInvalidateQueries) return ({ children }: { children: ReactNode }) => React.createElement(QueryClientProvider, { client: queryClient }, children) } @@ -151,8 +158,7 @@ describe('MCPDetailContent', () => { // Reset mocks mockUpdateTools.mockClear() mockAuthorizeMcp.mockClear() - mockInvalidateMCPTools.mockClear() - mockInvalidateAllMCPTools.mockClear() + mockInvalidateQueries.mockReset().mockResolvedValue(undefined) mockOpenOAuthPopup.mockClear() // Reset mock return values @@ -349,7 +355,7 @@ describe('MCPDetailContent', () => { await waitFor(() => { expect(onFirstCreate).toHaveBeenCalled() - expect(mockAuthorizeMcp).toHaveBeenCalledWith({ provider_id: 'mcp-1' }) + expect(mockAuthorizeMcp).toHaveBeenCalledWith({ body: { provider_id: 'mcp-1' } }) }) }) @@ -435,9 +441,9 @@ describe('MCPDetailContent', () => { fireEvent.click(getConfirmButton()) await waitFor(() => { - expect(mockUpdateTools).toHaveBeenCalledWith('mcp-1') - expect(mockInvalidateMCPTools).toHaveBeenCalledWith('mcp-1') - expect(mockInvalidateAllMCPTools).toHaveBeenCalled() + expect(mockUpdateTools).toHaveBeenCalledWith({ params: { provider_id: 'mcp-1' } }) + expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: mcpProviderToolsKey }) + expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: mcpToolsKey }) expect(onUpdate).toHaveBeenCalled() }) }) @@ -453,8 +459,8 @@ describe('MCPDetailContent', () => { fireEvent.click(getToolsBtn) await waitFor(() => { - expect(mockUpdateTools).toHaveBeenCalledWith('mcp-1') - expect(mockInvalidateAllMCPTools).toHaveBeenCalled() + expect(mockUpdateTools).toHaveBeenCalledWith({ params: { provider_id: 'mcp-1' } }) + expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: mcpToolsKey }) }) }) }) @@ -533,7 +539,7 @@ describe('MCPDetailContent', () => { oauthCallback() await waitFor(() => { - expect(mockUpdateTools).toHaveBeenCalledWith('mcp-1') + expect(mockUpdateTools).toHaveBeenCalledWith({ params: { provider_id: 'mcp-1' } }) }) }) diff --git a/web/app/components/tools/mcp/detail/content.tsx b/web/app/components/tools/mcp/detail/content.tsx index 8717e560ea2..e1ad0265c32 100644 --- a/web/app/components/tools/mcp/detail/content.tsx +++ b/web/app/components/tools/mcp/detail/content.tsx @@ -15,6 +15,7 @@ import { cn } from '@langgenius/dify-ui/cn' import { IconButton } from '@langgenius/dify-ui/icon-button' import { StatusDot } from '@langgenius/dify-ui/status-dot' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useBoolean } from 'ahooks' import copy from 'copy-to-clipboard' import * as React from 'react' @@ -23,13 +24,8 @@ import { useTranslation } from 'react-i18next' import Icon from '@/app/components/plugins/card/base/card-icon' import { useCanManageMCP } from '@/app/components/tools/hooks/use-tool-permissions' import { openOAuthPopup } from '@/hooks/use-oauth' -import { - useAuthorizeMCP, - useInvalidateAllMCPTools, - useInvalidateMCPTools, - useMCPTools, - useUpdateMCPTools, -} from '@/service/use-tools' +import { consoleQuery } from '@/service/console' +import { selectToolProviderTools } from '../../utils/query-select' import ListLoading from './list-loading' import OperationDropdown from './operation-dropdown' import ToolItem from './tool-item' @@ -56,13 +52,20 @@ const MCPDetailContent: FC = ({ const { t } = useTranslation(['common', 'tools']) const canManageMCP = useCanManageMCP() - const { data, isFetching: isGettingTools } = useMCPTools( - detail.is_team_authorization ? detail.id : '', + const queryClient = useQueryClient() + const { data, isFetching: isGettingTools } = useQuery( + consoleQuery.workspaces.current.toolProvider.mcp.tools.byProviderId.get.queryOptions({ + input: { params: { provider_id: detail.id } }, + enabled: !!detail.is_team_authorization && !!detail.id, + select: selectToolProviderTools, + }), + ) + const { mutateAsync: updateTools, isPending: isUpdating } = useMutation( + consoleQuery.workspaces.current.toolProvider.mcp.update.byProviderId.get.mutationOptions(), + ) + const { mutateAsync: authorizeMcp, isPending: isAuthorizing } = useMutation( + consoleQuery.workspaces.current.toolProvider.mcp.auth.post.mutationOptions(), ) - const invalidateMCPTools = useInvalidateMCPTools() - const invalidateAllMCPTools = useInvalidateAllMCPTools() - const { mutateAsync: updateTools, isPending: isUpdating } = useUpdateMCPTools() - const { mutateAsync: authorizeMcp, isPending: isAuthorizing } = useAuthorizeMCP() const toolList = data?.tools || [] const [isShowUpdateConfirm, { setTrue: showUpdateConfirm, setFalse: hideUpdateConfirm }] = @@ -71,19 +74,17 @@ const MCPDetailContent: FC = ({ const handleUpdateTools = useCallback(async () => { hideUpdateConfirm() if (!canManageMCP || !detail) return - await updateTools(detail.id) - invalidateMCPTools(detail.id) - invalidateAllMCPTools() + await updateTools({ params: { provider_id: detail.id } }) + queryClient.invalidateQueries({ + queryKey: consoleQuery.workspaces.current.toolProvider.mcp.tools.byProviderId.get.queryKey({ + input: { params: { provider_id: detail.id } }, + }), + }) + queryClient.invalidateQueries({ + queryKey: consoleQuery.workspaces.current.tools.mcp.get.queryKey(), + }) onUpdate() - }, [ - canManageMCP, - detail, - hideUpdateConfirm, - invalidateAllMCPTools, - invalidateMCPTools, - onUpdate, - updateTools, - ]) + }, [canManageMCP, detail, hideUpdateConfirm, onUpdate, queryClient, updateTools]) const hasTriggeredAuthorizeRef = useRef(false) @@ -98,9 +99,7 @@ const MCPDetailContent: FC = ({ onFirstCreate() if (!detail) return try { - const res = await authorizeMcp({ - provider_id: detail.id, - }) + const res = await authorizeMcp({ body: { provider_id: detail.id } }) if (res.result === 'success') handleUpdateTools() else if (res.authorization_url) openOAuthPopup(res.authorization_url, handleOAuthCallback) } catch { diff --git a/web/app/components/tools/mcp/index.tsx b/web/app/components/tools/mcp/index.tsx index 6d6fc853af0..f55292fabec 100644 --- a/web/app/components/tools/mcp/index.tsx +++ b/web/app/components/tools/mcp/index.tsx @@ -13,13 +13,15 @@ import { AlertDialogTitle, } from '@langgenius/dify-ui/alert-dialog' import { cn } from '@langgenius/dify-ui/cn' +import { useMutation, useQuery } from '@tanstack/react-query' import { useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { STEP_BY_STEP_TOUR_TARGETS } from '@/app/components/step-by-step-tour/target-registry' import { useCanManageMCP } from '@/app/components/tools/hooks/use-tool-permissions' import ToolCardSkeletonGrid from '@/app/components/tools/provider/tool-card-skeleton' -import { useAllMCPTools, useDeleteMCP, useUpdateMCP } from '@/service/use-tools' +import { consoleQuery } from '@/service/console' import { toolsContentInsetClassNames, toolsUnifiedContentFrameClassName } from '../content-inset' +import { selectToolsWithProvider } from '../utils/query-select' import NewMCPCard from './create-card' import MCPDetailPanel from './detail/provider-detail' import MCPModal from './modal' @@ -54,7 +56,12 @@ const MCPList = ({ }: Props) => { const { t } = useTranslation(['common', 'tools']) const canManageMCP = useCanManageMCP() - const fallbackMCPToolsQuery = useAllMCPTools(providers === undefined) + const fallbackMCPToolsQuery = useQuery( + consoleQuery.workspaces.current.tools.mcp.get.queryOptions({ + enabled: providers === undefined, + select: selectToolsWithProvider, + }), + ) const list = providers ?? fallbackMCPToolsQuery.data ?? EMPTY_MCP_TOOLS const isLoading = isLoadingProviders ?? fallbackMCPToolsQuery.isLoading const refetch = onRefresh ?? fallbackMCPToolsQuery.refetch @@ -76,8 +83,12 @@ const MCPList = ({ const editingProvider = list.find((provider) => provider.id === editingProviderID) const deletingProvider = list.find((provider) => provider.id === deletingProviderID) const detailProvider = editingProvider || deletingProvider ? undefined : currentProvider - const { mutateAsync: updateMCP } = useUpdateMCP({}) - const { mutateAsync: deleteMCP, isPending: isDeleting } = useDeleteMCP({}) + const { mutateAsync: updateMCP } = useMutation( + consoleQuery.workspaces.current.toolProvider.mcp.put.mutationOptions(), + ) + const { mutateAsync: deleteMCP, isPending: isDeleting } = useMutation( + consoleQuery.workspaces.current.toolProvider.mcp.delete.mutationOptions(), + ) const handleCreate = async (provider: ToolWithProvider) => { if (!canManageMCP) return @@ -121,8 +132,7 @@ const MCPList = ({ if (!canManageMCP || !editingProvider) return const res = (await updateMCP({ - ...form, - provider_id: editingProvider.id, + body: { ...form, provider_id: editingProvider.id }, })) as MutationResult if (res.result !== 'success') return @@ -141,7 +151,9 @@ const MCPList = ({ const handleDeleteConfirm = async () => { if (!canManageMCP || !deletingProvider) return - const res = (await deleteMCP(deletingProvider.id)) as MutationResult + const res = (await deleteMCP({ + body: { provider_id: deletingProvider.id }, + })) as MutationResult if (res.result !== 'success') return await refetch() diff --git a/web/app/components/tools/mcp/mcp-server-modal.tsx b/web/app/components/tools/mcp/mcp-server-modal.tsx index 6673108f797..f4313504945 100644 --- a/web/app/components/tools/mcp/mcp-server-modal.tsx +++ b/web/app/components/tools/mcp/mcp-server-modal.tsx @@ -5,14 +5,11 @@ import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog' import { Separator } from '@langgenius/dify-ui/separator' import { Textarea } from '@langgenius/dify-ui/textarea' import { RiCloseLine } from '@remixicon/react' +import { useMutation, useQueryClient } from '@tanstack/react-query' import * as React from 'react' import { useTranslation } from 'react-i18next' import MCPServerParamItem from '@/app/components/tools/mcp/mcp-server-param-item' -import { - useCreateMCPServer, - useInvalidateMCPServerDetail, - useUpdateMCPServer, -} from '@/service/use-tools' +import { consoleQuery } from '@/service/console' type ModalProps = { appID: string @@ -33,9 +30,19 @@ type MCPServerParam = { const MCPServerModal = ({ appID, latestParams = [], data, show, onHide, appInfo }: ModalProps) => { const { t } = useTranslation(['common', 'tools']) - const { mutateAsync: createMCPServer, isPending: creating } = useCreateMCPServer() - const { mutateAsync: updateMCPServer, isPending: updating } = useUpdateMCPServer() - const invalidateMCPServerDetail = useInvalidateMCPServerDetail() + const queryClient = useQueryClient() + const { mutateAsync: createMCPServer, isPending: creating } = useMutation( + consoleQuery.apps.byAppId.server.post.mutationOptions(), + ) + const { mutateAsync: updateMCPServer, isPending: updating } = useMutation( + consoleQuery.apps.byAppId.server.put.mutationOptions(), + ) + const invalidateMCPServerDetail = () => + queryClient.invalidateQueries({ + queryKey: consoleQuery.apps.byAppId.server.get.queryKey({ + input: { params: { app_id: appID } }, + }), + }) const defaultDescription = data?.description || appInfo?.description || '' const [description, setDescription] = React.useState(defaultDescription) @@ -61,35 +68,28 @@ const MCPServerModal = ({ appID, latestParams = [], data, show, onHide, appInfo const submit = async () => { if (!data) { - const payload: { - appID: string + const body: { description?: string parameters: Record } = { - appID, parameters: getParamValue(), } - if (description.trim()) payload.description = description + if (description.trim()) body.description = description - await createMCPServer(payload) - invalidateMCPServerDetail(appID) + await createMCPServer({ params: { app_id: appID }, body }) + invalidateMCPServerDetail() onHide() } else { - const payload: { - appID: string - id: string - description: string - parameters: Record - } = { - appID, - id: data.id, - parameters: getParamValue(), - description, - } - - await updateMCPServer(payload) - invalidateMCPServerDetail(appID) + await updateMCPServer({ + params: { app_id: appID }, + body: { + id: data.id, + parameters: getParamValue(), + description, + }, + }) + invalidateMCPServerDetail() onHide() } } diff --git a/web/app/components/tools/mcp/modal.tsx b/web/app/components/tools/mcp/modal.tsx index 0cf2156549e..ab9c0390493 100644 --- a/web/app/components/tools/mcp/modal.tsx +++ b/web/app/components/tools/mcp/modal.tsx @@ -44,9 +44,8 @@ type MCPModalConfirmPayload = { headers?: Record is_dynamic_registration?: boolean authentication?: { - client_id?: string + client_id: string client_secret?: string - grant_type?: string } configuration: { timeout: number diff --git a/web/app/components/tools/provider/__tests__/detail.spec.tsx b/web/app/components/tools/provider/__tests__/detail.spec.tsx index 8853010d663..cf811304a8e 100644 --- a/web/app/components/tools/provider/__tests__/detail.spec.tsx +++ b/web/app/components/tools/provider/__tests__/detail.spec.tsx @@ -109,10 +109,6 @@ vi.mock('@/service/tools', () => ({ saveWorkflowToolProvider: (...args: unknown[]) => mockSaveWorkflowToolProvider(...args), })) -vi.mock('@/service/use-tools', () => ({ - useInvalidateAllWorkflowTools: () => vi.fn(), -})) - vi.mock('@/utils/var', () => ({ basePath: '', })) diff --git a/web/app/components/tools/provider/detail.tsx b/web/app/components/tools/provider/detail.tsx index 13c20b51b3f..64f2ec50181 100644 --- a/web/app/components/tools/provider/detail.tsx +++ b/web/app/components/tools/provider/detail.tsx @@ -61,7 +61,6 @@ import { updateBuiltInToolCredential, updateCustomCollection, } from '@/service/tools' -import { useInvalidateAllWorkflowTools } from '@/service/use-tools' import { basePath } from '@/utils/var' import { AuthHeaderPrefix, AuthType, CollectionType } from '../types' import ToolItem from './tool-item' @@ -85,7 +84,6 @@ const ProviderDetail = ({ collection, onHide, onRefreshData }: Props) => { const canOpenCredentialSettings = isAuthed ? canUseCredential : canCreateCredential const canSaveCredentialSettings = isAuthed ? canManageCredential : canCreateCredential const canManageTools = useCanManageTools() - const invalidateAllWorkflowTools = useInvalidateAllWorkflowTools() const [isDetailLoading, setIsDetailLoading] = useState(false) // built in provider @@ -205,7 +203,9 @@ const ProviderDetail = ({ collection, onHide, onRefreshData }: Props) => { if (!canManageTools) return await saveWorkflowToolProvider(data) - invalidateAllWorkflowTools() + queryClient.invalidateQueries({ + queryKey: consoleQuery.workspaces.current.tools.workflow.get.queryKey(), + }) onRefreshData() getWorkflowToolProvider() setWorkflowToolDrawerOpen(false) diff --git a/web/app/components/tools/types.ts b/web/app/components/tools/types.ts index 560dc80a5f0..d8d9252cdcb 100644 --- a/web/app/components/tools/types.ts +++ b/web/app/components/tools/types.ts @@ -291,3 +291,15 @@ export const MCPAuthMethod = { } as const export type MCPAuthMethod = (typeof MCPAuthMethod)[keyof typeof MCPAuthMethod] + +export type AppTrigger = { + id: string + trigger_type: 'trigger-webhook' | 'trigger-schedule' | 'trigger-plugin' + title: string + node_id: string + provider_name: string + icon: string + status: 'enabled' | 'disabled' | 'unauthorized' + created_at: string + updated_at: string +} diff --git a/web/app/components/tools/utils/query-select.ts b/web/app/components/tools/utils/query-select.ts new file mode 100644 index 00000000000..2d5fbc85eee --- /dev/null +++ b/web/app/components/tools/utils/query-select.ts @@ -0,0 +1,47 @@ +import type { + AppMcpServerResponse, + WorkflowTriggerListResponse, +} from '@dify/contracts/api/console/apps/types.gen' +import type { RagPipelineOpaqueResponse } from '@dify/contracts/api/console/rag/types.gen' +import type { + ToolApiListResponse, + ToolProviderApiEntityResponse, + ToolProviderListResponse, + WorkflowToolDetailResponse, +} from '@dify/contracts/api/console/workspaces/types.gen' +import type { + AppTrigger, + Collection, + MCPServerDetail, + Tool, + WorkflowToolProviderResponse, +} from '../types' +import type { RAGRecommendedPlugins, ToolWithProvider } from '@/app/components/workflow/types' + +// The tool UI types narrow provider types, statuses, icons and parameter schemas that the +// backend declares as plain strings and loose records. +export const selectToolCollections = (data: ToolProviderListResponse) => + data as unknown as Collection[] + +export const selectToolsWithProvider = (data: ToolProviderListResponse) => + data as unknown as ToolWithProvider[] + +export const selectToolProvider = (data: ToolProviderApiEntityResponse) => + data as unknown as ToolWithProvider + +export const selectToolProviderTools = (data: ToolProviderApiEntityResponse) => + data as unknown as { tools: Tool[] } + +export const selectBuiltinProviderTools = (data: ToolApiListResponse) => data as unknown as Tool[] + +export const selectMCPServerDetail = (data: AppMcpServerResponse) => + data as unknown as MCPServerDetail + +export const selectWorkflowToolDetail = (data: WorkflowToolDetailResponse) => + data as unknown as WorkflowToolProviderResponse + +export const selectAppTriggers = (data: WorkflowTriggerListResponse) => + data as unknown as { data: AppTrigger[] } + +export const selectRAGRecommendedPlugins = (data: RagPipelineOpaqueResponse) => + data as unknown as RAGRecommendedPlugins diff --git a/web/app/components/tools/utils/tool-list-query-key.ts b/web/app/components/tools/utils/tool-list-query-key.ts new file mode 100644 index 00000000000..fa6293a87be --- /dev/null +++ b/web/app/components/tools/utils/tool-list-query-key.ts @@ -0,0 +1,20 @@ +import type { CollectionProviderType } from '../types' +import { consoleQuery } from '@/service/console' +import { CollectionType } from '../types' + +// Each provider type is listed by its own console operation; other provider types have no list. +export const getToolListQueryKey = (type?: CollectionProviderType) => { + const tools = consoleQuery.workspaces.current.tools + switch (type) { + case CollectionType.builtIn: + return tools.builtin.get.queryKey() + case CollectionType.custom: + return tools.api.get.queryKey() + case CollectionType.workflow: + return tools.workflow.get.queryKey() + case CollectionType.mcp: + return tools.mcp.get.queryKey() + default: + return undefined + } +} diff --git a/web/app/components/tools/workflow-tool/hooks/__tests__/use-configure-button.spec.ts b/web/app/components/tools/workflow-tool/hooks/__tests__/use-configure-button.spec.ts index 5949c47dd1d..6f7c21cd497 100644 --- a/web/app/components/tools/workflow-tool/hooks/__tests__/use-configure-button.spec.ts +++ b/web/app/components/tools/workflow-tool/hooks/__tests__/use-configure-button.spec.ts @@ -1,11 +1,15 @@ +import type { ReactNode } from 'react' import type { WorkflowToolOutputVariable, WorkflowToolProviderRequest, WorkflowToolProviderResponse, } from '@/app/components/tools/types' import type { InputVar } from '@/app/components/workflow/types' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { act, renderHook } from '@testing-library/react' +import { createElement } from 'react' import { InputVarType } from '@/app/components/workflow/types' +import { consoleQuery } from '@/service/console' import { isParametersOutdated, useConfigureButton } from '../use-configure-button' const mockCreateWorkflowToolProvider = vi.fn() @@ -15,14 +19,29 @@ vi.mock('@/service/tools', () => ({ saveWorkflowToolProvider: (...args: unknown[]) => mockSaveWorkflowToolProvider(...args), })) -const mockInvalidateAllWorkflowTools = vi.fn() -const mockInvalidateWorkflowToolDetailByAppID = vi.fn() const mockUseWorkflowToolDetailByAppID = vi.fn() -vi.mock('@/service/use-tools', () => ({ - useInvalidateAllWorkflowTools: () => mockInvalidateAllWorkflowTools, - useInvalidateWorkflowToolDetailByAppID: () => mockInvalidateWorkflowToolDetailByAppID, - useWorkflowToolDetailByAppID: (...args: unknown[]) => mockUseWorkflowToolDetailByAppID(...args), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.toolProvider.workflow.get.get': ({ + input, + enabled, + }: { + input: { query: { workflow_app_id: string } } + enabled?: boolean + }) => mockUseWorkflowToolDetailByAppID(input.query.workflow_app_id, enabled), + }) +}) + +let queryClient: QueryClient +const wrapper = ({ children }: { children: ReactNode }) => + createElement(QueryClientProvider, { client: queryClient }, children) +const workflowToolsKey = consoleQuery.workspaces.current.tools.workflow.get.queryKey() +const detailKey = consoleQuery.workspaces.current.toolProvider.workflow.get.get.queryKey({ + input: { query: { workflow_app_id: 'app-123' } }, +}) +const isInvalidated = (queryKey: readonly unknown[]) => + queryClient.getQueryState(queryKey)?.isInvalidated const mockToastNotify = vi.fn() vi.mock('@/app/notifications', () => ({ @@ -203,6 +222,9 @@ describe('isParametersOutdated', () => { describe('useConfigureButton', () => { beforeEach(() => { vi.clearAllMocks() + queryClient = new QueryClient() + for (const key of [workflowToolsKey, detailKey]) + queryClient.setQueryData(key as readonly unknown[], {}) mockUseWorkflowToolDetailByAppID.mockImplementation((_appId: string, enabled: boolean) => ({ data: enabled ? createMockDetail() : undefined, isLoading: false, @@ -215,31 +237,39 @@ describe('useConfigureButton', () => { describe('Initialization', () => { it('should return workflow tool state without owning drawer visibility', () => { - const { result } = renderHook(() => useConfigureButton(createDefaultOptions())) + const { result } = renderHook(() => useConfigureButton(createDefaultOptions()), { + wrapper, + }) expect(result.current.payload).toMatchObject({ workflow_app_id: 'app-123' }) }) it('should forward isLoading from query hook', () => { mockUseWorkflowToolDetailByAppID.mockReturnValue({ data: undefined, isLoading: true }) - const { result } = renderHook(() => - useConfigureButton(createDefaultOptions({ published: true })), + const { result } = renderHook( + () => useConfigureButton(createDefaultOptions({ published: true })), + { wrapper }, ) expect(result.current.isLoading).toBe(true) }) it('should call query hook with enabled=true when published', () => { - renderHook(() => useConfigureButton(createDefaultOptions({ published: true }))) + renderHook(() => useConfigureButton(createDefaultOptions({ published: true })), { + wrapper, + }) expect(mockUseWorkflowToolDetailByAppID).toHaveBeenCalledWith('app-123', true) }) it('should call query hook with enabled=false when not published', () => { - renderHook(() => useConfigureButton(createDefaultOptions({ published: false }))) + renderHook(() => useConfigureButton(createDefaultOptions({ published: false })), { + wrapper, + }) expect(mockUseWorkflowToolDetailByAppID).toHaveBeenCalledWith('app-123', false) }) it('should call query hook with enabled=false when controller is disabled', () => { - renderHook(() => - useConfigureButton(createDefaultOptions({ enabled: false, published: true })), + renderHook( + () => useConfigureButton(createDefaultOptions({ enabled: false, published: true })), + { wrapper }, ) expect(mockUseWorkflowToolDetailByAppID).toHaveBeenCalledWith('app-123', false) }) @@ -248,33 +278,39 @@ describe('useConfigureButton', () => { // Computed values describe('Computed - outdated', () => { it('should be false when not published (no detail)', () => { - const { result } = renderHook(() => useConfigureButton(createDefaultOptions())) + const { result } = renderHook(() => useConfigureButton(createDefaultOptions()), { + wrapper, + }) expect(result.current.outdated).toBe(false) }) it('should be true when parameters differ', () => { - const { result } = renderHook(() => - useConfigureButton( - createDefaultOptions({ - published: true, - inputs: [ - createMockInputVar({ variable: 'test_var' }), - createMockInputVar({ variable: 'extra_var' }), - ], - }), - ), + const { result } = renderHook( + () => + useConfigureButton( + createDefaultOptions({ + published: true, + inputs: [ + createMockInputVar({ variable: 'test_var' }), + createMockInputVar({ variable: 'extra_var' }), + ], + }), + ), + { wrapper }, ) expect(result.current.outdated).toBe(true) }) it('should be false when parameters match', () => { - const { result } = renderHook(() => - useConfigureButton( - createDefaultOptions({ - published: true, - inputs: [createMockInputVar({ variable: 'test_var', required: true })], - }), - ), + const { result } = renderHook( + () => + useConfigureButton( + createDefaultOptions({ + published: true, + inputs: [createMockInputVar({ variable: 'test_var', required: true })], + }), + ), + { wrapper }, ) expect(result.current.outdated).toBe(false) }) @@ -282,7 +318,9 @@ describe('useConfigureButton', () => { describe('Computed - payload', () => { it('should use prop values when not published', () => { - const { result } = renderHook(() => useConfigureButton(createDefaultOptions())) + const { result } = renderHook(() => useConfigureButton(createDefaultOptions()), { + wrapper, + }) expect(result.current.payload).toMatchObject({ icon: createMockEmoji(), @@ -304,8 +342,9 @@ describe('useConfigureButton', () => { }) it('should use detail values when published with detail', () => { - const { result } = renderHook(() => - useConfigureButton(createDefaultOptions({ published: true })), + const { result } = renderHook( + () => useConfigureButton(createDefaultOptions({ published: true })), + { wrapper }, ) expect(result.current.payload).toMatchObject({ @@ -326,8 +365,9 @@ describe('useConfigureButton', () => { it('should return empty parameters when published without detail', () => { mockUseWorkflowToolDetailByAppID.mockReturnValue({ data: undefined, isLoading: false }) - const { result } = renderHook(() => - useConfigureButton(createDefaultOptions({ published: true })), + const { result } = renderHook( + () => useConfigureButton(createDefaultOptions({ published: true })), + { wrapper }, ) expect(result.current.payload.parameters).toHaveLength(0) @@ -335,8 +375,9 @@ describe('useConfigureButton', () => { }) it('should build output parameters from detail output_schema', () => { - const { result } = renderHook(() => - useConfigureButton(createDefaultOptions({ published: true })), + const { result } = renderHook( + () => useConfigureButton(createDefaultOptions({ published: true })), + { wrapper }, ) expect(result.current.payload.outputParameters).toHaveLength(1) @@ -352,8 +393,9 @@ describe('useConfigureButton', () => { detail.tool.output_schema = undefined mockUseWorkflowToolDetailByAppID.mockReturnValue({ data: detail, isLoading: false }) - const { result } = renderHook(() => - useConfigureButton(createDefaultOptions({ published: true })), + const { result } = renderHook( + () => useConfigureButton(createDefaultOptions({ published: true })), + { wrapper }, ) expect(result.current.payload.outputParameters[0]).toMatchObject({ @@ -363,13 +405,15 @@ describe('useConfigureButton', () => { }) it('should convert paragraph type to string in existing parameters', () => { - const { result } = renderHook(() => - useConfigureButton( - createDefaultOptions({ - published: true, - inputs: [createMockInputVar({ variable: 'test_var', type: InputVarType.paragraph })], - }), - ), + const { result } = renderHook( + () => + useConfigureButton( + createDefaultOptions({ + published: true, + inputs: [createMockInputVar({ variable: 'test_var', type: InputVarType.paragraph })], + }), + ), + { wrapper }, ) expect(result.current.payload.parameters[0]!.type).toBe('string') @@ -381,8 +425,9 @@ describe('useConfigureButton', () => { it('should publish before creating the provider', async () => { mockCreateWorkflowToolProvider.mockResolvedValue({}) const handlePublish = vi.fn().mockResolvedValue(undefined) - const { result } = renderHook(() => - useConfigureButton(createDefaultOptions({ handlePublish })), + const { result } = renderHook( + () => useConfigureButton(createDefaultOptions({ handlePublish })), + { wrapper }, ) await act(async () => { @@ -402,8 +447,9 @@ describe('useConfigureButton', () => { it('should not create the provider when publishing fails', async () => { const handlePublish = vi.fn().mockRejectedValue(new Error('Publish failed')) - const { result } = renderHook(() => - useConfigureButton(createDefaultOptions({ handlePublish })), + const { result } = renderHook( + () => useConfigureButton(createDefaultOptions({ handlePublish })), + { wrapper }, ) await act(async () => { @@ -422,8 +468,9 @@ describe('useConfigureButton', () => { mockCreateWorkflowToolProvider.mockResolvedValue({}) const onRefreshData = vi.fn() const onConfigured = vi.fn() - const { result } = renderHook(() => - useConfigureButton(createDefaultOptions({ onRefreshData, onConfigured })), + const { result } = renderHook( + () => useConfigureButton(createDefaultOptions({ onRefreshData, onConfigured })), + { wrapper }, ) await act(async () => { @@ -435,16 +482,18 @@ describe('useConfigureButton', () => { }) expect(mockCreateWorkflowToolProvider).toHaveBeenCalled() - expect(mockInvalidateAllWorkflowTools).toHaveBeenCalled() + expect(isInvalidated(workflowToolsKey)).toBe(true) expect(onRefreshData).toHaveBeenCalled() - expect(mockInvalidateWorkflowToolDetailByAppID).toHaveBeenCalledWith('app-123') + expect(isInvalidated(detailKey)).toBe(true) expect(mockToastNotify).toHaveBeenCalledWith({ type: 'success', message: expect.any(String) }) expect(onConfigured).toHaveBeenCalled() }) it('should show error toast on failure', async () => { mockCreateWorkflowToolProvider.mockRejectedValue(new Error('Create failed')) - const { result } = renderHook(() => useConfigureButton(createDefaultOptions())) + const { result } = renderHook(() => useConfigureButton(createDefaultOptions()), { + wrapper, + }) await act(async () => { await result.current.handleCreate( @@ -464,15 +513,17 @@ describe('useConfigureButton', () => { const handlePublish = vi.fn().mockResolvedValue(undefined) const onRefreshData = vi.fn() const onConfigured = vi.fn() - const { result } = renderHook(() => - useConfigureButton( - createDefaultOptions({ - published: true, - handlePublish, - onRefreshData, - onConfigured, - }), - ), + const { result } = renderHook( + () => + useConfigureButton( + createDefaultOptions({ + published: true, + handlePublish, + onRefreshData, + onConfigured, + }), + ), + { wrapper }, ) await act(async () => { @@ -485,21 +536,23 @@ describe('useConfigureButton', () => { expect(handlePublish).toHaveBeenCalled() expect(mockSaveWorkflowToolProvider).toHaveBeenCalled() expect(onRefreshData).toHaveBeenCalled() - expect(mockInvalidateAllWorkflowTools).toHaveBeenCalled() - expect(mockInvalidateWorkflowToolDetailByAppID).toHaveBeenCalledWith('app-123') + expect(isInvalidated(workflowToolsKey)).toBe(true) + expect(isInvalidated(detailKey)).toBe(true) expect(mockToastNotify).toHaveBeenCalledWith({ type: 'success', message: expect.any(String) }) expect(onConfigured).toHaveBeenCalled() }) it('should show error toast when publish fails', async () => { const handlePublish = vi.fn().mockRejectedValue(new Error('Publish failed')) - const { result } = renderHook(() => - useConfigureButton( - createDefaultOptions({ - published: true, - handlePublish, - }), - ), + const { result } = renderHook( + () => + useConfigureButton( + createDefaultOptions({ + published: true, + handlePublish, + }), + ), + { wrapper }, ) await act(async () => { @@ -514,8 +567,9 @@ describe('useConfigureButton', () => { it('should show error toast when save fails', async () => { mockSaveWorkflowToolProvider.mockRejectedValue(new Error('Save failed')) - const { result } = renderHook(() => - useConfigureButton(createDefaultOptions({ published: true })), + const { result } = renderHook( + () => useConfigureButton(createDefaultOptions({ published: true })), + { wrapper }, ) await act(async () => { @@ -535,38 +589,40 @@ describe('useConfigureButton', () => { const options = createDefaultOptions({ published: true, detailNeedUpdate: false }) const { rerender } = renderHook( (props: ReturnType) => useConfigureButton(props), - { initialProps: options }, + { initialProps: options, wrapper }, ) rerender({ ...options, detailNeedUpdate: true }) - expect(mockInvalidateWorkflowToolDetailByAppID).toHaveBeenCalledWith('app-123') + expect(isInvalidated(detailKey)).toBe(true) }) it('should not invalidate when detailNeedUpdate stays false', () => { const options = createDefaultOptions({ published: true, detailNeedUpdate: false }) const { rerender } = renderHook( (props: ReturnType) => useConfigureButton(props), - { initialProps: options }, + { initialProps: options, wrapper }, ) rerender({ ...options }) - expect(mockInvalidateWorkflowToolDetailByAppID).not.toHaveBeenCalled() + expect(isInvalidated(detailKey)).toBe(false) }) it('should not invalidate detail while disabled', () => { - renderHook(() => - useConfigureButton( - createDefaultOptions({ - enabled: false, - published: true, - detailNeedUpdate: true, - }), - ), + renderHook( + () => + useConfigureButton( + createDefaultOptions({ + enabled: false, + published: true, + detailNeedUpdate: true, + }), + ), + { wrapper }, ) - expect(mockInvalidateWorkflowToolDetailByAppID).not.toHaveBeenCalled() + expect(isInvalidated(detailKey)).toBe(false) }) }) @@ -574,8 +630,9 @@ describe('useConfigureButton', () => { describe('Edge Cases', () => { it('should handle undefined detail from query gracefully', () => { mockUseWorkflowToolDetailByAppID.mockReturnValue({ data: undefined, isLoading: false }) - const { result } = renderHook(() => - useConfigureButton(createDefaultOptions({ published: true })), + const { result } = renderHook( + () => useConfigureButton(createDefaultOptions({ published: true })), + { wrapper }, ) expect(result.current.outdated).toBe(false) @@ -587,26 +644,30 @@ describe('useConfigureButton', () => { detail.tool.parameters = [] mockUseWorkflowToolDetailByAppID.mockReturnValue({ data: detail, isLoading: false }) - const { result } = renderHook(() => - useConfigureButton( - createDefaultOptions({ - published: true, - inputs: [], - }), - ), + const { result } = renderHook( + () => + useConfigureButton( + createDefaultOptions({ + published: true, + inputs: [], + }), + ), + { wrapper }, ) expect(result.current.outdated).toBe(false) }) it('should handle undefined inputs and outputs', () => { - const { result } = renderHook(() => - useConfigureButton( - createDefaultOptions({ - inputs: undefined, - outputs: undefined, - }), - ), + const { result } = renderHook( + () => + useConfigureButton( + createDefaultOptions({ + inputs: undefined, + outputs: undefined, + }), + ), + { wrapper }, ) expect(result.current.payload.parameters).toHaveLength(0) @@ -615,12 +676,14 @@ describe('useConfigureButton', () => { it('should handle missing onRefreshData callback in create', async () => { mockCreateWorkflowToolProvider.mockResolvedValue({}) - const { result } = renderHook(() => - useConfigureButton( - createDefaultOptions({ - onRefreshData: undefined, - }), - ), + const { result } = renderHook( + () => + useConfigureButton( + createDefaultOptions({ + onRefreshData: undefined, + }), + ), + { wrapper }, ) // Should not throw diff --git a/web/app/components/tools/workflow-tool/hooks/use-configure-button.ts b/web/app/components/tools/workflow-tool/hooks/use-configure-button.ts index 44e9f6f96e9..3cbbda0154b 100644 --- a/web/app/components/tools/workflow-tool/hooks/use-configure-button.ts +++ b/web/app/components/tools/workflow-tool/hooks/use-configure-button.ts @@ -8,15 +8,13 @@ import type { } from '@/app/components/tools/types' import type { InputVar } from '@/app/components/workflow/types' import type { PublishWorkflowParams } from '@/types/workflow' -import { useEffect, useMemo, useRef } from 'react' +import { useQuery, useQueryClient } from '@tanstack/react-query' +import { useEffect, useMemo } from 'react' import { useTranslation } from 'react-i18next' +import { selectWorkflowToolDetail } from '@/app/components/tools/utils/query-select' import { toast } from '@/app/notifications' +import { consoleQuery } from '@/service/console' import { createWorkflowToolProvider, saveWorkflowToolProvider } from '@/service/tools' -import { - useInvalidateAllWorkflowTools, - useInvalidateWorkflowToolDetailByAppID, - useWorkflowToolDetailByAppID, -} from '@/service/use-tools' // region Pure helpers @@ -129,24 +127,33 @@ export function useConfigureButton(options: UseConfigureButtonOptions) { const { t } = useTranslation(['common']) // Data fetching via React Query - const { data: detail, isLoading } = useWorkflowToolDetailByAppID( - workflowAppId, - enabled && published, + const queryClient = useQueryClient() + const detailQueryKey = consoleQuery.workspaces.current.toolProvider.workflow.get.get.queryKey({ + input: { query: { workflow_app_id: workflowAppId } }, + }) + const { data: detail, isLoading } = useQuery( + consoleQuery.workspaces.current.toolProvider.workflow.get.get.queryOptions({ + input: { query: { workflow_app_id: workflowAppId } }, + enabled: enabled && published && !!workflowAppId, + select: selectWorkflowToolDetail, + }), ) - // Invalidation functions (store in ref for stable effect dependency) - const invalidateDetail = useInvalidateWorkflowToolDetailByAppID() - const invalidateAllWorkflowTools = useInvalidateAllWorkflowTools() - - const invalidateDetailRef = useRef(invalidateDetail) - useEffect(() => { - invalidateDetailRef.current = invalidateDetail - }, [invalidateDetail]) + const invalidateDetail = () => queryClient.invalidateQueries({ queryKey: detailQueryKey }) + const invalidateAllWorkflowTools = () => + queryClient.invalidateQueries({ + queryKey: consoleQuery.workspaces.current.tools.workflow.get.queryKey(), + }) // Refetch when detailNeedUpdate becomes true useEffect(() => { - if (enabled && detailNeedUpdate) invalidateDetailRef.current(workflowAppId) - }, [detailNeedUpdate, enabled, workflowAppId]) + if (enabled && detailNeedUpdate) + queryClient.invalidateQueries({ + queryKey: consoleQuery.workspaces.current.toolProvider.workflow.get.get.queryKey({ + input: { query: { workflow_app_id: workflowAppId } }, + }), + }) + }, [detailNeedUpdate, enabled, queryClient, workflowAppId]) // Computed values const outdated = useMemo(() => isParametersOutdated(detail, inputs), [detail, inputs]) @@ -188,7 +195,7 @@ export function useConfigureButton(options: UseConfigureButtonOptions) { await createWorkflowToolProvider(data) invalidateAllWorkflowTools() onRefreshData?.() - invalidateDetail(workflowAppId) + invalidateDetail() toast.success(t(($) => $['api.actionSuccess'], { ns: 'common' })) onConfigured?.() } catch (e) { @@ -208,7 +215,7 @@ export function useConfigureButton(options: UseConfigureButtonOptions) { await saveWorkflowToolProvider(data) onRefreshData?.() invalidateAllWorkflowTools() - invalidateDetail(workflowAppId) + invalidateDetail() toast.success(t(($) => $['api.actionSuccess'], { ns: 'common' })) onConfigured?.() } catch (e) { diff --git a/web/app/components/workflow-app/__tests__/index.spec.tsx b/web/app/components/workflow-app/__tests__/index.spec.tsx index 7aa1cf62a96..cd29fb7ebf3 100644 --- a/web/app/components/workflow-app/__tests__/index.spec.tsx +++ b/web/app/components/workflow-app/__tests__/index.spec.tsx @@ -126,9 +126,12 @@ vi.mock('@/service/log', () => ({ fetchRunDetail: (...args: unknown[]) => mockFetchRunDetail(...args), })) -vi.mock('@/service/use-tools', () => ({ - useAppTriggers: () => appTriggersState, -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'apps.byAppId.triggers.get': () => appTriggersState, + }) +}) vi.mock('../hooks/use-workflow-init', () => ({ useWorkflowInit: () => workflowInitState, diff --git a/web/app/components/workflow-app/__tests__/provider-contract.spec.tsx b/web/app/components/workflow-app/__tests__/provider-contract.spec.tsx index 41b4e233c08..b6184690e67 100644 --- a/web/app/components/workflow-app/__tests__/provider-contract.spec.tsx +++ b/web/app/components/workflow-app/__tests__/provider-contract.spec.tsx @@ -46,12 +46,11 @@ vi.mock('@/next/navigation', async (importOriginal) => ({ useSearchParams: () => ({ get: () => null }), })) -vi.mock('@/service/use-tools', async (importOriginal) => { - const actual = await importOriginal() - return { - ...actual, - useAppTriggers: () => ({}), - } +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'apps.byAppId.triggers.get': () => ({}), + }) }) vi.mock('../hooks/use-get-run-and-trace-url', () => ({ diff --git a/web/app/components/workflow-app/components/workflow-header/__tests__/features-trigger.spec.tsx b/web/app/components/workflow-app/components/workflow-header/__tests__/features-trigger.spec.tsx index 0879fed0d1e..2ab27e24747 100644 --- a/web/app/components/workflow-app/components/workflow-header/__tests__/features-trigger.spec.tsx +++ b/web/app/components/workflow-app/components/workflow-header/__tests__/features-trigger.spec.tsx @@ -50,7 +50,6 @@ const mockHandleSyncWorkflowDraft = vi.fn() const mockPublishWorkflow = vi.fn() const mockUpdatePublishedWorkflow = vi.fn() const mockResetWorkflowVersionHistory = vi.fn() -const mockInvalidateAppTriggers = vi.fn() const mockInvalidateQueries = vi.fn() const mockSetPublishedAt = vi.fn() const mockSetLastPublishedHasUserInput = vi.fn() @@ -222,10 +221,6 @@ vi.mock('@/service/use-workflow', () => ({ useResetWorkflowVersionHistory: () => mockResetWorkflowVersionHistory, })) -vi.mock('@/service/use-tools', () => ({ - useInvalidateAppTriggers: () => mockInvalidateAppTriggers, -})) - vi.mock('@/next/navigation', async (importOriginal) => ({ ...(await importOriginal()), useParams: () => ({ appId: 'app-id' }), @@ -618,7 +613,11 @@ describe('FeaturesTrigger', () => { releaseNotes: '', }) expect(mockUpdatePublishedWorkflow).toHaveBeenCalledWith('app-id') - expect(mockInvalidateAppTriggers).toHaveBeenCalledWith('app-id') + expect(mockInvalidateQueries).toHaveBeenCalledWith({ + queryKey: consoleQuery.apps.byAppId.triggers.get.queryKey({ + input: { params: { app_id: 'app-id' } }, + }), + }) expect(mockSetPublishedAt).toHaveBeenCalledWith('2024-01-01T00:00:00Z') expect(mockSetLastPublishedHasUserInput).toHaveBeenCalledWith(true) expect(mockResetWorkflowVersionHistory).toHaveBeenCalled() @@ -785,7 +784,11 @@ describe('FeaturesTrigger', () => { message: 'common.api.actionSuccess', }) expect(mockUpdatePublishedWorkflow).not.toHaveBeenCalled() - expect(mockInvalidateAppTriggers).not.toHaveBeenCalled() + expect(mockInvalidateQueries).not.toHaveBeenCalledWith({ + queryKey: consoleQuery.apps.byAppId.triggers.get.queryKey({ + input: { params: { app_id: 'app-id' } }, + }), + }) expect(mockSetPublishedAt).not.toHaveBeenCalled() expect(mockSetLastPublishedHasUserInput).not.toHaveBeenCalled() expect(mockResetWorkflowVersionHistory).not.toHaveBeenCalled() diff --git a/web/app/components/workflow-app/components/workflow-header/features-trigger.tsx b/web/app/components/workflow-app/components/workflow-header/features-trigger.tsx index 5c0c0b9c605..903a625c6b5 100644 --- a/web/app/components/workflow-app/components/workflow-header/features-trigger.tsx +++ b/web/app/components/workflow-app/components/workflow-header/features-trigger.tsx @@ -31,7 +31,6 @@ import { systemFeaturesQueryOptions } from '@/features/system-features/client' import useTheme from '@/hooks/use-theme' import { useParams } from '@/next/navigation' import { consoleQuery } from '@/service/console' -import { useInvalidateAppTriggers } from '@/service/use-tools' import { useInvalidateAppWorkflow, usePublishWorkflow, @@ -149,7 +148,6 @@ const FeaturesTrigger = () => { }, [nodes]) const resetWorkflowVersionHistory = useResetWorkflowVersionHistory() - const invalidateAppTriggers = useInvalidateAppTriggers() const handleShowFeatures = useCallback(() => { const { showFeaturesPanel, isRestoring, setShowFeaturesPanel } = workflowStore.getState() @@ -195,7 +193,11 @@ const FeaturesTrigger = () => { input: { params: { app_id: appID } }, }), }) - invalidateAppTriggers(appID) + void queryClient.invalidateQueries({ + queryKey: consoleQuery.apps.byAppId.triggers.get.queryKey({ + input: { params: { app_id: appID } }, + }), + }) if (rosterAgentIds.length > 0) { void queryClient.invalidateQueries({ queryKey: consoleQuery.agent.get.key(), @@ -230,7 +232,6 @@ const FeaturesTrigger = () => { appID, t, updatePublishedWorkflow, - invalidateAppTriggers, rosterAgentIds, queryClient, workflowStore, diff --git a/web/app/components/workflow-app/index.tsx b/web/app/components/workflow-app/index.tsx index 7a47e2c20e5..7b265571f33 100644 --- a/web/app/components/workflow-app/index.tsx +++ b/web/app/components/workflow-app/index.tsx @@ -2,11 +2,12 @@ import type { Features as FeaturesData } from '@/app/components/base/features/types' import type { InjectWorkflowStoreSliceFn } from '@/app/components/workflow/store' -import { useSuspenseQuery } from '@tanstack/react-query' +import { useQuery, useSuspenseQuery } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import { useEffect, useMemo } from 'react' import { FeaturesProvider } from '@/app/components/base/features' import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder' +import { selectAppTriggers } from '@/app/components/tools/utils/query-select' import WorkflowWithDefaultContext from '@/app/components/workflow' import { WorkflowContextProvider } from '@/app/components/workflow/context' import { useWorkflowStore } from '@/app/components/workflow/store' @@ -18,7 +19,6 @@ import { userProfileQueryOptions } from '@/features/account-profile/client' import { useParams, useSearchParams } from '@/next/navigation' import { consoleQuery } from '@/service/console' import { fetchRunDetail } from '@/service/log' -import { useAppTriggers } from '@/service/use-tools' import { AppModeEnum } from '@/types/app' import { getAppACLCapabilities } from '@/utils/permission' import WorkflowAppMain from './components/workflow-main' @@ -54,10 +54,15 @@ const WorkflowAppWithAdditionalContext = () => { [appDetail.maintainer, appDetail.permission_keys, currentUserId, workspacePermissionKeys], ) const isWorkflowMode = appDetail.mode === AppModeEnum.WORKFLOW - const { data: triggersResponse } = useAppTriggers(isWorkflowMode ? appId : undefined, { - staleTime: 5 * 60 * 1000, // 5 minutes cache - refetchOnWindowFocus: false, - }) + const { data: triggersResponse } = useQuery( + consoleQuery.apps.byAppId.triggers.get.queryOptions({ + input: { params: { app_id: appId } }, + enabled: isWorkflowMode && !!appId, + select: selectAppTriggers, + staleTime: 5 * 60 * 1000, // 5 minutes cache + refetchOnWindowFocus: false, + }), + ) // Sync trigger statuses to store when data loads useEffect(() => { diff --git a/web/app/components/workflow/__tests__/service-mock-factory.ts b/web/app/components/workflow/__tests__/service-mock-factory.ts index 2705a6c5daa..820c3275bb7 100644 --- a/web/app/components/workflow/__tests__/service-mock-factory.ts +++ b/web/app/components/workflow/__tests__/service-mock-factory.ts @@ -3,8 +3,11 @@ * * Usage: * ```ts - * vi.mock('@/service/use-tools', async () => - * (await import('../../__tests__/service-mock-factory')).createToolServiceMock(), + * vi.mock('@tanstack/react-query', async (importOriginal) => + * (await import('@/test/console/operation-hooks')).mockConsoleOperationHooks( + * await importOriginal(), + * (await import('../../__tests__/service-mock-factory')).createToolOperationRoutes(), + * ), * ) * ``` */ @@ -24,12 +27,12 @@ type TriggerMockData = { triggerPlugins?: unknown[] } -export function createToolServiceMock(data?: ToolMockData) { +export function createToolOperationRoutes(data?: ToolMockData) { return { - useAllBuiltInTools: vi.fn(() => ({ data: data?.buildInTools ?? [] })), - useAllCustomTools: vi.fn(() => ({ data: data?.customTools ?? [] })), - useAllWorkflowTools: vi.fn(() => ({ data: data?.workflowTools ?? [] })), - useAllMCPTools: vi.fn(() => ({ data: data?.mcpTools ?? [] })), + 'workspaces.current.tools.builtin.get': () => ({ data: data?.buildInTools ?? [] }), + 'workspaces.current.tools.api.get': () => ({ data: data?.customTools ?? [] }), + 'workspaces.current.tools.workflow.get': () => ({ data: data?.workflowTools ?? [] }), + 'workspaces.current.tools.mcp.get': () => ({ data: data?.mcpTools ?? [] }), } } diff --git a/web/app/components/workflow/__tests__/workflow-edge-events.spec.tsx b/web/app/components/workflow/__tests__/workflow-edge-events.spec.tsx index 706e6378461..ce5689b2d59 100644 --- a/web/app/components/workflow/__tests__/workflow-edge-events.spec.tsx +++ b/web/app/components/workflow/__tests__/workflow-edge-events.spec.tsx @@ -182,12 +182,15 @@ vi.mock('@/context/event-emitter', () => ({ }), })) -vi.mock('@/service/use-tools', () => ({ - useAllBuiltInTools: () => ({ data: [] }), - useAllCustomTools: () => ({ data: [] }), - useAllMCPTools: () => ({ data: [] }), - useAllWorkflowTools: () => ({ data: [] }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.tools.builtin.get': () => ({ data: [] }), + 'workspaces.current.tools.api.get': () => ({ data: [] }), + 'workspaces.current.tools.mcp.get': () => ({ data: [] }), + 'workspaces.current.tools.workflow.get': () => ({ data: [] }), + }) +}) vi.mock('@/service/workflow', () => ({ fetchAllInspectVars: vi.fn().mockResolvedValue([]), diff --git a/web/app/components/workflow/block-selector/__tests__/index.spec.tsx b/web/app/components/workflow/block-selector/__tests__/index.spec.tsx index 65673beaebf..5dfa32b1379 100644 --- a/web/app/components/workflow/block-selector/__tests__/index.spec.tsx +++ b/web/app/components/workflow/block-selector/__tests__/index.spec.tsx @@ -38,13 +38,15 @@ vi.mock('@/service/use-triggers', () => ({ useInvalidateAllTriggerPlugins: () => vi.fn(), })) -vi.mock('@/service/use-tools', () => ({ - useAllBuiltInTools: () => ({ data: [] }), - useAllCustomTools: () => ({ data: [] }), - useAllWorkflowTools: () => ({ data: [] }), - useAllMCPTools: () => ({ data: [] }), - useInvalidateAllBuiltInTools: () => vi.fn(), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.tools.builtin.get': () => ({ data: [] }), + 'workspaces.current.tools.api.get': () => ({ data: [] }), + 'workspaces.current.tools.workflow.get': () => ({ data: [] }), + 'workspaces.current.tools.mcp.get': () => ({ data: [] }), + }) +}) const createBlock = (type: BlockEnum, title: string): NodeDefault => ({ metaData: { diff --git a/web/app/components/workflow/block-selector/__tests__/tabs.spec.tsx b/web/app/components/workflow/block-selector/__tests__/tabs.spec.tsx index 2ac5ef970d3..76c1c074890 100644 --- a/web/app/components/workflow/block-selector/__tests__/tabs.spec.tsx +++ b/web/app/components/workflow/block-selector/__tests__/tabs.spec.tsx @@ -1,6 +1,7 @@ import { screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import * as React from 'react' +import { consoleQuery } from '@/service/console' import { renderWithConsoleQuery } from '@/test/console/query-data' import { BlockSelectorPanels } from '../tabs' import { TabType } from '../types' @@ -8,8 +9,7 @@ import { TabType } from '../types' const render = (ui: React.ReactElement) => renderWithConsoleQuery(ui, { systemFeatures: { enable_marketplace: true } }) -const { mockInvalidateBuiltInTools, mockToolsState } = vi.hoisted(() => ({ - mockInvalidateBuiltInTools: vi.fn(), +const { mockToolsState } = vi.hoisted(() => ({ mockToolsState: { buildInTools: [{ icon: '/tool.svg', name: 'tool' }] as | Array<{ icon: string | Record; name: string }> @@ -27,13 +27,15 @@ vi.mock('@/service/use-plugins', () => ({ }), })) -vi.mock('@/service/use-tools', () => ({ - useAllBuiltInTools: () => ({ data: mockToolsState.buildInTools }), - useAllCustomTools: () => ({ data: mockToolsState.customTools }), - useAllWorkflowTools: () => ({ data: mockToolsState.workflowTools }), - useAllMCPTools: () => ({ data: mockToolsState.mcpTools }), - useInvalidateAllBuiltInTools: () => mockInvalidateBuiltInTools, -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.tools.builtin.get': () => ({ data: mockToolsState.buildInTools }), + 'workspaces.current.tools.api.get': () => ({ data: mockToolsState.customTools }), + 'workspaces.current.tools.workflow.get': () => ({ data: mockToolsState.workflowTools }), + 'workspaces.current.tools.mcp.get': () => ({ data: mockToolsState.mcpTools }), + }) +}) vi.mock('@/utils/var', () => ({ basePath: '/console', @@ -219,12 +221,16 @@ describe('Tabs', () => { it('should force start content to render and invalidate built-in tools after featured installs', async () => { const user = userEvent.setup() - render() + const { queryClient } = render( + , + ) + const builtInToolsKey = consoleQuery.workspaces.current.tools.builtin.get.queryKey() + queryClient.setQueryData(builtInToolsKey, []) await user.click(screen.getByRole('button', { name: 'Install featured tool' })) expect(screen.getByText('tools-content'))!.toBeInTheDocument() - expect(mockInvalidateBuiltInTools).toHaveBeenCalledTimes(1) + expect(queryClient.getQueryState(builtInToolsKey)?.isInvalidated).toBe(true) }) it('should compose start content directly without tab semantics in standalone mode', () => { diff --git a/web/app/components/workflow/block-selector/__tests__/tool-picker.spec.tsx b/web/app/components/workflow/block-selector/__tests__/tool-picker.spec.tsx index abf3e22d17f..c500e12e24c 100644 --- a/web/app/components/workflow/block-selector/__tests__/tool-picker.spec.tsx +++ b/web/app/components/workflow/block-selector/__tests__/tool-picker.spec.tsx @@ -13,27 +13,20 @@ import { PluginCategoryEnum } from '@/app/components/plugins/types' import { CollectionType } from '@/app/components/tools/types' import { useGetLanguage } from '@/context/i18n' import useTheme from '@/hooks/use-theme' +import { consoleQuery } from '@/service/console' import { createCustomCollection } from '@/service/tools' import { useFeaturedToolsRecommendations } from '@/service/use-plugins' -import { - useAllBuiltInTools, - useAllCustomTools, - useAllMCPTools, - useAllWorkflowTools, - useInvalidateAllBuiltInTools, - useInvalidateAllCustomTools, - useInvalidateAllMCPTools, - useInvalidateAllWorkflowTools, -} from '@/service/use-tools' import { renderWithConsoleQuery } from '@/test/console/query-data' import { Theme } from '@/types/app' import ToolPicker, { ToolPickerContent } from '../tool-picker' const mockNotify = vi.fn() -const mockInvalidateBuiltInTools = vi.fn() -const mockInvalidateCustomTools = vi.fn() -const mockInvalidateWorkflowTools = vi.fn() -const mockInvalidateMcpTools = vi.fn() +const toolListKeys = { + builtin: consoleQuery.workspaces.current.tools.builtin.get.queryKey(), + custom: consoleQuery.workspaces.current.tools.api.get.queryKey(), + workflow: consoleQuery.workspaces.current.tools.workflow.get.queryKey(), + mcp: consoleQuery.workspaces.current.tools.mcp.get.queryKey(), +} const mockCreateCustomCollection = vi.mocked(createCustomCollection) const mockInstallPackageFromMarketPlace = vi.fn() const mockCheckInstalled = vi.fn() @@ -44,14 +37,13 @@ const mockUseGetLanguage = vi.mocked(useGetLanguage) const mockUseTheme = vi.mocked(useTheme) const mockUseTags = vi.mocked(useTags) const mockUseMarketplacePlugins = vi.mocked(useMarketplacePlugins) -const mockUseAllBuiltInTools = vi.mocked(useAllBuiltInTools) -const mockUseAllCustomTools = vi.mocked(useAllCustomTools) -const mockUseAllWorkflowTools = vi.mocked(useAllWorkflowTools) -const mockUseAllMCPTools = vi.mocked(useAllMCPTools) -const mockUseInvalidateAllBuiltInTools = vi.mocked(useInvalidateAllBuiltInTools) -const mockUseInvalidateAllCustomTools = vi.mocked(useInvalidateAllCustomTools) -const mockUseInvalidateAllWorkflowTools = vi.mocked(useInvalidateAllWorkflowTools) -const mockUseInvalidateAllMCPTools = vi.mocked(useInvalidateAllMCPTools) +const { mockBuiltInToolsQuery, mockCustomToolsQuery, mockWorkflowToolsQuery, mockMCPToolsQuery } = + vi.hoisted(() => ({ + mockBuiltInToolsQuery: vi.fn(), + mockCustomToolsQuery: vi.fn(), + mockWorkflowToolsQuery: vi.fn(), + mockMCPToolsQuery: vi.fn(), + })) const mockUseFeaturedToolsRecommendations = vi.mocked(useFeaturedToolsRecommendations) vi.mock('@/context/i18n', () => ({ @@ -121,16 +113,17 @@ vi.mock('@/service/use-plugins', () => ({ }), })) -vi.mock('@/service/use-tools', () => ({ - useAllBuiltInTools: vi.fn(), - useAllCustomTools: vi.fn(), - useAllWorkflowTools: vi.fn(), - useAllMCPTools: vi.fn(), - useInvalidateAllBuiltInTools: vi.fn(), - useInvalidateAllCustomTools: vi.fn(), - useInvalidateAllWorkflowTools: vi.fn(), - useInvalidateAllMCPTools: vi.fn(), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.tools.builtin.get': () => mockBuiltInToolsQuery(), + 'workspaces.current.tools.api.get': ({ enabled }: { enabled?: boolean }) => + mockCustomToolsQuery(enabled), + 'workspaces.current.tools.workflow.get': ({ enabled }: { enabled?: boolean }) => + mockWorkflowToolsQuery(enabled), + 'workspaces.current.tools.mcp.get': () => mockMCPToolsQuery(), + }) +}) vi.mock('@/app/notifications', () => ({ toast: { @@ -363,26 +356,14 @@ describe('ToolPicker', () => { mockUseMarketplacePlugins.mockReturnValue({ data: undefined, } as ReturnType) - mockUseAllBuiltInTools.mockReturnValue({ data: builtInTools } as ReturnType< - typeof useAllBuiltInTools - >) - mockUseAllCustomTools.mockImplementation( - (enabled = true) => - ({ - data: enabled ? customTools : [], - }) as ReturnType, - ) - mockUseAllWorkflowTools.mockImplementation( - (enabled = true) => - ({ - data: enabled ? workflowTools : [], - }) as ReturnType, - ) - mockUseAllMCPTools.mockReturnValue({ data: mcpTools } as ReturnType) - mockUseInvalidateAllBuiltInTools.mockReturnValue(mockInvalidateBuiltInTools) - mockUseInvalidateAllCustomTools.mockReturnValue(mockInvalidateCustomTools) - mockUseInvalidateAllWorkflowTools.mockReturnValue(mockInvalidateWorkflowTools) - mockUseInvalidateAllMCPTools.mockReturnValue(mockInvalidateMcpTools) + mockBuiltInToolsQuery.mockReturnValue({ data: builtInTools }) + mockCustomToolsQuery.mockImplementation((enabled = true) => ({ + data: enabled ? customTools : [], + })) + mockWorkflowToolsQuery.mockImplementation((enabled = true) => ({ + data: enabled ? workflowTools : [], + })) + mockMCPToolsQuery.mockReturnValue({ data: mcpTools }) mockUseFeaturedToolsRecommendations.mockReturnValue({ plugins: [], isLoading: false, @@ -587,9 +568,9 @@ describe('ToolPicker', () => { plugin_id: 'available-plugin', label: { en_US: 'Available Plugin' }, }) - mockUseAllBuiltInTools.mockReturnValue({ + mockBuiltInToolsQuery.mockReturnValue({ data: [createToolProvider({ ...builtInTools[0], plugin_id: installedPlugin.plugin_id })], - } as ReturnType) + }) mockUseMarketplacePlugins.mockImplementation( (params) => ({ @@ -670,12 +651,12 @@ describe('ToolPicker', () => { expect(await screen.findByText('New Plugin')).toBeInTheDocument() expect(screen.getByText('Available Plugin')).toBeInTheDocument() - mockUseAllBuiltInTools.mockReturnValue({ + mockBuiltInToolsQuery.mockReturnValue({ data: [ ...builtInTools, createToolProvider({ id: 'new-provider', plugin_id: newlyInstalledPlugin.plugin_id }), ], - } as ReturnType) + }) rerender() expect(screen.queryByText('New Plugin')).not.toBeInTheDocument() @@ -686,10 +667,11 @@ describe('ToolPicker', () => { it('should create a custom collection from the add button and refresh custom tools', async () => { const user = userEvent.setup() - renderToolPicker({ + const { queryClient } = renderToolPicker({ isShow: true, supportAddCustomTool: true, }) + for (const key of Object.values(toolListKeys)) queryClient.setQueryData(key, []) await user.click(screen.getByRole('button', { name: 'tools.addToolModal.custom.tip' })) expect(screen.getByTestId('edit-custom-tool-modal')).toBeInTheDocument() @@ -699,7 +681,8 @@ describe('ToolPicker', () => { await waitFor(() => { expect(mockCreateCustomCollection).toHaveBeenCalledWith({ name: 'collection-a' }) }) - expect(mockInvalidateCustomTools).toHaveBeenCalledTimes(1) + expect(queryClient.getQueryState(toolListKeys.custom)?.isInvalidated).toBe(true) + expect(queryClient.getQueryState(toolListKeys.builtin)?.isInvalidated).toBe(false) expect(screen.queryByTestId('edit-custom-tool-modal')).not.toBeInTheDocument() }) @@ -711,8 +694,8 @@ describe('ToolPicker', () => { supportAddCustomTool: true, }) - expect(mockUseAllCustomTools).toHaveBeenCalledWith(true) - expect(mockUseAllWorkflowTools).toHaveBeenCalledWith(true) + expect(mockCustomToolsQuery).toHaveBeenCalledWith(true) + expect(mockWorkflowToolsQuery).toHaveBeenCalledWith(true) expect(screen.getByText('Custom Provider')).toBeInTheDocument() expect(screen.getByText('Built-in Provider')).toBeInTheDocument() expect(screen.getByText('MCP Provider')).toBeInTheDocument() @@ -731,10 +714,11 @@ describe('ToolPicker', () => { isLoading: false, } as ReturnType) - renderToolPicker({ + const { queryClient } = renderToolPicker({ isShow: true, selectedTools: [], }) + for (const key of Object.values(toolListKeys)) queryClient.setQueryData(key, []) const featuredPluginItem = await screen.findByText('Plugin One') await user.hover(featuredPluginItem) @@ -744,10 +728,8 @@ describe('ToolPicker', () => { await waitFor( () => { - expect(mockInvalidateBuiltInTools).toHaveBeenCalledTimes(1) - expect(mockInvalidateCustomTools).toHaveBeenCalledTimes(1) - expect(mockInvalidateWorkflowTools).toHaveBeenCalledTimes(1) - expect(mockInvalidateMcpTools).toHaveBeenCalledTimes(1) + for (const key of Object.values(toolListKeys)) + expect(queryClient.getQueryState(key)?.isInvalidated).toBe(true) }, { timeout: 3000 }, ) diff --git a/web/app/components/workflow/block-selector/rag-tool-recommendations/index.tsx b/web/app/components/workflow/block-selector/rag-tool-recommendations/index.tsx index a775e06c8f3..f1f309435d4 100644 --- a/web/app/components/workflow/block-selector/rag-tool-recommendations/index.tsx +++ b/web/app/components/workflow/block-selector/rag-tool-recommendations/index.tsx @@ -3,13 +3,15 @@ import type { ViewType } from '@/app/components/workflow/block-selector/types' import type { OnSelectBlock } from '@/app/components/workflow/types' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' +import { useQuery } from '@tanstack/react-query' import { useMemo } from 'react' import { Trans, useTranslation } from 'react-i18next' import { LoadingPlaceholder } from '@/app/components/base/loading-placeholder' import { getFormattedPlugin } from '@/app/components/plugins/marketplace/utils' +import { selectRAGRecommendedPlugins } from '@/app/components/tools/utils/query-select' import { useRAGRecommendationsCollapsed } from '@/app/components/workflow/block-selector/storage' import Link from '@/next/link' -import { useRAGRecommendedPlugins } from '@/service/use-tools' +import { consoleQuery } from '@/service/console' import { getMarketplaceUrl } from '@/utils/var' import List from './list' @@ -31,7 +33,12 @@ export function RAGToolRecommendations({ data: ragRecommendedPlugins, isLoading: isLoadingRAGRecommendedPlugins, isFetching: isFetchingRAGRecommendedPlugins, - } = useRAGRecommendedPlugins('tool') + } = useQuery( + consoleQuery.rag.pipelines.recommendedPlugins.get.queryOptions({ + input: { query: { type: 'tool' } }, + select: selectRAGRecommendedPlugins, + }), + ) const recommendedPlugins = useMemo(() => { if (ragRecommendedPlugins) return ragRecommendedPlugins.installed_recommended_plugins diff --git a/web/app/components/workflow/block-selector/tool-panel.tsx b/web/app/components/workflow/block-selector/tool-panel.tsx index a08ae8cd2c6..eccce8510d9 100644 --- a/web/app/components/workflow/block-selector/tool-panel.tsx +++ b/web/app/components/workflow/block-selector/tool-panel.tsx @@ -1,17 +1,12 @@ import type { RagPipelineDatasourceProviderResponse } from '@dify/contracts/api/console/rag/types.gen' import type { OnSelectBlock, ToolWithProvider } from '../types' -import { useSuspenseQuery } from '@tanstack/react-query' +import { useQuery, useQueryClient, useSuspenseQuery } from '@tanstack/react-query' import dynamic from 'next/dynamic' import { useEffect, useMemo } from 'react' +import { selectToolsWithProvider } from '@/app/components/tools/utils/query-select' import { systemFeaturesQueryOptions } from '@/features/system-features/client' +import { consoleQuery } from '@/service/console' import { useFeaturedToolsRecommendations } from '@/service/use-plugins' -import { - useAllBuiltInTools, - useAllCustomTools, - useAllMCPTools, - useAllWorkflowTools, - useInvalidateAllBuiltInTools, -} from '@/service/use-tools' import { basePath } from '@/utils/var' import { useWorkflowStore } from '../store' @@ -85,11 +80,23 @@ export function ToolPanel({ onSelect: OnSelectBlock dataSources: RagPipelineDatasourceProviderResponse[] }) { - const { data: buildInTools } = useAllBuiltInTools() - const { data: customTools } = useAllCustomTools() - const { data: workflowTools } = useAllWorkflowTools() - const { data: mcpTools } = useAllMCPTools() - const invalidateBuiltInTools = useInvalidateAllBuiltInTools() + const { data: buildInTools } = useQuery( + consoleQuery.workspaces.current.tools.builtin.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: customTools } = useQuery( + consoleQuery.workspaces.current.tools.api.get.queryOptions({ select: selectToolsWithProvider }), + ) + const { data: workflowTools } = useQuery( + consoleQuery.workspaces.current.tools.workflow.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: mcpTools } = useQuery( + consoleQuery.workspaces.current.tools.mcp.get.queryOptions({ select: selectToolsWithProvider }), + ) + const queryClient = useQueryClient() const { data: enableMarketplace } = useSuspenseQuery({ ...systemFeaturesQueryOptions(), select: (systemFeatures) => systemFeatures.enable_marketplace, @@ -150,7 +157,11 @@ export function ToolPanel({ featuredPlugins={featuredPlugins} featuredLoading={isFeaturedLoading} showFeatured={enableMarketplace && !inRAGPipeline} - onFeaturedInstallSuccess={invalidateBuiltInTools} + onFeaturedInstallSuccess={() => + queryClient.invalidateQueries({ + queryKey: consoleQuery.workspaces.current.tools.builtin.get.queryKey(), + }) + } /> ) } diff --git a/web/app/components/workflow/block-selector/tool-picker.tsx b/web/app/components/workflow/block-selector/tool-picker.tsx index ad888b267fc..b7c14e230bd 100644 --- a/web/app/components/workflow/block-selector/tool-picker.tsx +++ b/web/app/components/workflow/block-selector/tool-picker.tsx @@ -7,7 +7,7 @@ import type { BlockEnum, OnSelectBlock } from '@/app/components/workflow/types' import { cn } from '@langgenius/dify-ui/cn' import { IconButton } from '@langgenius/dify-ui/icon-button' import { Popover, PopoverContent, PopoverTitle, PopoverTrigger } from '@langgenius/dify-ui/popover' -import { useSuspenseQuery } from '@tanstack/react-query' +import { useQuery, useQueryClient, useSuspenseQuery } from '@tanstack/react-query' import { useBoolean } from 'ahooks' import dynamic from 'next/dynamic' import { useMemo, useState } from 'react' @@ -15,19 +15,11 @@ import { useTranslation } from 'react-i18next' import { PluginSearchInput } from '@/app/components/plugins/plugin-search-input' import EditCustomToolModal from '@/app/components/tools/edit-custom-collection-modal' import { useCanManageTools } from '@/app/components/tools/hooks/use-tool-permissions' +import { selectToolsWithProvider } from '@/app/components/tools/utils/query-select' import { systemFeaturesQueryOptions } from '@/features/system-features/client' +import { consoleQuery } from '@/service/console' import { createCustomCollection } from '@/service/tools' import { useFeaturedToolsRecommendations } from '@/service/use-plugins' -import { - useAllBuiltInTools, - useAllCustomTools, - useAllMCPTools, - useAllWorkflowTools, - useInvalidateAllBuiltInTools, - useInvalidateAllCustomTools, - useInvalidateAllMCPTools, - useInvalidateAllWorkflowTools, -} from '@/service/use-tools' const ToolBrowser = dynamic(() => import('./tool-browser'), { loading: () =>
, @@ -73,20 +65,33 @@ export function ToolPickerContent({ ...systemFeaturesQueryOptions(), select: (s) => s.enable_marketplace, }) - const { data: buildInTools } = useAllBuiltInTools() + const { data: buildInTools } = useQuery( + consoleQuery.workspaces.current.tools.builtin.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) const installedPluginIds = useMemo( () => new Set(buildInTools?.map((provider) => provider.plugin_id || provider.id)), [buildInTools], ) const shouldFetchCustomTools = scope !== 'plugins' && scope !== 'workflow' - const { data: customTools } = useAllCustomTools(shouldFetchCustomTools) - const invalidateCustomTools = useInvalidateAllCustomTools() + const { data: customTools } = useQuery( + consoleQuery.workspaces.current.tools.api.get.queryOptions({ + enabled: shouldFetchCustomTools, + select: selectToolsWithProvider, + }), + ) + const queryClient = useQueryClient() const shouldFetchWorkflowTools = scope !== 'plugins' && scope !== 'custom' - const { data: workflowTools } = useAllWorkflowTools(shouldFetchWorkflowTools) - const { data: mcpTools } = useAllMCPTools() - const invalidateBuiltInTools = useInvalidateAllBuiltInTools() - const invalidateWorkflowTools = useInvalidateAllWorkflowTools() - const invalidateMcpTools = useInvalidateAllMCPTools() + const { data: workflowTools } = useQuery( + consoleQuery.workspaces.current.tools.workflow.get.queryOptions({ + enabled: shouldFetchWorkflowTools, + select: selectToolsWithProvider, + }), + ) + const { data: mcpTools } = useQuery( + consoleQuery.workspaces.current.tools.mcp.get.queryOptions({ select: selectToolsWithProvider }), + ) const { plugins: featuredPlugins = [], isLoading: isFeaturedLoading } = useFeaturedToolsRecommendations(enable_marketplace) @@ -138,7 +143,9 @@ export function ToolPickerContent({ await createCustomCollection(data) hideEditCustomCollectionModal() - invalidateCustomTools() + queryClient.invalidateQueries({ + queryKey: consoleQuery.workspaces.current.tools.api.get.queryKey(), + }) } if (isShowEditCollectionToolModal && canManageTools) { @@ -202,10 +209,11 @@ export function ToolPickerContent({ featuredLoading={isFeaturedLoading} showFeatured={scope === 'all' && enable_marketplace} onFeaturedInstallSuccess={async () => { - invalidateBuiltInTools() - invalidateCustomTools() - invalidateWorkflowTools() - invalidateMcpTools() + const tools = consoleQuery.workspaces.current.tools + queryClient.invalidateQueries({ queryKey: tools.builtin.get.queryKey() }) + queryClient.invalidateQueries({ queryKey: tools.api.get.queryKey() }) + queryClient.invalidateQueries({ queryKey: tools.workflow.get.queryKey() }) + queryClient.invalidateQueries({ queryKey: tools.mcp.get.queryKey() }) }} />
diff --git a/web/app/components/workflow/hooks/__tests__/use-available-blocks.spec.ts b/web/app/components/workflow/hooks/__tests__/use-available-blocks.spec.ts index df354ccf3f0..09ea5c77ff6 100644 --- a/web/app/components/workflow/hooks/__tests__/use-available-blocks.spec.ts +++ b/web/app/components/workflow/hooks/__tests__/use-available-blocks.spec.ts @@ -5,8 +5,11 @@ import { BlockEnum } from '../../types' import { useAvailableBlocks } from '../use-available-blocks' // Transitive imports of use-nodes-meta-data.ts — only useNodeMetaData uses these -vi.mock('@/service/use-tools', async () => - (await import('../../__tests__/service-mock-factory')).createToolServiceMock(), +vi.mock('@tanstack/react-query', async (importOriginal) => + (await import('@/test/console/operation-hooks')).mockConsoleOperationHooks( + await importOriginal(), + (await import('../../__tests__/service-mock-factory')).createToolOperationRoutes(), + ), ) vi.mock('@/context/i18n', () => ({ useGetLanguage: () => 'en' })) diff --git a/web/app/components/workflow/hooks/__tests__/use-checklist.spec.ts b/web/app/components/workflow/hooks/__tests__/use-checklist.spec.ts index 054a4d6d729..7caad67d661 100644 --- a/web/app/components/workflow/hooks/__tests__/use-checklist.spec.ts +++ b/web/app/components/workflow/hooks/__tests__/use-checklist.spec.ts @@ -60,13 +60,6 @@ vi.mock('reactflow', async () => { } }) -vi.mock('@/service/use-tools', () => ({ - useAllBuiltInTools: () => ({ data: toolServiceState.buildInTools }), - useAllCustomTools: () => ({ data: toolServiceState.customTools }), - useAllMCPTools: () => ({ data: toolServiceState.mcpTools }), - useAllWorkflowTools: () => ({ data: toolServiceState.workflowTools }), -})) - vi.mock('@/service/use-plugins', () => ({ useFetchPluginsInMarketPlaceByInfo: (infos: Array<{ organization: string; plugin: string }>) => ({ data: @@ -853,7 +846,8 @@ describe('useWorkflowRunValidation', () => { vi.mock('@tanstack/react-query', async (importOriginal) => { const actual = await importOriginal() - return { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + const reactQuery = { ...actual, useQuery: (options: { queryKey: OperationKey< @@ -871,5 +865,11 @@ vi.mock('@tanstack/react-query', async (importOriginal) => { if (!options.queryKey[0].includes('modelTypes')) return actual.useQuery(options) return { data: [] } }, - } + } as typeof actual + return mockConsoleOperationHooks(reactQuery, { + 'workspaces.current.tools.builtin.get': () => ({ data: toolServiceState.buildInTools }), + 'workspaces.current.tools.api.get': () => ({ data: toolServiceState.customTools }), + 'workspaces.current.tools.mcp.get': () => ({ data: toolServiceState.mcpTools }), + 'workspaces.current.tools.workflow.get': () => ({ data: toolServiceState.workflowTools }), + }) }) diff --git a/web/app/components/workflow/hooks/__tests__/use-fetch-workflow-inspect-vars.spec.ts b/web/app/components/workflow/hooks/__tests__/use-fetch-workflow-inspect-vars.spec.ts index 553dc9f224f..92ef5704940 100644 --- a/web/app/components/workflow/hooks/__tests__/use-fetch-workflow-inspect-vars.spec.ts +++ b/web/app/components/workflow/hooks/__tests__/use-fetch-workflow-inspect-vars.spec.ts @@ -27,8 +27,11 @@ vi.mock('reactflow', async () => (await import('../../__tests__/reactflow-mock-state')).createReactFlowModuleMock(), ) -vi.mock('@/service/use-tools', async () => - (await import('../../__tests__/service-mock-factory')).createToolServiceMock(), +vi.mock('@tanstack/react-query', async (importOriginal) => + (await import('@/test/console/operation-hooks')).mockConsoleOperationHooks( + await importOriginal(), + (await import('../../__tests__/service-mock-factory')).createToolOperationRoutes(), + ), ) vi.mock('@/service/use-workflow', () => ({ diff --git a/web/app/components/workflow/hooks/__tests__/use-inspect-vars-crud-common.spec.ts b/web/app/components/workflow/hooks/__tests__/use-inspect-vars-crud-common.spec.ts index c91c3b8f686..1a421db84dd 100644 --- a/web/app/components/workflow/hooks/__tests__/use-inspect-vars-crud-common.spec.ts +++ b/web/app/components/workflow/hooks/__tests__/use-inspect-vars-crud-common.spec.ts @@ -42,8 +42,11 @@ vi.mock('@/service/use-flow', () => ({ }), })) -vi.mock('@/service/use-tools', async () => - (await import('../../__tests__/service-mock-factory')).createToolServiceMock(), +vi.mock('@tanstack/react-query', async (importOriginal) => + (await import('@/test/console/operation-hooks')).mockConsoleOperationHooks( + await importOriginal(), + (await import('../../__tests__/service-mock-factory')).createToolOperationRoutes(), + ), ) vi.mock('@/service/workflow', () => ({ diff --git a/web/app/components/workflow/hooks/__tests__/use-node-plugin-installation.spec.ts b/web/app/components/workflow/hooks/__tests__/use-node-plugin-installation.spec.ts index 552159f2d64..134e2a4cf0b 100644 --- a/web/app/components/workflow/hooks/__tests__/use-node-plugin-installation.spec.ts +++ b/web/app/components/workflow/hooks/__tests__/use-node-plugin-installation.spec.ts @@ -1,11 +1,11 @@ import type { CommonNodeType } from '../../types' -import type { CollectionProviderType } from '@/app/components/tools/types' import { QueryClient } from '@tanstack/react-query' import { act } from '@testing-library/react' import { createDatasourceProvider } from '@/app/components/rag-pipeline/__tests__/datasource-fixtures' import { CollectionType } from '@/app/components/tools/types' import { consoleQuery } from '@/service/console' import { seedAccountProfileQuery } from '@/test/console/account-profile' +import { createTestQueryClient } from '@/test/query-client' import { renderWorkflowHook } from '../../__tests__/workflow-test-env' import { BlockEnum } from '../../types' import { useNodePluginInstallation } from '../use-node-plugin-installation' @@ -14,7 +14,6 @@ const mockBuiltInTools = vi.fn() const mockCustomTools = vi.fn() const mockWorkflowTools = vi.fn() const mockMcpTools = vi.fn() -const mockInvalidToolsByType = vi.fn() const mockTriggerPlugins = vi.fn() const mockInvalidateTriggers = vi.fn() let mockWorkspacePermissionKeys = ['plugin.install'] @@ -26,14 +25,19 @@ vi.mock('@/context/permission-state', async () => { })) }) -vi.mock('@/service/use-tools', () => ({ - useAllBuiltInTools: (enabled: boolean) => mockBuiltInTools(enabled), - useAllCustomTools: (enabled: boolean) => mockCustomTools(enabled), - useAllWorkflowTools: (enabled: boolean) => mockWorkflowTools(enabled), - useAllMCPTools: (enabled: boolean) => mockMcpTools(enabled), - useInvalidToolsByType: (providerType?: CollectionProviderType) => - mockInvalidToolsByType(providerType), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.tools.builtin.get': ({ enabled }: { enabled?: boolean }) => + mockBuiltInTools(enabled), + 'workspaces.current.tools.api.get': ({ enabled }: { enabled?: boolean }) => + mockCustomTools(enabled), + 'workspaces.current.tools.workflow.get': ({ enabled }: { enabled?: boolean }) => + mockWorkflowTools(enabled), + 'workspaces.current.tools.mcp.get': ({ enabled }: { enabled?: boolean }) => + mockMcpTools(enabled), + }) +}) vi.mock('@/service/use-triggers', () => ({ useAllTriggerPlugins: (enabled: boolean) => mockTriggerPlugins(enabled), @@ -103,7 +107,6 @@ describe('useNodePluginInstallation', () => { mockCustomTools.mockReturnValue({ data: undefined, isLoading: false }) mockWorkflowTools.mockReturnValue({ data: undefined, isLoading: false }) mockMcpTools.mockReturnValue({ data: undefined, isLoading: false }) - mockInvalidToolsByType.mockReturnValue(undefined) mockTriggerPlugins.mockReturnValue({ data: undefined, isLoading: false }) mockInvalidateTriggers.mockReset() }) @@ -128,11 +131,17 @@ describe('useNodePluginInstallation', () => { }) it('should report loading and invalidate built-in tools while the collection is resolving', () => { - const invalidateTools = vi.fn() mockBuiltInTools.mockReturnValue({ data: undefined, isLoading: true }) - mockInvalidToolsByType.mockReturnValue(invalidateTools) + const queryClient = createTestQueryClient() + seedAccountProfileQuery(queryClient) + const builtInToolsKey = consoleQuery.workspaces.current.tools.builtin.get.queryKey() + const customToolsKey = consoleQuery.workspaces.current.tools.api.get.queryKey() + queryClient.setQueryData(builtInToolsKey, []) + queryClient.setQueryData(customToolsKey, []) - const { result } = renderWorkflowHook(() => useNodePluginInstallation(makeToolNode())) + const { result } = renderWorkflowHook(() => useNodePluginInstallation(makeToolNode()), { + queryClient, + }) expect(mockBuiltInTools).toHaveBeenCalledWith(true) expect(result.current.isChecking).toBe(true) @@ -145,7 +154,8 @@ describe('useNodePluginInstallation', () => { result.current.onInstallSuccess() }) - expect(invalidateTools).toHaveBeenCalled() + expect(queryClient.getQueryState(builtInToolsKey)?.isInvalidated).toBe(true) + expect(queryClient.getQueryState(customToolsKey)?.isInvalidated).toBe(false) }) it.each([ diff --git a/web/app/components/workflow/hooks/__tests__/use-nodes-meta-data.spec.tsx b/web/app/components/workflow/hooks/__tests__/use-nodes-meta-data.spec.tsx index 8850b018f94..d50303803dd 100644 --- a/web/app/components/workflow/hooks/__tests__/use-nodes-meta-data.spec.tsx +++ b/web/app/components/workflow/hooks/__tests__/use-nodes-meta-data.spec.tsx @@ -19,11 +19,14 @@ const workflowToolsState = vi.hoisted( () => [] as Array<{ id: string; author: string; description: Record }>, ) -vi.mock('@/service/use-tools', () => ({ - useAllBuiltInTools: () => ({ data: buildInToolsState }), - useAllCustomTools: () => ({ data: customToolsState }), - useAllWorkflowTools: () => ({ data: workflowToolsState }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.tools.builtin.get': () => ({ data: buildInToolsState }), + 'workspaces.current.tools.api.get': () => ({ data: customToolsState }), + 'workspaces.current.tools.workflow.get': () => ({ data: workflowToolsState }), + }) +}) const createNode = (overrides: Partial = {}): Node => ({ diff --git a/web/app/components/workflow/hooks/__tests__/use-tool-icon.spec.ts b/web/app/components/workflow/hooks/__tests__/use-tool-icon.spec.ts index 02c6250c625..8f930fc1219 100644 --- a/web/app/components/workflow/hooks/__tests__/use-tool-icon.spec.ts +++ b/web/app/components/workflow/hooks/__tests__/use-tool-icon.spec.ts @@ -9,23 +9,26 @@ vi.mock('reactflow', async () => (await import('../../__tests__/reactflow-mock-state')).createReactFlowModuleMock(), ) -vi.mock('@/service/use-tools', async () => - (await import('../../__tests__/service-mock-factory')).createToolServiceMock({ - buildInTools: [ - { - id: 'builtin-1', - name: 'builtin', - icon: '/builtin.svg', - icon_dark: '/builtin-dark.svg', - plugin_id: 'p1', - }, - ], - customTools: [{ id: 'custom-1', name: 'custom', icon: '/custom.svg', plugin_id: 'p2' }], - workflowTools: [ - { id: 'workflow-1', name: 'workflow-tool', icon: '/workflow.svg', plugin_id: 'p3' }, - ], - mcpTools: [{ id: 'mcp-1', name: 'mcp-tool', icon: '/mcp.svg', plugin_id: 'p4' }], - }), +vi.mock('@tanstack/react-query', async (importOriginal) => + (await import('@/test/console/operation-hooks')).mockConsoleOperationHooks( + await importOriginal(), + (await import('../../__tests__/service-mock-factory')).createToolOperationRoutes({ + buildInTools: [ + { + id: 'builtin-1', + name: 'builtin', + icon: '/builtin.svg', + icon_dark: '/builtin-dark.svg', + plugin_id: 'p1', + }, + ], + customTools: [{ id: 'custom-1', name: 'custom', icon: '/custom.svg', plugin_id: 'p2' }], + workflowTools: [ + { id: 'workflow-1', name: 'workflow-tool', icon: '/workflow.svg', plugin_id: 'p3' }, + ], + mcpTools: [{ id: 'mcp-1', name: 'mcp-tool', icon: '/mcp.svg', plugin_id: 'p4' }], + }), + ), ) vi.mock('@/service/use-triggers', async () => diff --git a/web/app/components/workflow/hooks/__tests__/use-workflow-search.spec.tsx b/web/app/components/workflow/hooks/__tests__/use-workflow-search.spec.tsx index e0a9a9d5aab..8d06f72ed57 100644 --- a/web/app/components/workflow/hooks/__tests__/use-workflow-search.spec.tsx +++ b/web/app/components/workflow/hooks/__tests__/use-workflow-search.spec.tsx @@ -18,20 +18,23 @@ vi.mock('../use-nodes-interactions', () => ({ }), })) -vi.mock('@/service/use-tools', () => ({ - useAllBuiltInTools: () => ({ - data: [ - { - id: 'provider-1', - icon: 'tool-icon', - tools: [], - }, - ] satisfies Partial[], - }), - useAllCustomTools: () => ({ data: [] }), - useAllWorkflowTools: () => ({ data: [] }), - useAllMCPTools: () => ({ data: [] }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.tools.builtin.get': () => ({ + data: [ + { + id: 'provider-1', + icon: 'tool-icon', + tools: [], + }, + ] satisfies Partial[], + }), + 'workspaces.current.tools.api.get': () => ({ data: [] }), + 'workspaces.current.tools.workflow.get': () => ({ data: [] }), + 'workspaces.current.tools.mcp.get': () => ({ data: [] }), + }) +}) const createNode = (overrides: Partial = {}): Node => ({ id: 'node-1', diff --git a/web/app/components/workflow/hooks/__tests__/use-workflow-variables.spec.ts b/web/app/components/workflow/hooks/__tests__/use-workflow-variables.spec.ts index 6bdb76d83f5..a729060b2ab 100644 --- a/web/app/components/workflow/hooks/__tests__/use-workflow-variables.spec.ts +++ b/web/app/components/workflow/hooks/__tests__/use-workflow-variables.spec.ts @@ -6,8 +6,11 @@ vi.mock('reactflow', async () => (await import('../../__tests__/reactflow-mock-state')).createReactFlowModuleMock(), ) -vi.mock('@/service/use-tools', async () => - (await import('../../__tests__/service-mock-factory')).createToolServiceMock(), +vi.mock('@tanstack/react-query', async (importOriginal) => + (await import('@/test/console/operation-hooks')).mockConsoleOperationHooks( + await importOriginal(), + (await import('../../__tests__/service-mock-factory')).createToolOperationRoutes(), + ), ) const { mockToNodeAvailableVars, mockGetVarType } = vi.hoisted(() => ({ diff --git a/web/app/components/workflow/hooks/use-checklist.ts b/web/app/components/workflow/hooks/use-checklist.ts index d6519980626..4bd90eaa81a 100644 --- a/web/app/components/workflow/hooks/use-checklist.ts +++ b/web/app/components/workflow/hooks/use-checklist.ts @@ -25,6 +25,7 @@ import { useTranslation } from 'react-i18next' import { useEdges, useStoreApi } from 'reactflow' import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { normalizeModelProviderModelsResponse } from '@/app/components/header/account-setting/model-provider-page/utils' +import { selectToolsWithProvider } from '@/app/components/tools/utils/query-select' import useNodes from '@/app/components/workflow/store/workflow/use-nodes' import { toast } from '@/app/notifications' import { MAX_TREE_DEPTH } from '@/config' @@ -38,12 +39,6 @@ import { import { useParams } from '@/next/navigation' import { consoleQuery } from '@/service/console' import { fetchDatasets } from '@/service/datasets' -import { - useAllBuiltInTools, - useAllCustomTools, - useAllMCPTools, - useAllWorkflowTools, -} from '@/service/use-tools' import { useAllTriggerPlugins } from '@/service/use-triggers' import { AppModeEnum } from '@/types/app' import { FlowType } from '@/types/common' @@ -166,10 +161,22 @@ export const useChecklist = (nodes: Node[], edges: Edge[], options?: { flowType? const { t } = useTranslation(['agentV2', 'common', 'workflow', 'modelProvider']) const language = useGetLanguage() const { nodesMap: nodesExtraData } = useNodesMetaData() - const { data: buildInTools } = useAllBuiltInTools() - const { data: customTools } = useAllCustomTools() - const { data: workflowTools } = useAllWorkflowTools() - const { data: mcpTools } = useAllMCPTools() + const { data: buildInTools } = useQuery( + consoleQuery.workspaces.current.tools.builtin.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: customTools } = useQuery( + consoleQuery.workspaces.current.tools.api.get.queryOptions({ select: selectToolsWithProvider }), + ) + const { data: workflowTools } = useQuery( + consoleQuery.workspaces.current.tools.workflow.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: mcpTools } = useQuery( + consoleQuery.workspaces.current.tools.mcp.get.queryOptions({ select: selectToolsWithProvider }), + ) const inlineAgentToolProviderCatalog = useMemo( () => createAgentToolProviderCatalog({ buildInTools, customTools, mcpTools, workflowTools }), [buildInTools, customTools, mcpTools, workflowTools], @@ -653,9 +660,19 @@ export const useChecklistBeforePublish = () => { select: (response) => response.data, }), ) - const { data: buildInTools } = useAllBuiltInTools() - const { data: customTools } = useAllCustomTools() - const { data: workflowTools } = useAllWorkflowTools() + const { data: buildInTools } = useQuery( + consoleQuery.workspaces.current.tools.builtin.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: customTools } = useQuery( + consoleQuery.workspaces.current.tools.api.get.queryOptions({ select: selectToolsWithProvider }), + ) + const { data: workflowTools } = useQuery( + consoleQuery.workspaces.current.tools.workflow.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) const flowType = useHooksStore((s) => s.configsMap?.flowType) const { appId } = useParams<{ appId?: string }>() const { data: appMode } = useQuery( diff --git a/web/app/components/workflow/hooks/use-fetch-workflow-inspect-vars.ts b/web/app/components/workflow/hooks/use-fetch-workflow-inspect-vars.ts index 653bd9d5595..b96b6d4abc3 100644 --- a/web/app/components/workflow/hooks/use-fetch-workflow-inspect-vars.ts +++ b/web/app/components/workflow/hooks/use-fetch-workflow-inspect-vars.ts @@ -2,15 +2,12 @@ import type { Node, WorkflowPluginCatalogs } from '@/app/components/workflow/typ import type { SchemaTypeDefinition } from '@/service/use-common' import type { FlowType } from '@/types/common' import type { NodeWithVar, VarInInspect } from '@/types/workflow' +import { useQuery } from '@tanstack/react-query' import { useCallback, useMemo } from 'react' import { useStoreApi } from 'reactflow' +import { selectToolsWithProvider } from '@/app/components/tools/utils/query-select' import { useStore, useWorkflowStore } from '@/app/components/workflow/store' -import { - useAllBuiltInTools, - useAllCustomTools, - useAllMCPTools, - useAllWorkflowTools, -} from '@/service/use-tools' +import { consoleQuery } from '@/service/console' import { useInvalidateConversationVarValues, useInvalidateSysVarValues, @@ -32,10 +29,22 @@ export const useSetWorkflowVarsWithValue = ({ flowType, flowId }: Params) => { const invalidateSysVarValues = useInvalidateSysVarValues(flowType, flowId) const { handleCancelAllNodeSuccessStatus } = useNodesInteractionsWithoutSync() const { schemaTypeDefinitions } = useMatchSchemaType() - const { data: buildInTools } = useAllBuiltInTools() - const { data: customTools } = useAllCustomTools() - const { data: workflowTools } = useAllWorkflowTools() - const { data: mcpTools } = useAllMCPTools() + const { data: buildInTools } = useQuery( + consoleQuery.workspaces.current.tools.builtin.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: customTools } = useQuery( + consoleQuery.workspaces.current.tools.api.get.queryOptions({ select: selectToolsWithProvider }), + ) + const { data: workflowTools } = useQuery( + consoleQuery.workspaces.current.tools.workflow.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: mcpTools } = useQuery( + consoleQuery.workspaces.current.tools.mcp.get.queryOptions({ select: selectToolsWithProvider }), + ) const dataSourceList = useStore((s) => s.dataSourceList) const allPluginInfoList = useMemo(() => { diff --git a/web/app/components/workflow/hooks/use-inspect-vars-crud-common.ts b/web/app/components/workflow/hooks/use-inspect-vars-crud-common.ts index d8913d5a5fc..f1d42c6e490 100644 --- a/web/app/components/workflow/hooks/use-inspect-vars-crud-common.ts +++ b/web/app/components/workflow/hooks/use-inspect-vars-crud-common.ts @@ -2,9 +2,11 @@ import type { Node, ValueSelector } from '@/app/components/workflow/types' import type { SchemaTypeDefinition } from '@/service/use-common' import type { FlowType } from '@/types/common' import type { VarInInspect } from '@/types/workflow' +import { useQuery } from '@tanstack/react-query' import { produce } from 'immer' import { useCallback } from 'react' import { useStoreApi } from 'reactflow' +import { selectToolsWithProvider } from '@/app/components/tools/utils/query-select' import { isConversationVar, isENV, @@ -12,13 +14,8 @@ import { toNodeOutputVars, } from '@/app/components/workflow/nodes/_base/components/variable/utils' import { useWorkflowStore } from '@/app/components/workflow/store' +import { consoleQuery } from '@/service/console' import useFLow from '@/service/use-flow' -import { - useAllBuiltInTools, - useAllCustomTools, - useAllMCPTools, - useAllWorkflowTools, -} from '@/service/use-tools' import { fetchNodeInspectVars } from '@/service/workflow' import { VarInInspectType } from '@/types/workflow' import { useEdgesInteractionsWithoutSync } from './use-edges-interactions-without-sync' @@ -53,10 +50,22 @@ export const useInspectVarsCrudCommon = ({ flowId, flowType }: Params) => { const { mutateAsync: doEditInspectorVar } = useEditInspectorVar(flowId) const { handleCancelNodeSuccessStatus } = useNodesInteractionsWithoutSync() const { handleEdgeCancelRunningStatus } = useEdgesInteractionsWithoutSync() - const { data: buildInTools } = useAllBuiltInTools() - const { data: customTools } = useAllCustomTools() - const { data: workflowTools } = useAllWorkflowTools() - const { data: mcpTools } = useAllMCPTools() + const { data: buildInTools } = useQuery( + consoleQuery.workspaces.current.tools.builtin.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: customTools } = useQuery( + consoleQuery.workspaces.current.tools.api.get.queryOptions({ select: selectToolsWithProvider }), + ) + const { data: workflowTools } = useQuery( + consoleQuery.workspaces.current.tools.workflow.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: mcpTools } = useQuery( + consoleQuery.workspaces.current.tools.mcp.get.queryOptions({ select: selectToolsWithProvider }), + ) const getNodeInspectVars = useCallback( (nodeId: string) => { diff --git a/web/app/components/workflow/hooks/use-node-plugin-installation.ts b/web/app/components/workflow/hooks/use-node-plugin-installation.ts index 5c228a791c9..c754f27f56c 100644 --- a/web/app/components/workflow/hooks/use-node-plugin-installation.ts +++ b/web/app/components/workflow/hooks/use-node-plugin-installation.ts @@ -2,18 +2,13 @@ import type { DataSourceNodeType } from '../nodes/data-source/types' import type { ToolNodeType } from '../nodes/tool/types' import type { PluginTriggerNodeType } from '../nodes/trigger-plugin/types' import type { CommonNodeType } from '../types' -import { useQueryClient } from '@tanstack/react-query' +import { useQuery, useQueryClient } from '@tanstack/react-query' import { useCallback, useMemo } from 'react' import useWorkspacePluginInstallPermission from '@/app/components/plugins/install-plugin/hooks/use-workspace-plugin-install-permission' import { CollectionType } from '@/app/components/tools/types' +import { selectToolsWithProvider } from '@/app/components/tools/utils/query-select' +import { getToolListQueryKey } from '@/app/components/tools/utils/tool-list-query-key' import { consoleQuery } from '@/service/console' -import { - useAllBuiltInTools, - useAllCustomTools, - useAllMCPTools, - useAllWorkflowTools, - useInvalidToolsByType, -} from '@/service/use-tools' import { useAllTriggerPlugins, useInvalidateAllTriggerPlugins } from '@/service/use-triggers' import { useStore } from '../store' import { BlockEnum } from '../types' @@ -51,11 +46,32 @@ const useToolInstallation = ( const isWorkflow = enabled && data.provider_type === CollectionType.workflow const isMcp = enabled && data.provider_type === CollectionType.mcp - const builtInQuery = useAllBuiltInTools(isBuiltIn) - const customQuery = useAllCustomTools(isCustom) - const workflowQuery = useAllWorkflowTools(isWorkflow) - const mcpQuery = useAllMCPTools(isMcp) - const invalidateTools = useInvalidToolsByType(enabled ? data.provider_type : undefined) + const builtInQuery = useQuery( + consoleQuery.workspaces.current.tools.builtin.get.queryOptions({ + enabled: isBuiltIn, + select: selectToolsWithProvider, + }), + ) + const customQuery = useQuery( + consoleQuery.workspaces.current.tools.api.get.queryOptions({ + enabled: isCustom, + select: selectToolsWithProvider, + }), + ) + const workflowQuery = useQuery( + consoleQuery.workspaces.current.tools.workflow.get.queryOptions({ + enabled: isWorkflow, + select: selectToolsWithProvider, + }), + ) + const mcpQuery = useQuery( + consoleQuery.workspaces.current.tools.mcp.get.queryOptions({ + enabled: isMcp, + select: selectToolsWithProvider, + }), + ) + const queryClient = useQueryClient() + const toolListQueryKey = enabled ? getToolListQueryKey(data.provider_type) : undefined const collectionInfo = useMemo(() => { if (!enabled) return undefined @@ -112,8 +128,8 @@ const useToolInstallation = ( const canInstall = Boolean(data.plugin_unique_identifier) && canInstallPlugin const onInstallSuccess = useCallback(() => { - if (invalidateTools) invalidateTools() - }, [invalidateTools]) + if (toolListQueryKey) queryClient.invalidateQueries({ queryKey: toolListQueryKey }) + }, [queryClient, toolListQueryKey]) const shouldDim = (!!collectionInfo && !isResolved) || (isResolved && !matchedCollection) diff --git a/web/app/components/workflow/hooks/use-nodes-meta-data.ts b/web/app/components/workflow/hooks/use-nodes-meta-data.ts index b7d195a878f..054377f3e63 100644 --- a/web/app/components/workflow/hooks/use-nodes-meta-data.ts +++ b/web/app/components/workflow/hooks/use-nodes-meta-data.ts @@ -1,14 +1,16 @@ import type { AvailableNodesMetaData } from '@/app/components/workflow/hooks-store' import type { Node } from '@/app/components/workflow/types' +import { useQuery } from '@tanstack/react-query' import { useMemo } from 'react' import { CollectionType } from '@/app/components/tools/types' +import { selectToolsWithProvider } from '@/app/components/tools/utils/query-select' import { useHooksStore } from '@/app/components/workflow/hooks-store' import { useStore } from '@/app/components/workflow/store' import { BlockEnum } from '@/app/components/workflow/types' import { getNodeCatalogType } from '@/app/components/workflow/utils/node' import { useGetLanguage } from '@/context/i18n' import { renderI18nObject } from '@/i18n/metadata' -import { useAllBuiltInTools, useAllCustomTools, useAllWorkflowTools } from '@/service/use-tools' +import { consoleQuery } from '@/service/console' import { matchesProviderReference } from '@/utils/provider-reference' import { matchDataSource } from '../utils/plugin-install-check' @@ -25,9 +27,19 @@ export const useNodesMetaData = () => { export const useNodeMetaData = (node: Node) => { const language = useGetLanguage() - const { data: buildInTools } = useAllBuiltInTools() - const { data: customTools } = useAllCustomTools() - const { data: workflowTools } = useAllWorkflowTools() + const { data: buildInTools } = useQuery( + consoleQuery.workspaces.current.tools.builtin.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: customTools } = useQuery( + consoleQuery.workspaces.current.tools.api.get.queryOptions({ select: selectToolsWithProvider }), + ) + const { data: workflowTools } = useQuery( + consoleQuery.workspaces.current.tools.workflow.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) const dataSourceList = useStore((s) => s.dataSourceList) const availableNodesMetaData = useNodesMetaData() const { data } = node diff --git a/web/app/components/workflow/hooks/use-tool-icon.ts b/web/app/components/workflow/hooks/use-tool-icon.ts index 1b71c7fd0f6..f9a13bf1af4 100644 --- a/web/app/components/workflow/hooks/use-tool-icon.ts +++ b/web/app/components/workflow/hooks/use-tool-icon.ts @@ -4,16 +4,13 @@ import type { DataSourceNodeType } from '../nodes/data-source/types' import type { ToolNodeType } from '../nodes/tool/types' import type { PluginTriggerNodeType } from '../nodes/trigger-plugin/types' import type { Node, ToolWithProvider } from '../types' +import { useQuery } from '@tanstack/react-query' import { useCallback, useMemo } from 'react' import { resolveDatasourceIcon } from '@/app/components/rag-pipeline/utils/datasource-icon' import { CollectionType } from '@/app/components/tools/types' +import { selectToolsWithProvider } from '@/app/components/tools/utils/query-select' import useTheme from '@/hooks/use-theme' -import { - useAllBuiltInTools, - useAllCustomTools, - useAllMCPTools, - useAllWorkflowTools, -} from '@/service/use-tools' +import { consoleQuery } from '@/service/console' import { useAllTriggerPlugins } from '@/service/use-triggers' import { canFindTool } from '@/utils' import { matchesProviderReference } from '@/utils/provider-reference' @@ -174,10 +171,22 @@ export const findNodeIcon = ({ } export const useToolIcon = (data?: Node['data']) => { - const { data: buildInTools } = useAllBuiltInTools() - const { data: customTools } = useAllCustomTools() - const { data: workflowTools } = useAllWorkflowTools() - const { data: mcpTools } = useAllMCPTools() + const { data: buildInTools } = useQuery( + consoleQuery.workspaces.current.tools.builtin.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: customTools } = useQuery( + consoleQuery.workspaces.current.tools.api.get.queryOptions({ select: selectToolsWithProvider }), + ) + const { data: workflowTools } = useQuery( + consoleQuery.workspaces.current.tools.workflow.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: mcpTools } = useQuery( + consoleQuery.workspaces.current.tools.mcp.get.queryOptions({ select: selectToolsWithProvider }), + ) const dataSourceList = useStore((s) => s.dataSourceList) const { data: triggerPlugins } = useAllTriggerPlugins() const { theme } = useTheme() @@ -212,10 +221,22 @@ export const useToolIcon = (data?: Node['data']) => { } export const useGetToolIcon = () => { - const { data: buildInTools } = useAllBuiltInTools() - const { data: customTools } = useAllCustomTools() - const { data: workflowTools } = useAllWorkflowTools() - const { data: mcpTools } = useAllMCPTools() + const { data: buildInTools } = useQuery( + consoleQuery.workspaces.current.tools.builtin.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: customTools } = useQuery( + consoleQuery.workspaces.current.tools.api.get.queryOptions({ select: selectToolsWithProvider }), + ) + const { data: workflowTools } = useQuery( + consoleQuery.workspaces.current.tools.workflow.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: mcpTools } = useQuery( + consoleQuery.workspaces.current.tools.mcp.get.queryOptions({ select: selectToolsWithProvider }), + ) const { data: triggerPlugins } = useAllTriggerPlugins() const workflowStore = useWorkflowStore() const { theme } = useTheme() diff --git a/web/app/components/workflow/hooks/use-workflow-search.tsx b/web/app/components/workflow/hooks/use-workflow-search.tsx index fa7e7cb9b5a..c605b9562d9 100644 --- a/web/app/components/workflow/hooks/use-workflow-search.tsx +++ b/web/app/components/workflow/hooks/use-workflow-search.tsx @@ -3,17 +3,14 @@ import type { LLMNodeType } from '../nodes/llm/types' import type { CommonNodeType } from '../types' import type { Emoji } from '@/app/components/tools/types' +import { useQuery } from '@tanstack/react-query' import { useCallback, useEffect, useMemo } from 'react' import { useNodes } from 'reactflow' import { CollectionType } from '@/app/components/tools/types' +import { selectToolsWithProvider } from '@/app/components/tools/utils/query-select' import BlockIcon from '@/app/components/workflow/block-icon' import { registerWorkflowNodeSearch } from '@/app/components/workflow/goto-anything-search' -import { - useAllBuiltInTools, - useAllCustomTools, - useAllMCPTools, - useAllWorkflowTools, -} from '@/service/use-tools' +import { consoleQuery } from '@/service/console' import { matchesProviderReference } from '@/utils/provider-reference' import { BlockEnum } from '../types' import { setupNodeSelectionListener } from '../utils/node-navigation' @@ -27,10 +24,22 @@ export const useWorkflowSearch = () => { const { handleNodeSelect } = useNodesInteractions() // Filter and process nodes for search - const { data: buildInTools } = useAllBuiltInTools() - const { data: customTools } = useAllCustomTools() - const { data: workflowTools } = useAllWorkflowTools() - const { data: mcpTools } = useAllMCPTools() + const { data: buildInTools } = useQuery( + consoleQuery.workspaces.current.tools.builtin.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: customTools } = useQuery( + consoleQuery.workspaces.current.tools.api.get.queryOptions({ select: selectToolsWithProvider }), + ) + const { data: workflowTools } = useQuery( + consoleQuery.workspaces.current.tools.workflow.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: mcpTools } = useQuery( + consoleQuery.workspaces.current.tools.mcp.get.queryOptions({ select: selectToolsWithProvider }), + ) // Extract tool icon logic - clean separation of concerns const getToolIcon = useCallback( diff --git a/web/app/components/workflow/hooks/use-workflow-variables.ts b/web/app/components/workflow/hooks/use-workflow-variables.ts index 2d722372518..297aaf32ae1 100644 --- a/web/app/components/workflow/hooks/use-workflow-variables.ts +++ b/web/app/components/workflow/hooks/use-workflow-variables.ts @@ -1,18 +1,15 @@ import type { Type } from '../nodes/llm/types' import type { Node, NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types' +import { useQuery } from '@tanstack/react-query' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' import { useStoreApi } from 'reactflow' +import { selectToolsWithProvider } from '@/app/components/tools/utils/query-select' import { getVarType, toNodeAvailableVars, } from '@/app/components/workflow/nodes/_base/components/variable/utils' -import { - useAllBuiltInTools, - useAllCustomTools, - useAllMCPTools, - useAllWorkflowTools, -} from '@/service/use-tools' +import { consoleQuery } from '@/service/console' import useMatchSchemaType from '../nodes/_base/components/variable/use-match-schema-type' import { useWorkflowStore } from '../store' import { useIsChatMode } from './use-workflow' @@ -22,10 +19,22 @@ export const useWorkflowVariables = () => { const workflowStore = useWorkflowStore() const { schemaTypeDefinitions } = useMatchSchemaType() - const { data: buildInTools } = useAllBuiltInTools() - const { data: customTools } = useAllCustomTools() - const { data: workflowTools } = useAllWorkflowTools() - const { data: mcpTools } = useAllMCPTools() + const { data: buildInTools } = useQuery( + consoleQuery.workspaces.current.tools.builtin.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: customTools } = useQuery( + consoleQuery.workspaces.current.tools.api.get.queryOptions({ select: selectToolsWithProvider }), + ) + const { data: workflowTools } = useQuery( + consoleQuery.workspaces.current.tools.workflow.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: mcpTools } = useQuery( + consoleQuery.workspaces.current.tools.mcp.get.queryOptions({ select: selectToolsWithProvider }), + ) const getNodeAvailableVars = useCallback( ({ diff --git a/web/app/components/workflow/index.tsx b/web/app/components/workflow/index.tsx index e2f3a6bd476..5d836b9cadb 100644 --- a/web/app/components/workflow/index.tsx +++ b/web/app/components/workflow/index.tsx @@ -19,6 +19,7 @@ import { AlertDialogTitle, } from '@langgenius/dify-ui/alert-dialog' import { cn } from '@langgenius/dify-ui/cn' +import { useQuery } from '@tanstack/react-query' import { useEventListener } from 'ahooks' import { isEqual } from 'es-toolkit/predicate' import { setAutoFreeze } from 'immer' @@ -46,15 +47,11 @@ import { useReactFlow, useStoreApi, } from 'reactflow' +import { selectToolsWithProvider } from '@/app/components/tools/utils/query-select' import { toast } from '@/app/notifications' import { IS_DEV } from '@/config' import { useEventEmitterContextContext } from '@/context/event-emitter' -import { - useAllBuiltInTools, - useAllCustomTools, - useAllMCPTools, - useAllWorkflowTools, -} from '@/service/use-tools' +import { consoleQuery } from '@/service/console' import { fetchAllInspectVars } from '@/service/workflow' import CandidateNode from './candidate-node' import UserCursors from './collaboration/components/user-cursors' @@ -567,10 +564,26 @@ export const Workflow: FC = memo( const { schemaTypeDefinitions } = useMatchSchemaType() const { fetchInspectVars } = useSetWorkflowVarsWithValue() - const { data: buildInTools } = useAllBuiltInTools() - const { data: customTools } = useAllCustomTools() - const { data: workflowTools } = useAllWorkflowTools() - const { data: mcpTools } = useAllMCPTools() + const { data: buildInTools } = useQuery( + consoleQuery.workspaces.current.tools.builtin.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: customTools } = useQuery( + consoleQuery.workspaces.current.tools.api.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: workflowTools } = useQuery( + consoleQuery.workspaces.current.tools.workflow.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: mcpTools } = useQuery( + consoleQuery.workspaces.current.tools.mcp.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) const dataSourceList = useStore((s) => s.dataSourceList) // buildInTools, customTools, workflowTools, mcpTools, dataSourceList const configsMap = useHooksStore((s) => s.configsMap) diff --git a/web/app/components/workflow/node-actions-menu/__tests__/details.spec.tsx b/web/app/components/workflow/node-actions-menu/__tests__/details.spec.tsx index 62334178808..a62903423ba 100644 --- a/web/app/components/workflow/node-actions-menu/__tests__/details.spec.tsx +++ b/web/app/components/workflow/node-actions-menu/__tests__/details.spec.tsx @@ -5,7 +5,6 @@ import { renderWorkflowFlowComponent } from '@/app/components/workflow/__tests__ import { useHooksStore } from '@/app/components/workflow/hooks-store' import useNodes from '@/app/components/workflow/store/workflow/use-nodes' import { BlockEnum, NodeRunningStatus } from '@/app/components/workflow/types' -import { useAllWorkflowTools } from '@/service/use-tools' import { FlowType } from '@/types/common' import { BlockClassification } from '../../block-selector/types' import { useAvailableBlocks } from '../../hooks/use-available-blocks' @@ -59,13 +58,17 @@ vi.mock('@/app/components/workflow/store/workflow/use-nodes', () => ({ default: vi.fn(), })) -vi.mock('@/service/use-tools', () => ({ - useAllWorkflowTools: vi.fn(), - useAllBuiltInTools: () => ({ data: [] }), - useAllCustomTools: () => ({ data: [] }), - useAllMCPTools: () => ({ data: [] }), - useInvalidateAllBuiltInTools: () => vi.fn(), -})) +const { mockUseAllWorkflowTools } = vi.hoisted(() => ({ mockUseAllWorkflowTools: vi.fn() })) + +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.tools.workflow.get': () => mockUseAllWorkflowTools(), + 'workspaces.current.tools.builtin.get': () => ({ data: [] }), + 'workspaces.current.tools.api.get': () => ({ data: [] }), + 'workspaces.current.tools.mcp.get': () => ({ data: [] }), + }) +}) vi.mock('@/service/use-plugins', () => ({ useFeaturedToolsRecommendations: () => ({ plugins: [], isLoading: false }), @@ -86,7 +89,6 @@ const mockUseNodesInteractions = vi.mocked(useNodesInteractions) const mockUseNodesReadOnly = vi.mocked(useNodesReadOnly) const mockUseHooksStore = vi.mocked(useHooksStore) const mockUseNodes = vi.mocked(useNodes) -const mockUseAllWorkflowTools = vi.mocked(useAllWorkflowTools) async function renderDropdownContent({ showHelpLink = true, diff --git a/web/app/components/workflow/node-actions-menu/__tests__/index.spec.tsx b/web/app/components/workflow/node-actions-menu/__tests__/index.spec.tsx index d0a655d6fad..92909f0934e 100644 --- a/web/app/components/workflow/node-actions-menu/__tests__/index.spec.tsx +++ b/web/app/components/workflow/node-actions-menu/__tests__/index.spec.tsx @@ -5,7 +5,6 @@ import { fireEvent, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { renderWorkflowFlowComponent } from '@/app/components/workflow/__tests__/workflow-test-env' import { BlockEnum } from '@/app/components/workflow/types' -import { useAllWorkflowTools } from '@/service/use-tools' import { useNodesInteractions } from '../../hooks/use-nodes-interactions' import { useNodeMetaData } from '../../hooks/use-nodes-meta-data' import { useNodesReadOnly } from '../../hooks/use-workflow' @@ -38,9 +37,14 @@ vi.mock('../../hooks/use-workflow', async (importOriginal) => { } }) -vi.mock('@/service/use-tools', () => ({ - useAllWorkflowTools: vi.fn(), -})) +const { mockUseAllWorkflowTools } = vi.hoisted(() => ({ mockUseAllWorkflowTools: vi.fn() })) + +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.tools.workflow.get': () => mockUseAllWorkflowTools(), + }) +}) vi.mock('../change-block-popup', () => ({ ChangeBlockPopup: () =>
, @@ -49,7 +53,6 @@ vi.mock('../change-block-popup', () => ({ const mockUseNodeMetaData = vi.mocked(useNodeMetaData) const mockUseNodesInteractions = vi.mocked(useNodesInteractions) const mockUseNodesReadOnly = vi.mocked(useNodesReadOnly) -const mockUseAllWorkflowTools = vi.mocked(useAllWorkflowTools) const createQueryResult = (data: T): UseQueryResult => ({ diff --git a/web/app/components/workflow/node-actions-menu/use-node-actions-menu-model.ts b/web/app/components/workflow/node-actions-menu/use-node-actions-menu-model.ts index f3652a4b6c3..4110a996fb5 100644 --- a/web/app/components/workflow/node-actions-menu/use-node-actions-menu-model.ts +++ b/web/app/components/workflow/node-actions-menu/use-node-actions-menu-model.ts @@ -1,12 +1,14 @@ import type { Node } from '@/app/components/workflow/types' +import { useQuery } from '@tanstack/react-query' import { useCallback, useMemo } from 'react' import { useEdges } from 'reactflow' import { CollectionType } from '@/app/components/tools/types' +import { selectToolsWithProvider } from '@/app/components/tools/utils/query-select' import { useHooksStore } from '@/app/components/workflow/hooks-store' import { useWorkflowStore } from '@/app/components/workflow/store' import { BlockEnum, NodeRunningStatus } from '@/app/components/workflow/types' import { canRunBySingle } from '@/app/components/workflow/utils' -import { useAllWorkflowTools } from '@/service/use-tools' +import { consoleQuery } from '@/service/console' import { matchesProviderReference } from '@/utils/provider-reference' import { useNodesInteractions } from '../hooks/use-nodes-interactions' import { useNodeMetaData } from '../hooks/use-nodes-meta-data' @@ -32,7 +34,11 @@ export function useNodeActionsMenuModel({ const { nodesReadOnly } = useNodesReadOnly() const canRunWorkflow = useHooksStore((s) => s.accessControl.canRun) const nodeMetaData = useNodeMetaData({ id, data } as Node) - const { data: workflowTools } = useAllWorkflowTools() + const { data: workflowTools } = useQuery( + consoleQuery.workspaces.current.tools.workflow.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) const isChildNode = !!(data.isInIteration || data.isInLoop) const canRun = canRunWorkflow && !nodesReadOnly && canRunBySingle(data.type, isChildNode) diff --git a/web/app/components/workflow/nodes/_base/components/__tests__/node-focus.browser.spec.tsx b/web/app/components/workflow/nodes/_base/components/__tests__/node-focus.browser.spec.tsx index cacd1ca5b69..297fec4f32c 100644 --- a/web/app/components/workflow/nodes/_base/components/__tests__/node-focus.browser.spec.tsx +++ b/web/app/components/workflow/nodes/_base/components/__tests__/node-focus.browser.spec.tsx @@ -59,15 +59,16 @@ vi.mock('@/service/use-triggers', () => ({ useInvalidateAllTriggerPlugins: () => vi.fn(), })) -vi.mock('@/service/use-tools', () => ({ - useRAGRecommendedPlugins: () => ({ data: [] }), - useInvalidateAllToolProviders: () => vi.fn(), - useAllBuiltInTools: () => ({ data: [] }), - useAllCustomTools: () => ({ data: [] }), - useAllWorkflowTools: () => ({ data: [] }), - useAllMCPTools: () => ({ data: [] }), - useInvalidateAllBuiltInTools: () => vi.fn(), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'rag.pipelines.recommendedPlugins.get': () => ({ data: [] }), + 'workspaces.current.tools.builtin.get': () => ({ data: [] }), + 'workspaces.current.tools.api.get': () => ({ data: [] }), + 'workspaces.current.tools.workflow.get': () => ({ data: [] }), + 'workspaces.current.tools.mcp.get': () => ({ data: [] }), + }) +}) vi.mock('@/app/components/plugins/marketplace/query', () => ({ useMarketplacePlugins: () => ({ data: undefined }), diff --git a/web/app/components/workflow/nodes/_base/components/__tests__/node-handle.spec.tsx b/web/app/components/workflow/nodes/_base/components/__tests__/node-handle.spec.tsx index 57c7ff0c34b..9c8adec850b 100644 --- a/web/app/components/workflow/nodes/_base/components/__tests__/node-handle.spec.tsx +++ b/web/app/components/workflow/nodes/_base/components/__tests__/node-handle.spec.tsx @@ -144,13 +144,15 @@ vi.mock('@/service/use-triggers', () => ({ useInvalidateAllTriggerPlugins: () => vi.fn(), })) -vi.mock('@/service/use-tools', () => ({ - useAllBuiltInTools: () => ({ data: [] }), - useAllCustomTools: () => ({ data: [] }), - useAllWorkflowTools: () => ({ data: [] }), - useAllMCPTools: () => ({ data: [] }), - useInvalidateAllBuiltInTools: () => vi.fn(), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.tools.builtin.get': () => ({ data: [] }), + 'workspaces.current.tools.api.get': () => ({ data: [] }), + 'workspaces.current.tools.workflow.get': () => ({ data: [] }), + 'workspaces.current.tools.mcp.get': () => ({ data: [] }), + }) +}) vi.mock('@/app/components/plugins/marketplace/query', () => ({ useMarketplacePlugins: () => ({ data: undefined }), diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/__tests__/index.spec.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/__tests__/index.spec.tsx index 5cfda71a375..7365ebda700 100644 --- a/web/app/components/workflow/nodes/_base/components/workflow-panel/__tests__/index.spec.tsx +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/__tests__/index.spec.tsx @@ -207,11 +207,14 @@ vi.mock('@/app/components/workflow/run/hooks', () => ({ useLogs: () => mockLogsState, })) -vi.mock('@/service/use-tools', () => ({ - useAllBuiltInTools: () => ({ - data: mockBuiltInTools, - }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.tools.builtin.get': () => ({ + data: mockBuiltInTools, + }), + }) +}) vi.mock('@/service/use-triggers', () => ({ useAllTriggerPlugins: () => ({ diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx index 24b5391f58f..0c1bd954b35 100644 --- a/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx @@ -6,7 +6,7 @@ import { IconButton } from '@langgenius/dify-ui/icon-button' import { Tabs, TabsList, TabsPanel, TabsTab } from '@langgenius/dify-ui/tabs' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { RiCloseLine, RiPlayLargeLine } from '@remixicon/react' -import { useSuspenseQuery } from '@tanstack/react-query' +import { useQuery, useSuspenseQuery } from '@tanstack/react-query' import { debounce } from 'es-toolkit/compat' import { useQueryState } from 'nuqs' import * as React from 'react' @@ -28,6 +28,7 @@ import { } from '@/app/components/plugins/plugin-auth' import { usePluginStore } from '@/app/components/plugins/plugin-detail-panel/store' import { ReadmeEntrance } from '@/app/components/plugins/readme-panel/entrance' +import { selectToolsWithProvider } from '@/app/components/tools/utils/query-select' import BlockIcon from '@/app/components/workflow/block-icon' import { collaborationManager } from '@/app/components/workflow/collaboration/core/collaboration-manager' import { useCollaboration } from '@/app/components/workflow/collaboration/hooks/use-collaboration' @@ -48,7 +49,7 @@ import { } from '@/app/components/workflow/utils' import { userProfileQueryOptions } from '@/features/account-profile/client' import { useParams } from '@/next/navigation' -import { useAllBuiltInTools } from '@/service/use-tools' +import { consoleQuery } from '@/service/console' import { useAllTriggerPlugins } from '@/service/use-triggers' import { FlowType } from '@/types/common' import { useAvailableBlocks } from '../../../../hooks/use-available-blocks' @@ -361,7 +362,11 @@ const BasePanel: FC = ({ id, data, children }) => { }, [data.type, logParams]) const storeBuildInTools = useStore((s) => s.buildInTools) - const { data: buildInTools } = useAllBuiltInTools() + const { data: buildInTools } = useQuery( + consoleQuery.workspaces.current.tools.builtin.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) const currToolCollection = useMemo( () => getCurrentToolCollection(buildInTools, storeBuildInTools, data.provider_id), [buildInTools, storeBuildInTools, data.provider_id], diff --git a/web/app/components/workflow/nodes/_base/hooks/__tests__/use-one-step-run.spec.ts b/web/app/components/workflow/nodes/_base/hooks/__tests__/use-one-step-run.spec.ts index 4b30f53ab70..47fbad8a972 100644 --- a/web/app/components/workflow/nodes/_base/hooks/__tests__/use-one-step-run.spec.ts +++ b/web/app/components/workflow/nodes/_base/hooks/__tests__/use-one-step-run.spec.ts @@ -95,12 +95,15 @@ vi.mock('reactflow', () => ({ }), })) -vi.mock('@/service/use-tools', () => ({ - useAllBuiltInTools: () => ({ data: [] }), - useAllCustomTools: () => ({ data: [] }), - useAllWorkflowTools: () => ({ data: [] }), - useAllMCPTools: () => ({ data: [] }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.tools.builtin.get': () => ({ data: [] }), + 'workspaces.current.tools.api.get': () => ({ data: [] }), + 'workspaces.current.tools.workflow.get': () => ({ data: [] }), + 'workspaces.current.tools.mcp.get': () => ({ data: [] }), + }) +}) vi.mock('@/service/use-workflow', () => ({ useInvalidLastRun: () => vi.fn(), diff --git a/web/app/components/workflow/nodes/_base/hooks/use-one-step-run.ts b/web/app/components/workflow/nodes/_base/hooks/use-one-step-run.ts index c28c0c22e61..9a616c0eadc 100644 --- a/web/app/components/workflow/nodes/_base/hooks/use-one-step-run.ts +++ b/web/app/components/workflow/nodes/_base/hooks/use-one-step-run.ts @@ -8,6 +8,7 @@ import type { } from '@/app/components/workflow/types' import type { FlowType } from '@/types/common' import type { NodeRunResult, NodeTracing } from '@/types/workflow' +import { useQuery } from '@tanstack/react-query' import { unionBy } from 'es-toolkit/compat' import { noop } from 'es-toolkit/function' import { produce } from 'immer' @@ -16,6 +17,7 @@ import { useTranslation } from 'react-i18next' import { useStoreApi } from 'reactflow' import { trackEvent } from '@/app/components/base/amplitude' import { getInputVars as doGetInputVars } from '@/app/components/base/prompt-editor/constants' +import { selectToolsWithProvider } from '@/app/components/tools/utils/query-select' import { getNodeInfoById, isConversationVar, @@ -50,12 +52,7 @@ import { EVENT_WORKFLOW_STOP } from '@/app/components/workflow/variable-inspect/ import { toast } from '@/app/notifications' import { useEventEmitterContextContext } from '@/context/event-emitter' import { post, ssePost } from '@/service/base' -import { - useAllBuiltInTools, - useAllCustomTools, - useAllMCPTools, - useAllWorkflowTools, -} from '@/service/use-tools' +import { consoleQuery } from '@/service/console' import { useInvalidLastRun } from '@/service/use-workflow' import { fetchNodeInspectVars, @@ -171,10 +168,22 @@ const useOneStepRun = ({ const workflowStore = useWorkflowStore() const { schemaTypeDefinitions } = useMatchSchemaType() - const { data: buildInTools } = useAllBuiltInTools() - const { data: customTools } = useAllCustomTools() - const { data: workflowTools } = useAllWorkflowTools() - const { data: mcpTools } = useAllMCPTools() + const { data: buildInTools } = useQuery( + consoleQuery.workspaces.current.tools.builtin.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: customTools } = useQuery( + consoleQuery.workspaces.current.tools.api.get.queryOptions({ select: selectToolsWithProvider }), + ) + const { data: workflowTools } = useQuery( + consoleQuery.workspaces.current.tools.workflow.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: mcpTools } = useQuery( + consoleQuery.workspaces.current.tools.mcp.get.queryOptions({ select: selectToolsWithProvider }), + ) const getVar = (valueSelector: ValueSelector): Var | undefined => { const isSystem = valueSelector[0] === 'sys' diff --git a/web/app/components/workflow/nodes/agent/__tests__/integration.spec.tsx b/web/app/components/workflow/nodes/agent/__tests__/integration.spec.tsx index 3af81888d1b..c6a71bba7be 100644 --- a/web/app/components/workflow/nodes/agent/__tests__/integration.spec.tsx +++ b/web/app/components/workflow/nodes/agent/__tests__/integration.spec.tsx @@ -55,13 +55,6 @@ vi.mock('@langgenius/dify-ui/status-dot', () => ({ StatusDot: ({ status }: any) =>
{`indicator:${status}`}
, })) -vi.mock('@/service/use-tools', () => ({ - useAllBuiltInTools: () => ({ data: mockBuiltInTools }), - useAllCustomTools: () => ({ data: mockCustomTools }), - useAllWorkflowTools: () => ({ data: mockWorkflowTools }), - useAllMCPTools: () => ({ data: mockMcpTools }), -})) - vi.mock('@/utils/get-icon', () => ({ getIconFromMarketPlace: () => mockMarketplaceIcon, })) @@ -372,7 +365,8 @@ describe('agent path', () => { vi.mock('@tanstack/react-query', async (importOriginal) => { const actual = await importOriginal() - return { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + const reactQuery = { ...actual, useQuery: (options: { queryKey: OperationKey< @@ -391,5 +385,11 @@ vi.mock('@tanstack/react-query', async (importOriginal) => { if (modelType === ModelTypeEnum.textEmbedding) return { data: mockTextEmbeddingModels } return { data: mockTtsModels } }, - } + } as typeof actual + return mockConsoleOperationHooks(reactQuery, { + 'workspaces.current.tools.builtin.get': () => ({ data: mockBuiltInTools }), + 'workspaces.current.tools.api.get': () => ({ data: mockCustomTools }), + 'workspaces.current.tools.workflow.get': () => ({ data: mockWorkflowTools }), + 'workspaces.current.tools.mcp.get': () => ({ data: mockMcpTools }), + }) }) diff --git a/web/app/components/workflow/nodes/agent/components/__tests__/tool-icon.spec.tsx b/web/app/components/workflow/nodes/agent/components/__tests__/tool-icon.spec.tsx index 810a1771523..d645249e4ed 100644 --- a/web/app/components/workflow/nodes/agent/components/__tests__/tool-icon.spec.tsx +++ b/web/app/components/workflow/nodes/agent/components/__tests__/tool-icon.spec.tsx @@ -14,12 +14,15 @@ let mockWorkflowTools: ToolProvider[] | undefined let mockMcpTools: ToolProvider[] | undefined let mockMarketplaceIcon: string | { content: string; background: string } | undefined -vi.mock('@/service/use-tools', () => ({ - useAllBuiltInTools: () => ({ data: mockBuiltInTools }), - useAllCustomTools: () => ({ data: mockCustomTools }), - useAllWorkflowTools: () => ({ data: mockWorkflowTools }), - useAllMCPTools: () => ({ data: mockMcpTools }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.tools.builtin.get': () => ({ data: mockBuiltInTools }), + 'workspaces.current.tools.api.get': () => ({ data: mockCustomTools }), + 'workspaces.current.tools.workflow.get': () => ({ data: mockWorkflowTools }), + 'workspaces.current.tools.mcp.get': () => ({ data: mockMcpTools }), + }) +}) vi.mock('@langgenius/dify-ui/status-dot', () => ({ StatusDot: ({ status }: { status: string }) =>
{`indicator:${status}`}
, diff --git a/web/app/components/workflow/nodes/agent/components/tool-icon.tsx b/web/app/components/workflow/nodes/agent/components/tool-icon.tsx index 7aae1bdaa06..aa5397b782b 100644 --- a/web/app/components/workflow/nodes/agent/components/tool-icon.tsx +++ b/web/app/components/workflow/nodes/agent/components/tool-icon.tsx @@ -2,15 +2,12 @@ import type { ReactNode } from 'react' import { cn } from '@langgenius/dify-ui/cn' import { StatusDot } from '@langgenius/dify-ui/status-dot' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' +import { useQuery } from '@tanstack/react-query' import { memo, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import AppIcon from '@/app/components/base/app-icon' -import { - useAllBuiltInTools, - useAllCustomTools, - useAllMCPTools, - useAllWorkflowTools, -} from '@/service/use-tools' +import { selectToolsWithProvider } from '@/app/components/tools/utils/query-select' +import { consoleQuery } from '@/service/console' import { getIconFromMarketPlace } from '@/utils/get-icon' import { matchesProviderReference } from '@/utils/provider-reference' @@ -23,10 +20,22 @@ export type ToolIconProps = { export const ToolIcon = memo(({ providerName }: ToolIconProps) => { const containerRef = useRef(null) - const { data: buildInTools } = useAllBuiltInTools() - const { data: customTools } = useAllCustomTools() - const { data: workflowTools } = useAllWorkflowTools() - const { data: mcpTools } = useAllMCPTools() + const { data: buildInTools } = useQuery( + consoleQuery.workspaces.current.tools.builtin.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: customTools } = useQuery( + consoleQuery.workspaces.current.tools.api.get.queryOptions({ select: selectToolsWithProvider }), + ) + const { data: workflowTools } = useQuery( + consoleQuery.workspaces.current.tools.workflow.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: mcpTools } = useQuery( + consoleQuery.workspaces.current.tools.mcp.get.queryOptions({ select: selectToolsWithProvider }), + ) const isDataReady = !!buildInTools && !!customTools && !!workflowTools && !!mcpTools const currentProvider = useMemo(() => { const mergedTools = [ diff --git a/web/app/components/workflow/nodes/if-else/__tests__/integration.spec.tsx b/web/app/components/workflow/nodes/if-else/__tests__/integration.spec.tsx index 912cd0334a7..c48160ef89d 100644 --- a/web/app/components/workflow/nodes/if-else/__tests__/integration.spec.tsx +++ b/web/app/components/workflow/nodes/if-else/__tests__/integration.spec.tsx @@ -101,12 +101,15 @@ vi.mock('../../variable-assigner/hooks', () => ({ ], })) -vi.mock('@/service/use-tools', () => ({ - useAllBuiltInTools: () => ({ data: [] }), - useAllCustomTools: () => ({ data: [] }), - useAllWorkflowTools: () => ({ data: [] }), - useAllMCPTools: () => ({ data: [] }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.tools.builtin.get': () => ({ data: [] }), + 'workspaces.current.tools.api.get': () => ({ data: [] }), + 'workspaces.current.tools.workflow.get': () => ({ data: [] }), + 'workspaces.current.tools.mcp.get': () => ({ data: [] }), + }) +}) vi.mock('../use-config', () => ({ default: vi.fn(), diff --git a/web/app/components/workflow/nodes/if-else/components/condition-list/condition-item.tsx b/web/app/components/workflow/nodes/if-else/components/condition-list/condition-item.tsx index 666db4f05f3..b341795bc06 100644 --- a/web/app/components/workflow/nodes/if-else/components/condition-list/condition-item.tsx +++ b/web/app/components/workflow/nodes/if-else/components/condition-list/condition-item.tsx @@ -22,19 +22,16 @@ import { SelectTrigger, } from '@langgenius/dify-ui/select' import { RiDeleteBinLine } from '@remixicon/react' +import { useQuery } from '@tanstack/react-query' import { produce } from 'immer' import { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' +import { selectToolsWithProvider } from '@/app/components/tools/utils/query-select' import { getVarType } from '@/app/components/workflow/nodes/_base/components/variable/utils' import BoolValue from '@/app/components/workflow/panel/chat-variable-panel/components/bool-value' import { useWorkflowStore } from '@/app/components/workflow/store' import { VarType } from '@/app/components/workflow/types' -import { - useAllBuiltInTools, - useAllCustomTools, - useAllMCPTools, - useAllWorkflowTools, -} from '@/service/use-tools' +import { consoleQuery } from '@/service/console' import { useIsChatMode } from '../../../../hooks/use-workflow' import useMatchSchemaType from '../../../_base/components/variable/use-match-schema-type' import { FILE_TYPE_OPTIONS, SUB_VARIABLES, TRANSFER_METHOD } from '../../../constants' @@ -105,10 +102,22 @@ const ConditionItem = ({ const [isHovered, setIsHovered] = useState(false) const [open, setOpen] = useState(false) - const { data: buildInTools } = useAllBuiltInTools() - const { data: customTools } = useAllCustomTools() - const { data: workflowTools } = useAllWorkflowTools() - const { data: mcpTools } = useAllMCPTools() + const { data: buildInTools } = useQuery( + consoleQuery.workspaces.current.tools.builtin.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: customTools } = useQuery( + consoleQuery.workspaces.current.tools.api.get.queryOptions({ select: selectToolsWithProvider }), + ) + const { data: workflowTools } = useQuery( + consoleQuery.workspaces.current.tools.workflow.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: mcpTools } = useQuery( + consoleQuery.workspaces.current.tools.mcp.get.queryOptions({ select: selectToolsWithProvider }), + ) const workflowStore = useWorkflowStore() diff --git a/web/app/components/workflow/nodes/iteration/__tests__/use-config.spec.ts b/web/app/components/workflow/nodes/iteration/__tests__/use-config.spec.ts index 564ed804d75..f10ca6e2dcc 100644 --- a/web/app/components/workflow/nodes/iteration/__tests__/use-config.spec.ts +++ b/web/app/components/workflow/nodes/iteration/__tests__/use-config.spec.ts @@ -43,12 +43,15 @@ vi.mock('@/app/components/workflow/nodes/_base/hooks/use-node-crud', () => ({ default: (...args: unknown[]) => mockUseNodeCrud(...args), })) -vi.mock('@/service/use-tools', () => ({ - useAllBuiltInTools: () => mockUseAllBuiltInTools(), - useAllCustomTools: () => mockUseAllCustomTools(), - useAllWorkflowTools: () => mockUseAllWorkflowTools(), - useAllMCPTools: () => mockUseAllMCPTools(), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.tools.builtin.get': () => mockUseAllBuiltInTools(), + 'workspaces.current.tools.api.get': () => mockUseAllCustomTools(), + 'workspaces.current.tools.workflow.get': () => mockUseAllWorkflowTools(), + 'workspaces.current.tools.mcp.get': () => mockUseAllMCPTools(), + }) +}) vi.mock('@/app/components/workflow/nodes/_base/components/variable/utils', () => ({ toNodeOutputVars: (...args: unknown[]) => mockToNodeOutputVars(...args), diff --git a/web/app/components/workflow/nodes/iteration/use-config.ts b/web/app/components/workflow/nodes/iteration/use-config.ts index 4aa1d471c26..dfda7124a90 100644 --- a/web/app/components/workflow/nodes/iteration/use-config.ts +++ b/web/app/components/workflow/nodes/iteration/use-config.ts @@ -1,15 +1,12 @@ import type { ErrorHandleMode, ValueSelector, Var } from '../../types' import type { IterationNodeType } from './types' import type { VarKindType } from '@/app/components/workflow/nodes/_base/types' +import { useQuery } from '@tanstack/react-query' import { isEqual } from 'es-toolkit/predicate' import { produce } from 'immer' import { useCallback } from 'react' -import { - useAllBuiltInTools, - useAllCustomTools, - useAllMCPTools, - useAllWorkflowTools, -} from '@/service/use-tools' +import { selectToolsWithProvider } from '@/app/components/tools/utils/query-select' +import { consoleQuery } from '@/service/console' import useInspectVarsCrud from '../../hooks/use-inspect-vars-crud' import { useIsChatMode, useNodesReadOnly, useWorkflow } from '../../hooks/use-workflow' import { useStore } from '../../store' @@ -56,10 +53,22 @@ const useConfig = (id: string, payload: IterationNodeType) => { // output const { getIterationNodeChildren } = useWorkflow() const iterationChildrenNodes = getIterationNodeChildren(id) - const { data: buildInTools } = useAllBuiltInTools() - const { data: customTools } = useAllCustomTools() - const { data: workflowTools } = useAllWorkflowTools() - const { data: mcpTools } = useAllMCPTools() + const { data: buildInTools } = useQuery( + consoleQuery.workspaces.current.tools.builtin.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: customTools } = useQuery( + consoleQuery.workspaces.current.tools.api.get.queryOptions({ select: selectToolsWithProvider }), + ) + const { data: workflowTools } = useQuery( + consoleQuery.workspaces.current.tools.workflow.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: mcpTools } = useQuery( + consoleQuery.workspaces.current.tools.mcp.get.queryOptions({ select: selectToolsWithProvider }), + ) const dataSourceList = useStore((s) => s.dataSourceList) const allPluginInfoList = { buildInTools: buildInTools || [], diff --git a/web/app/components/workflow/nodes/loop/__tests__/use-config.spec.tsx b/web/app/components/workflow/nodes/loop/__tests__/use-config.spec.tsx index 84261702a96..a73d69813ce 100644 --- a/web/app/components/workflow/nodes/loop/__tests__/use-config.spec.tsx +++ b/web/app/components/workflow/nodes/loop/__tests__/use-config.spec.tsx @@ -27,12 +27,15 @@ vi.mock('@/app/components/workflow/store', () => ({ }), })) -vi.mock('@/service/use-tools', () => ({ - useAllBuiltInTools: () => ({ data: [] }), - useAllCustomTools: () => ({ data: [] }), - useAllWorkflowTools: () => ({ data: [] }), - useAllMCPTools: () => ({ data: [] }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.tools.builtin.get': () => ({ data: [] }), + 'workspaces.current.tools.api.get': () => ({ data: [] }), + 'workspaces.current.tools.workflow.get': () => ({ data: [] }), + 'workspaces.current.tools.mcp.get': () => ({ data: [] }), + }) +}) vi.mock('../../../hooks/use-workflow', async (importOriginal) => { const actual = await importOriginal() diff --git a/web/app/components/workflow/nodes/loop/use-config.ts b/web/app/components/workflow/nodes/loop/use-config.ts index 118e3095053..68eeeb013cd 100644 --- a/web/app/components/workflow/nodes/loop/use-config.ts +++ b/web/app/components/workflow/nodes/loop/use-config.ts @@ -9,14 +9,11 @@ import type { HandleUpdateSubVariableCondition, LoopNodeType, } from './types' +import { useQuery } from '@tanstack/react-query' import { useCallback, useEffect, useRef } from 'react' +import { selectToolsWithProvider } from '@/app/components/tools/utils/query-select' import { useStore } from '@/app/components/workflow/store' -import { - useAllBuiltInTools, - useAllCustomTools, - useAllMCPTools, - useAllWorkflowTools, -} from '@/service/use-tools' +import { consoleQuery } from '@/service/console' import { useIsChatMode, useNodesReadOnly, useWorkflow } from '../../hooks/use-workflow' import { toNodeOutputVars } from '../_base/components/variable/utils' import useNodeCrud from '../_base/hooks/use-node-crud' @@ -61,10 +58,22 @@ const useConfig = (id: string, payload: LoopNodeType) => { // output const { getLoopNodeChildren } = useWorkflow() const loopChildrenNodes = [{ id, data: payload } as any, ...getLoopNodeChildren(id)] - const { data: buildInTools } = useAllBuiltInTools() - const { data: customTools } = useAllCustomTools() - const { data: workflowTools } = useAllWorkflowTools() - const { data: mcpTools } = useAllMCPTools() + const { data: buildInTools } = useQuery( + consoleQuery.workspaces.current.tools.builtin.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: customTools } = useQuery( + consoleQuery.workspaces.current.tools.api.get.queryOptions({ select: selectToolsWithProvider }), + ) + const { data: workflowTools } = useQuery( + consoleQuery.workspaces.current.tools.workflow.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: mcpTools } = useQuery( + consoleQuery.workspaces.current.tools.mcp.get.queryOptions({ select: selectToolsWithProvider }), + ) const dataSourceList = useStore((s) => s.dataSourceList) const allPluginInfoList = { buildInTools: buildInTools || [], diff --git a/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/__tests__/import-from-tool.spec.tsx b/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/__tests__/import-from-tool.spec.tsx index a65eb3e3ba2..dda0e6953eb 100644 --- a/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/__tests__/import-from-tool.spec.tsx +++ b/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/__tests__/import-from-tool.spec.tsx @@ -32,13 +32,15 @@ const mockToolCollections = vi.hoisted(() => ({ workflow: [] as ToolWithProvider[], })) -vi.mock('@/service/use-tools', () => ({ - useAllBuiltInTools: () => ({ data: mockToolCollections.builtIn }), - useAllCustomTools: () => ({ data: mockToolCollections.custom }), - useAllWorkflowTools: () => ({ data: mockToolCollections.workflow }), - useAllMCPTools: () => ({ data: [] }), - useInvalidateAllBuiltInTools: () => vi.fn(), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.tools.builtin.get': () => ({ data: mockToolCollections.builtIn }), + 'workspaces.current.tools.api.get': () => ({ data: mockToolCollections.custom }), + 'workspaces.current.tools.workflow.get': () => ({ data: mockToolCollections.workflow }), + 'workspaces.current.tools.mcp.get': () => ({ data: [] }), + }) +}) vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({ useLanguage: () => 'en_US', diff --git a/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/import-from-tool.tsx b/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/import-from-tool.tsx index 61e4f2e1c54..d827dc07b37 100644 --- a/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/import-from-tool.tsx +++ b/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/import-from-tool.tsx @@ -8,11 +8,13 @@ import type { } from '@/app/components/workflow/block-selector/types' import type { BlockEnum } from '@/app/components/workflow/types' import { Button } from '@langgenius/dify-ui/button' +import { useQuery } from '@tanstack/react-query' import { memo, useCallback } from 'react' import { useTranslation } from 'react-i18next' import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks' import { CollectionType } from '@/app/components/tools/types' -import { useAllBuiltInTools, useAllCustomTools, useAllWorkflowTools } from '@/service/use-tools' +import { selectToolsWithProvider } from '@/app/components/tools/utils/query-select' +import { consoleQuery } from '@/service/console' import { matchesProviderReference } from '@/utils/provider-reference' import BlockSelector from '../../../../block-selector' import { TabType } from '../../../../block-selector/types' @@ -38,9 +40,19 @@ const ImportFromTool: FC = ({ onImport }) => { const { t } = useTranslation(['workflowModels']) const language = useLanguage() - const { data: buildInTools } = useAllBuiltInTools() - const { data: customTools } = useAllCustomTools() - const { data: workflowTools } = useAllWorkflowTools() + const { data: buildInTools } = useQuery( + consoleQuery.workspaces.current.tools.builtin.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: customTools } = useQuery( + consoleQuery.workspaces.current.tools.api.get.queryOptions({ select: selectToolsWithProvider }), + ) + const { data: workflowTools } = useQuery( + consoleQuery.workspaces.current.tools.workflow.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) const handleSelectTool = useCallback( (_type: BlockEnum, toolInfo?: BlockDefaultValue) => { diff --git a/web/app/components/workflow/nodes/tool/hooks/__tests__/use-config.spec.tsx b/web/app/components/workflow/nodes/tool/hooks/__tests__/use-config.spec.tsx index 4c5eb618e81..9b4268ac895 100644 --- a/web/app/components/workflow/nodes/tool/hooks/__tests__/use-config.spec.tsx +++ b/web/app/components/workflow/nodes/tool/hooks/__tests__/use-config.spec.tsx @@ -3,8 +3,13 @@ import { renderHook } from '@testing-library/react' import { CollectionType } from '@/app/components/tools/types' import { VarKindType } from '@/app/components/workflow/nodes/_base/types' import { BlockEnum } from '@/app/components/workflow/types' +import { createQueryClientWrapper } from '@/test/console/query-client' +import { createTestQueryClient } from '@/test/query-client' import useConfig from '../use-config' +const queryClient = createTestQueryClient() +const queryClientWrapper = createQueryClientWrapper(queryClient) + const mockSetInputs = vi.hoisted(() => vi.fn()) const mockSetControlPromptEditorRerenderKey = vi.hoisted(() => vi.fn()) const mockUseCurrentToolCollection = vi.hoisted(() => vi.fn()) @@ -41,10 +46,6 @@ vi.mock('@/service/tools', () => ({ updateBuiltInToolCredential: vi.fn(), })) -vi.mock('@/service/use-tools', () => ({ - useInvalidToolsByType: () => vi.fn(), -})) - vi.mock('@/app/components/workflow/store', () => ({ useWorkflowStore: () => ({ getState: () => ({ @@ -162,6 +163,7 @@ describe('useConfig', () => { const { rerender } = renderHook(({ payload }) => useConfig('tool-node-1', payload), { initialProps: { payload: emptyPayload }, + wrapper: queryClientWrapper, }) expect(mockSetInputs).toHaveBeenCalledTimes(1) @@ -178,14 +180,16 @@ describe('useConfig', () => { }) it('should not update inputs when tool values are already populated on first render', () => { - renderHook(() => - useConfig( - 'tool-node-1', - createNodeData({ - tool_parameters: { query: createToolVarInput('existing query') }, - tool_configurations: { api_key: createToolVarInput('existing secret') }, - }), - ), + renderHook( + () => + useConfig( + 'tool-node-1', + createNodeData({ + tool_parameters: { query: createToolVarInput('existing query') }, + tool_configurations: { api_key: createToolVarInput('existing secret') }, + }), + ), + { wrapper: queryClientWrapper }, ) expect(mockSetInputs).not.toHaveBeenCalled() @@ -202,7 +206,7 @@ describe('useConfig', () => { }) mockGetConfiguredValue.mockReturnValue({}) - renderHook(() => useConfig('tool-node-1', createNodeData())) + renderHook(() => useConfig('tool-node-1', createNodeData()), { wrapper: queryClientWrapper }) expect(mockSetInputs).not.toHaveBeenCalled() }) diff --git a/web/app/components/workflow/nodes/tool/hooks/__tests__/use-current-tool-collection.spec.tsx b/web/app/components/workflow/nodes/tool/hooks/__tests__/use-current-tool-collection.spec.tsx index 3e0b36ecb9c..c0ed4709e0a 100644 --- a/web/app/components/workflow/nodes/tool/hooks/__tests__/use-current-tool-collection.spec.tsx +++ b/web/app/components/workflow/nodes/tool/hooks/__tests__/use-current-tool-collection.spec.tsx @@ -9,12 +9,15 @@ const mockUseAllWorkflowTools = vi.hoisted(() => vi.fn()) const mockUseAllMCPTools = vi.hoisted(() => vi.fn()) const mockCanFindTool = vi.hoisted(() => vi.fn()) -vi.mock('@/service/use-tools', () => ({ - useAllBuiltInTools: () => mockUseAllBuiltInTools(), - useAllCustomTools: () => mockUseAllCustomTools(), - useAllWorkflowTools: () => mockUseAllWorkflowTools(), - useAllMCPTools: () => mockUseAllMCPTools(), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.tools.builtin.get': () => mockUseAllBuiltInTools(), + 'workspaces.current.tools.api.get': () => mockUseAllCustomTools(), + 'workspaces.current.tools.workflow.get': () => mockUseAllWorkflowTools(), + 'workspaces.current.tools.mcp.get': () => mockUseAllMCPTools(), + }) +}) vi.mock('@/utils', () => ({ canFindTool: (...args: unknown[]) => mockCanFindTool(...args), diff --git a/web/app/components/workflow/nodes/tool/hooks/use-config.ts b/web/app/components/workflow/nodes/tool/hooks/use-config.ts index a782820cb07..df389818d35 100644 --- a/web/app/components/workflow/nodes/tool/hooks/use-config.ts +++ b/web/app/components/workflow/nodes/tool/hooks/use-config.ts @@ -1,6 +1,7 @@ import type { ToolNodeType } from '../types' import type { ResourceVarInputs } from '@/app/components/workflow/nodes/_base/types' import type { InputVar } from '@/app/components/workflow/types' +import { useQueryClient } from '@tanstack/react-query' import { capitalize } from 'es-toolkit/string' import { produce } from 'immer' import { useCallback, useEffect, useMemo, useState } from 'react' @@ -10,10 +11,10 @@ import { getConfiguredValue, toolParametersToFormSchemas, } from '@/app/components/tools/utils/to-form-schema' +import { getToolListQueryKey } from '@/app/components/tools/utils/tool-list-query-key' import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud' import { useWorkflowStore } from '@/app/components/workflow/store' import { updateBuiltInToolCredential } from '@/service/tools' -import { useInvalidToolsByType } from '@/service/use-tools' import { useNodesReadOnly } from '../../../hooks/use-workflow' import { isToolAuthorizationRequired } from '../auth' import { normalizeJsonSchemaType } from '../output-schema-utils' @@ -42,15 +43,16 @@ const useConfig = (id: string, payload: ToolNodeType) => { const isShowAuthBtn = isToolAuthorizationRequired(provider_type, currCollection) const [showSetAuth, setShowSetAuth] = useState(false) - const invalidToolsByType = useInvalidToolsByType(provider_type) + const queryClient = useQueryClient() const handleSaveAuth = useCallback( async (value: any) => { await updateBuiltInToolCredential(currCollection?.name as string, value) - invalidToolsByType() + const toolListQueryKey = getToolListQueryKey(provider_type) + if (toolListQueryKey) queryClient.invalidateQueries({ queryKey: toolListQueryKey }) setShowSetAuth(false) }, - [currCollection?.name, invalidToolsByType], + [currCollection?.name, provider_type, queryClient], ) const currTool = useMemo(() => { diff --git a/web/app/components/workflow/nodes/tool/hooks/use-current-tool-collection.ts b/web/app/components/workflow/nodes/tool/hooks/use-current-tool-collection.ts index 87199877414..b792a67bb7b 100644 --- a/web/app/components/workflow/nodes/tool/hooks/use-current-tool-collection.ts +++ b/web/app/components/workflow/nodes/tool/hooks/use-current-tool-collection.ts @@ -1,23 +1,32 @@ import type { ToolNodeType } from '../types' import type { ToolWithProvider } from '@/app/components/workflow/types' +import { useQuery } from '@tanstack/react-query' import { useMemo } from 'react' import { CollectionType } from '@/app/components/tools/types' -import { - useAllBuiltInTools, - useAllCustomTools, - useAllMCPTools, - useAllWorkflowTools, -} from '@/service/use-tools' +import { selectToolsWithProvider } from '@/app/components/tools/utils/query-select' +import { consoleQuery } from '@/service/console' import { matchesProviderReference } from '@/utils/provider-reference' const useCurrentToolCollection = ( providerType: ToolNodeType['provider_type'], providerId: string, ) => { - const { data: buildInTools } = useAllBuiltInTools() - const { data: customTools } = useAllCustomTools() - const { data: workflowTools } = useAllWorkflowTools() - const { data: mcpTools } = useAllMCPTools() + const { data: buildInTools } = useQuery( + consoleQuery.workspaces.current.tools.builtin.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: customTools } = useQuery( + consoleQuery.workspaces.current.tools.api.get.queryOptions({ select: selectToolsWithProvider }), + ) + const { data: workflowTools } = useQuery( + consoleQuery.workspaces.current.tools.workflow.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: mcpTools } = useQuery( + consoleQuery.workspaces.current.tools.mcp.get.queryOptions({ select: selectToolsWithProvider }), + ) const currentTools = useMemo(() => { switch (providerType) { diff --git a/web/app/components/workflow/operator/__tests__/add-block.spec.tsx b/web/app/components/workflow/operator/__tests__/add-block.spec.tsx index 6a08533e79e..61d00afba3b 100644 --- a/web/app/components/workflow/operator/__tests__/add-block.spec.tsx +++ b/web/app/components/workflow/operator/__tests__/add-block.spec.tsx @@ -105,13 +105,15 @@ vi.mock('@/service/use-triggers', () => ({ useInvalidateAllTriggerPlugins: () => vi.fn(), })) -vi.mock('@/service/use-tools', () => ({ - useAllBuiltInTools: () => ({ data: [] }), - useAllCustomTools: () => ({ data: [] }), - useAllWorkflowTools: () => ({ data: [] }), - useAllMCPTools: () => ({ data: [] }), - useInvalidateAllBuiltInTools: () => vi.fn(), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.tools.builtin.get': () => ({ data: [] }), + 'workspaces.current.tools.api.get': () => ({ data: [] }), + 'workspaces.current.tools.workflow.get': () => ({ data: [] }), + 'workspaces.current.tools.mcp.get': () => ({ data: [] }), + }) +}) vi.mock('@/app/components/plugins/marketplace/query', () => ({ useMarketplacePlugins: () => ({ data: undefined }), diff --git a/web/app/components/workflow/shortcuts/__tests__/workflow-canvas.browser.spec.tsx b/web/app/components/workflow/shortcuts/__tests__/workflow-canvas.browser.spec.tsx index fc04d89d9af..f92bfdbfe89 100644 --- a/web/app/components/workflow/shortcuts/__tests__/workflow-canvas.browser.spec.tsx +++ b/web/app/components/workflow/shortcuts/__tests__/workflow-canvas.browser.spec.tsx @@ -94,7 +94,12 @@ vi.mock('../../hooks/use-nodes-meta-data', () => ({ author: 'Dify', }), })) -vi.mock('@/service/use-tools', () => ({ useAllWorkflowTools: () => ({ data: [] }) })) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.tools.workflow.get': () => ({ data: [] }), + }) +}) vi.mock('../../node-actions-menu/change-block-popup', () => ({ ChangeBlockPopup: () => null })) vi.mock('@/context/permission-state', async () => { diff --git a/web/features/agent-v2/agent-detail/configure/__tests__/use-agent-configure-sync.spec.tsx b/web/features/agent-v2/agent-detail/configure/__tests__/use-agent-configure-sync.spec.tsx index 2c153d9b912..3cead400987 100644 --- a/web/features/agent-v2/agent-detail/configure/__tests__/use-agent-configure-sync.spec.tsx +++ b/web/features/agent-v2/agent-detail/configure/__tests__/use-agent-configure-sync.spec.tsx @@ -183,59 +183,75 @@ vi.mock('@/service/use-plugins', () => ({ }), })) -vi.mock('@/service/use-tools', () => ({ - useAllBuiltInTools: () => ({ data: toolProviderState.builtInTools }), - useAllCustomTools: () => ({ data: toolProviderState.customTools }), - useAllMCPTools: () => ({ data: toolProviderState.mcpTools }), - useAllWorkflowTools: () => ({ data: toolProviderState.workflowTools }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.tools.builtin.get': () => ({ data: toolProviderState.builtInTools }), + 'workspaces.current.tools.api.get': () => ({ data: toolProviderState.customTools }), + 'workspaces.current.tools.mcp.get': () => ({ data: toolProviderState.mcpTools }), + 'workspaces.current.tools.workflow.get': () => ({ data: toolProviderState.workflowTools }), + }) +}) -vi.mock('@/service/console', () => ({ - consoleQuery: { - agent: { - get: { - key: () => ['agents'], +vi.mock('@/service/console', async () => { + const { consoleOperationStub } = await import('@/test/console/operation-hooks') + return { + consoleQuery: { + workspaces: { + current: { + tools: { + builtin: { get: consoleOperationStub('workspaces.current.tools.builtin.get') }, + api: { get: consoleOperationStub('workspaces.current.tools.api.get') }, + workflow: { get: consoleOperationStub('workspaces.current.tools.workflow.get') }, + mcp: { get: consoleOperationStub('workspaces.current.tools.mcp.get') }, + }, + }, }, - byAgentId: { + agent: { get: { - queryKey: ({ input }: { input: { params: { agent_id: string } } }) => [ - 'agent-detail', - input.params.agent_id, - ], + key: () => ['agents'], }, - apiAccess: { + byAgentId: { get: { queryKey: ({ input }: { input: { params: { agent_id: string } } }) => [ - 'agent-api-access', + 'agent-detail', input.params.agent_id, ], }, - }, - composer: { - get: { - queryKey: ({ input }: { input: { params: { agent_id: string } } }) => [ - 'agent-composer', - input.params.agent_id, - ], + apiAccess: { + get: { + queryKey: ({ input }: { input: { params: { agent_id: string } } }) => [ + 'agent-api-access', + input.params.agent_id, + ], + }, }, - put: { - mutationOptions: composerPutMutationOptions, + composer: { + get: { + queryKey: ({ input }: { input: { params: { agent_id: string } } }) => [ + 'agent-composer', + input.params.agent_id, + ], + }, + put: { + mutationOptions: composerPutMutationOptions, + }, }, - }, - publish: { - post: { - mutationOptions: publishAgentMutationOptions, + publish: { + post: { + mutationOptions: publishAgentMutationOptions, + }, }, - }, - versions: { - get: { - key: () => ['agent-versions'], + versions: { + get: { + key: () => ['agent-versions'], + }, }, }, }, }, - }, -})) + } +}) function renderUseAgentConfigureSync({ agentName = 'Agent', diff --git a/web/features/agent-v2/agent-detail/configure/components/__tests__/agent-prompt-editor.spec.tsx b/web/features/agent-v2/agent-detail/configure/components/__tests__/agent-prompt-editor.spec.tsx index b5d1246bdd9..64df485a1f5 100644 --- a/web/features/agent-v2/agent-detail/configure/components/__tests__/agent-prompt-editor.spec.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/__tests__/agent-prompt-editor.spec.tsx @@ -230,12 +230,15 @@ vi.mock('@/context/workspace-state', async () => { })) }) -vi.mock('@/service/use-tools', () => ({ - useAllBuiltInTools: () => ({ data: mockBuiltInTools }), - useAllCustomTools: () => ({ data: [] }), - useAllWorkflowTools: () => ({ data: [] }), - useAllMCPTools: () => ({ data: mockMCPTools.current }), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.tools.builtin.get': () => ({ data: mockBuiltInTools }), + 'workspaces.current.tools.api.get': () => ({ data: [] }), + 'workspaces.current.tools.workflow.get': () => ({ data: [] }), + 'workspaces.current.tools.mcp.get': () => ({ data: mockMCPTools.current }), + }) +}) vi.mock('@/hooks/use-theme', () => ({ default: () => ({ theme: 'light' }), diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/prompt-editor/hooks.ts b/web/features/agent-v2/agent-detail/configure/components/orchestrate/prompt-editor/hooks.ts index 330964fe154..b8fdb5eb45e 100644 --- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/prompt-editor/hooks.ts +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/prompt-editor/hooks.ts @@ -2,18 +2,15 @@ import type { ToolWithProvider } from '@/app/components/workflow/types' import type { AgentTool } from '@/features/agent-v2/agent-composer/form-state' +import { useQuery } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import { useMemo } from 'react' import { CollectionType } from '@/app/components/tools/types' +import { selectToolsWithProvider } from '@/app/components/tools/utils/query-select' import { API_PREFIX } from '@/config' import { currentWorkspaceIdAtom } from '@/context/workspace-state' import useTheme from '@/hooks/use-theme' -import { - useAllBuiltInTools, - useAllCustomTools, - useAllMCPTools, - useAllWorkflowTools, -} from '@/service/use-tools' +import { consoleQuery } from '@/service/console' import { Theme } from '@/types/app' import { basePath } from '@/utils/var' import { @@ -50,16 +47,31 @@ export function useAgentPromptToolIconResolver( ) { const { theme } = useTheme() const currentWorkspaceId = useAtomValue(currentWorkspaceIdAtom) - const { data: builtInTools } = useAllBuiltInTools( - !providerTypes || providerTypes.has(CollectionType.builtIn) || providerTypes.has('plugin'), + const { data: builtInTools } = useQuery( + consoleQuery.workspaces.current.tools.builtin.get.queryOptions({ + enabled: + !providerTypes || providerTypes.has(CollectionType.builtIn) || providerTypes.has('plugin'), + select: selectToolsWithProvider, + }), ) - const { data: customTools } = useAllCustomTools( - !providerTypes || providerTypes.has(CollectionType.custom), + const { data: customTools } = useQuery( + consoleQuery.workspaces.current.tools.api.get.queryOptions({ + enabled: !providerTypes || providerTypes.has(CollectionType.custom), + select: selectToolsWithProvider, + }), ) - const { data: workflowTools } = useAllWorkflowTools( - !providerTypes || providerTypes.has(CollectionType.workflow), + const { data: workflowTools } = useQuery( + consoleQuery.workspaces.current.tools.workflow.get.queryOptions({ + enabled: !providerTypes || providerTypes.has(CollectionType.workflow), + select: selectToolsWithProvider, + }), + ) + const { data: mcpTools } = useQuery( + consoleQuery.workspaces.current.tools.mcp.get.queryOptions({ + enabled: !providerTypes || providerTypes.has(CollectionType.mcp), + select: selectToolsWithProvider, + }), ) - const { data: mcpTools } = useAllMCPTools(!providerTypes || providerTypes.has(CollectionType.mcp)) const providerById = useMemo( () => diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/prompt-editor/slash.tsx b/web/features/agent-v2/agent-detail/configure/components/orchestrate/prompt-editor/slash.tsx index 54647fa3d2a..1539ca1b098 100644 --- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/prompt-editor/slash.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/prompt-editor/slash.tsx @@ -15,23 +15,20 @@ import type { import type { AgentProviderToolDefaultValue } from '@/features/agent-v2/agent-composer/store-modules/tools' import { cn } from '@langgenius/dify-ui/cn' import { FileTreeIcon } from '@langgenius/dify-ui/file-tree' +import { useQuery } from '@tanstack/react-query' import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { getMarketplaceCategoryUrl } from '@/app/components/plugins/marketplace/utils' import { PluginCategoryEnum } from '@/app/components/plugins/types' import { parseToolProviderType } from '@/app/components/tools/provider-type' import { CollectionType } from '@/app/components/tools/types' +import { selectToolsWithProvider } from '@/app/components/tools/utils/query-select' import BlockIcon from '@/app/components/workflow/block-icon' import { ToolType } from '@/app/components/workflow/block-selector/types' import { BlockEnum } from '@/app/components/workflow/types' import { useGetLanguage } from '@/context/i18n' import { ENABLE_AGENT_CLI_TOOLS } from '@/features/agent-v2/agent-detail/configure/feature-flags' -import { - useAllBuiltInTools, - useAllCustomTools, - useAllMCPTools, - useAllWorkflowTools, -} from '@/service/use-tools' +import { consoleQuery } from '@/service/console' import { getProviderReference, matchesProviderReference } from '@/utils/provider-reference' import { useAgentPromptToolIconResolver } from './hooks' @@ -362,10 +359,22 @@ function AgentPromptToolRows({ const { getProviderIcon, getProviderIcons } = useAgentPromptToolIconResolver() const [activeTab, setActiveTab] = useState('all') const [expandedProviderIds, setExpandedProviderIds] = useState>(() => new Set()) - const { data: builtInTools = [] } = useAllBuiltInTools() - const { data: customTools = [] } = useAllCustomTools() - const { data: workflowTools = [] } = useAllWorkflowTools() - const { data: mcpTools = [] } = useAllMCPTools() + const { data: builtInTools = [] } = useQuery( + consoleQuery.workspaces.current.tools.builtin.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: customTools = [] } = useQuery( + consoleQuery.workspaces.current.tools.api.get.queryOptions({ select: selectToolsWithProvider }), + ) + const { data: workflowTools = [] } = useQuery( + consoleQuery.workspaces.current.tools.workflow.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: mcpTools = [] } = useQuery( + consoleQuery.workspaces.current.tools.mcp.get.queryOptions({ select: selectToolsWithProvider }), + ) const configuredCliTools = ENABLE_AGENT_CLI_TOOLS ? configuredTools.filter((tool) => tool.kind === 'cli') : [] diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/tools/__tests__/index.spec.tsx b/web/features/agent-v2/agent-detail/configure/components/orchestrate/tools/__tests__/index.spec.tsx index 276e198dcd4..b6837be6ec2 100644 --- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/tools/__tests__/index.spec.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/tools/__tests__/index.spec.tsx @@ -15,6 +15,7 @@ import { agentComposerSavedDraftAtom, isAgentComposerDirtyAtom, } from '@/features/agent-v2/agent-composer/store' +import { consoleQuery } from '@/service/console' import { seedAccountProfileQuery } from '@/test/console/account-profile' import { AgentOrchestrateReadOnlyContext, @@ -45,7 +46,6 @@ const pluginInstallState = vi.hoisted(() => ({ latest_package_identifier: string } | undefined, - invalidateBuiltInTools: vi.fn(), invalidateInstalledPluginList: vi.fn(), })) @@ -158,14 +158,15 @@ vi.mock('@/app/components/header/account-setting/model-provider-page/model-modal ), })) -vi.mock('@/service/use-tools', () => ({ - useAllBuiltInTools: () => ({ data: toolProviderState.builtInTools }), - useAllCustomTools: () => ({ data: toolProviderState.customTools }), - useAllWorkflowTools: () => ({ data: [] }), - useAllMCPTools: () => ({ data: [] }), - useInvalidateAllBuiltInTools: () => pluginInstallState.invalidateBuiltInTools, - useInvalidToolsByType: () => vi.fn(), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const { mockConsoleOperationHooks } = await import('@/test/console/operation-hooks') + return mockConsoleOperationHooks(await importOriginal(), { + 'workspaces.current.tools.builtin.get': () => ({ data: toolProviderState.builtInTools }), + 'workspaces.current.tools.api.get': () => ({ data: toolProviderState.customTools }), + 'workspaces.current.tools.workflow.get': () => ({ data: [] }), + 'workspaces.current.tools.mcp.get': () => ({ data: [] }), + }) +}) const agentToolsDraft = { ...defaultAgentSoulConfigFormState, @@ -452,13 +453,14 @@ function renderAgentTools(initialDraft: AgentSoulConfigFormState = agentToolsDra }) seedAccountProfileQuery(queryClient, { id: 'user-1' }) - return render( + const view = render( , ) + return { ...view, queryClient } } function renderAgentToolsWithStore(initialDraft: AgentSoulConfigFormState = agentToolsDraft) { @@ -531,7 +533,6 @@ describe('AgentTools', () => { pluginAuthState.notAllowCustomCredential = false pluginInstallState.manifest = undefined pluginInstallState.fallbackManifest = undefined - pluginInstallState.invalidateBuiltInTools.mockResolvedValue(undefined) pluginInstallState.invalidateInstalledPluginList.mockResolvedValue(undefined) }) @@ -727,7 +728,9 @@ describe('AgentTools', () => { }, latest_package_identifier: 'langgenius/google:1.0.0@checksum', } - renderAgentTools(reflectedUninstalledPluginDraft) + const { queryClient } = renderAgentTools(reflectedUninstalledPluginDraft) + const builtInToolsKey = consoleQuery.workspaces.current.tools.builtin.get.queryKey() + queryClient.setQueryData(builtInToolsKey, []) expect(screen.getByRole('button', { name: 'Google Tools' })).toBeInTheDocument() expect( @@ -750,7 +753,7 @@ describe('AgentTools', () => { await user.click(installButton) await waitFor(() => { - expect(pluginInstallState.invalidateBuiltInTools).toHaveBeenCalledTimes(1) + expect(queryClient.getQueryState(builtInToolsKey)?.isInvalidated).toBe(true) expect(pluginInstallState.invalidateInstalledPluginList).toHaveBeenCalledTimes(1) }) }) diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/tools/index.tsx b/web/features/agent-v2/agent-detail/configure/components/orchestrate/tools/index.tsx index 9e0efc56250..df35738f0bb 100644 --- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/tools/index.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/tools/index.tsx @@ -12,6 +12,7 @@ import type { import type { AgentProviderToolDefaultValue } from '@/features/agent-v2/agent-composer/store-modules/tools' import { cn } from '@langgenius/dify-ui/cn' import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover' +import { useQuery, useQueryClient } from '@tanstack/react-query' import { noop } from 'es-toolkit/function' import { useAtomValue, useSetAtom } from 'jotai' import { memo, useCallback, useId, useMemo, useRef, useState } from 'react' @@ -19,6 +20,7 @@ import { useTranslation } from 'react-i18next' import { PluginCategoryEnum } from '@/app/components/plugins/types' import { parseToolProviderType } from '@/app/components/tools/provider-type' import { CollectionType } from '@/app/components/tools/types' +import { selectToolsWithProvider } from '@/app/components/tools/utils/query-select' import { ToolPickerContent } from '@/app/components/workflow/block-selector/tool-picker' import { useGetLanguage } from '@/context/i18n' import { @@ -27,14 +29,8 @@ import { setProviderToolCredentialAtom, } from '@/features/agent-v2/agent-composer/store-modules/tools' import { ENABLE_AGENT_CLI_TOOLS } from '@/features/agent-v2/agent-detail/configure/feature-flags' +import { consoleQuery } from '@/service/console' import { useInvalidateInstalledPluginList } from '@/service/use-plugins' -import { - useAllBuiltInTools, - useAllCustomTools, - useAllMCPTools, - useAllWorkflowTools, - useInvalidateAllBuiltInTools, -} from '@/service/use-tools' import { createAgentToolProviderCatalog, getAgentProviderPluginId, @@ -398,7 +394,7 @@ export function AgentTools() { const { t } = useTranslation(['agentV2']) const readOnly = useAgentOrchestrateReadOnly() const setProviderToolCredential = useSetAtom(setProviderToolCredentialAtom) - const invalidateAllBuiltInTools = useInvalidateAllBuiltInTools() + const queryClient = useQueryClient() const invalidateInstalledPluginList = useInvalidateInstalledPluginList() const { providerById, resolvedProviderTypes } = useAgentToolProviderCatalog() const tools = useAtomValue(agentComposerToolsAtom) @@ -432,10 +428,12 @@ export function AgentTools() { ) const handlePluginInstalled = useCallback(() => { void Promise.allSettled([ - invalidateAllBuiltInTools(), + queryClient.invalidateQueries({ + queryKey: consoleQuery.workspaces.current.tools.builtin.get.queryKey(), + }), invalidateInstalledPluginList(PluginCategoryEnum.tool), ]) - }, [invalidateAllBuiltInTools, invalidateInstalledPluginList]) + }, [invalidateInstalledPluginList, queryClient]) const visibleTools = useMemo( () => (ENABLE_AGENT_CLI_TOOLS ? tools : tools.filter((tool) => tool.kind !== 'cli')), [tools], @@ -603,12 +601,30 @@ export function AgentTemplateTools() { const providerTypes = new Set( visibleTools.filter((tool) => tool.kind === 'provider').map((tool) => tool.providerType), ) - const { data: buildInTools } = useAllBuiltInTools( - providerTypes.has(CollectionType.builtIn) || providerTypes.has('plugin'), + const { data: buildInTools } = useQuery( + consoleQuery.workspaces.current.tools.builtin.get.queryOptions({ + enabled: providerTypes.has(CollectionType.builtIn) || providerTypes.has('plugin'), + select: selectToolsWithProvider, + }), + ) + const { data: customTools } = useQuery( + consoleQuery.workspaces.current.tools.api.get.queryOptions({ + enabled: providerTypes.has(CollectionType.custom), + select: selectToolsWithProvider, + }), + ) + const { data: workflowTools } = useQuery( + consoleQuery.workspaces.current.tools.workflow.get.queryOptions({ + enabled: providerTypes.has(CollectionType.workflow), + select: selectToolsWithProvider, + }), + ) + const { data: mcpTools } = useQuery( + consoleQuery.workspaces.current.tools.mcp.get.queryOptions({ + enabled: providerTypes.has(CollectionType.mcp), + select: selectToolsWithProvider, + }), ) - const { data: customTools } = useAllCustomTools(providerTypes.has(CollectionType.custom)) - const { data: workflowTools } = useAllWorkflowTools(providerTypes.has(CollectionType.workflow)) - const { data: mcpTools } = useAllMCPTools(providerTypes.has(CollectionType.mcp)) const catalog = useMemo( () => createAgentToolProviderCatalog({ buildInTools, customTools, workflowTools, mcpTools }), [buildInTools, customTools, workflowTools, mcpTools], diff --git a/web/features/agent-v2/agent-detail/configure/tool-provider-catalog.ts b/web/features/agent-v2/agent-detail/configure/tool-provider-catalog.ts index 51a4b7e3a26..68261d7ae79 100644 --- a/web/features/agent-v2/agent-detail/configure/tool-provider-catalog.ts +++ b/web/features/agent-v2/agent-detail/configure/tool-provider-catalog.ts @@ -3,16 +3,13 @@ import type { MarketplacePlugin } from '@dify/contracts/marketplace' import type { ToolWithProvider } from '@/app/components/workflow/types' import type { AgentProviderTool, AgentTool } from '@/features/agent-v2/agent-composer/form-state' +import { useQuery } from '@tanstack/react-query' import { useMemo } from 'react' import { CollectionType } from '@/app/components/tools/types' +import { selectToolsWithProvider } from '@/app/components/tools/utils/query-select' import { useGetLanguage } from '@/context/i18n' +import { consoleQuery } from '@/service/console' import { useFetchPluginsInMarketPlaceByInfo } from '@/service/use-plugins' -import { - useAllBuiltInTools, - useAllCustomTools, - useAllMCPTools, - useAllWorkflowTools, -} from '@/service/use-tools' import { getIconFromMarketPlace } from '@/utils/get-icon' import { getProviderReference } from '@/utils/provider-reference' @@ -73,10 +70,22 @@ export function createAgentToolProviderCatalog({ } export function useAgentToolProviderCatalog(): AgentToolProviderCatalog { - const { data: buildInTools } = useAllBuiltInTools() - const { data: customTools } = useAllCustomTools() - const { data: workflowTools } = useAllWorkflowTools() - const { data: mcpTools } = useAllMCPTools() + const { data: buildInTools } = useQuery( + consoleQuery.workspaces.current.tools.builtin.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: customTools } = useQuery( + consoleQuery.workspaces.current.tools.api.get.queryOptions({ select: selectToolsWithProvider }), + ) + const { data: workflowTools } = useQuery( + consoleQuery.workspaces.current.tools.workflow.get.queryOptions({ + select: selectToolsWithProvider, + }), + ) + const { data: mcpTools } = useQuery( + consoleQuery.workspaces.current.tools.mcp.get.queryOptions({ select: selectToolsWithProvider }), + ) return useMemo( () => createAgentToolProviderCatalog({ buildInTools, customTools, mcpTools, workflowTools }), diff --git a/web/service/__tests__/use-tools.spec.tsx b/web/service/__tests__/use-tools.spec.tsx deleted file mode 100644 index e5a463b68bc..00000000000 --- a/web/service/__tests__/use-tools.spec.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import type { ReactNode } from 'react' -import { QueryClient, QueryClientProvider } from '@tanstack/react-query' -import { act, waitFor } from '@testing-library/react' -import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' -import { renderHook } from '@/test/console/render' -import { useAllToolProviders, useRefreshMCPServerCode } from '../use-tools' - -const { mockGet, mockPost } = vi.hoisted(() => ({ - mockGet: vi.fn(), - mockPost: vi.fn(), -})) - -vi.mock('@/service/console', () => ({ - consoleClient: { - workspaces: { current: { toolProviders: { get: mockGet } } }, - apps: { - byAppId: { - server: { - refresh: { - post: mockPost, - }, - }, - }, - }, - }, -})) - -const createWrapper = () => { - const queryClient = new QueryClient({ - defaultOptions: { - mutations: { - retry: false, - }, - }, - }) - - return function Wrapper({ children }: { children: ReactNode }) { - return {children} - } -} - -describe('useRefreshMCPServerCode', () => { - beforeEach(() => { - vi.clearAllMocks() - }) - - it('refreshes the MCP server with POST and the app ID', async () => { - mockPost.mockResolvedValue({ id: 'server-1', server_code: 'new-code' }) - const { result } = renderHook(() => useRefreshMCPServerCode(), { - wrapper: createWrapper(), - }) - - await act(async () => { - await result.current.mutateAsync('app-1') - }) - - expect(mockPost).toHaveBeenCalledWith({ - params: { - app_id: 'app-1', - }, - }) - expect(mockGet).not.toHaveBeenCalled() - }) -}) - -describe('useAllToolProviders', () => { - beforeEach(() => { - vi.clearAllMocks() - mockGet.mockResolvedValue([]) - }) - - it('keeps filtered MCP providers in a separate query', async () => { - renderHook( - () => { - useAllToolProviders() - useAllToolProviders(true, 'mcp') - }, - { wrapper: createWrapper() }, - ) - - await waitFor(() => expect(mockGet).toHaveBeenCalledTimes(2)) - expect(mockGet).toHaveBeenCalledWith({ query: {} }) - expect(mockGet).toHaveBeenCalledWith({ query: { type: 'mcp' } }) - }) -}) diff --git a/web/service/use-plugins.ts b/web/service/use-plugins.ts index bca83fda64b..4eda2c81aaa 100644 --- a/web/service/use-plugins.ts +++ b/web/service/use-plugins.ts @@ -54,7 +54,6 @@ import { uninstallPlugin } from '@/service/plugins' import { hasPermission } from '@/utils/permission' // oxlint-disable-next-line no-restricted-imports import { getMarketplace, postMarketplace } from './base' -import { useInvalidateAllBuiltInTools } from './use-tools' const NAME_SPACE = 'plugins' const usePluginTaskListKey = [NAME_SPACE, 'pluginTaskList'] @@ -760,7 +759,10 @@ export const useRemoveFilteredInstalledPluginPageOnUnmount = ( export const useInvalidateInstalledPluginList = () => { const queryClient = useQueryClient() - const invalidateAllBuiltInTools = useInvalidateAllBuiltInTools() + const invalidateAllBuiltInTools = () => + queryClient.invalidateQueries({ + queryKey: consoleQuery.workspaces.current.tools.builtin.get.queryKey(), + }) return (category?: PluginCategoryEnum) => { if (category) { const invalidateCategoryPluginList = queryClient.invalidateQueries({ diff --git a/web/service/use-tools.ts b/web/service/use-tools.ts deleted file mode 100644 index 15c024f7d26..00000000000 --- a/web/service/use-tools.ts +++ /dev/null @@ -1,360 +0,0 @@ -import type { AppMcpServerStatus } from '@dify/contracts/api/console/apps/types.gen' -import type { QueryKey, UseQueryOptions } from '@tanstack/react-query' -import type { - Collection, - CollectionProviderType, - MCPServerDetail, - Tool, - WorkflowToolProviderResponse, -} from '@/app/components/tools/types' -import type { RAGRecommendedPlugins, ToolWithProvider } from '@/app/components/workflow/types' -import type { AppIconType } from '@/types/app' -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' -import { CollectionType } from '@/app/components/tools/types' -import { consoleClient } from '@/service/console' -import { narrowResponse } from './console/narrow-response' -import { useInvalid } from './use-base' - -const NAME_SPACE = 'tools' - -const useAllToolProvidersKey = [NAME_SPACE, 'allToolProviders'] -type ToolProviderListType = 'builtin' | 'model' | 'api' | 'workflow' | 'mcp' - -export const useAllToolProviders = (enabled = true, type?: ToolProviderListType) => { - return useQuery({ - queryKey: type ? [...useAllToolProvidersKey, type] : useAllToolProvidersKey, - queryFn: () => - narrowResponse( - consoleClient.workspaces.current.toolProviders.get({ query: type ? { type } : {} }), - ), - enabled, - }) -} - -export const useInvalidateAllToolProviders = () => { - return useInvalid(useAllToolProvidersKey) -} - -const useAllBuiltInToolsKey = [NAME_SPACE, 'builtIn'] -export const useAllBuiltInTools = (enabled = true) => { - return useQuery({ - queryKey: useAllBuiltInToolsKey, - queryFn: () => - narrowResponse(consoleClient.workspaces.current.tools.builtin.get()), - enabled, - }) -} - -export const useInvalidateAllBuiltInTools = () => { - return useInvalid(useAllBuiltInToolsKey) -} - -const useAllCustomToolsKey = [NAME_SPACE, 'customTools'] -export const useAllCustomTools = (enabled = true) => { - return useQuery({ - queryKey: useAllCustomToolsKey, - queryFn: () => - narrowResponse(consoleClient.workspaces.current.tools.api.get()), - enabled, - }) -} - -export const useInvalidateAllCustomTools = () => { - return useInvalid(useAllCustomToolsKey) -} - -const useAllWorkflowToolsKey = [NAME_SPACE, 'workflowTools'] -export const useAllWorkflowTools = (enabled = true) => { - return useQuery({ - queryKey: useAllWorkflowToolsKey, - queryFn: () => - narrowResponse(consoleClient.workspaces.current.tools.workflow.get()), - enabled, - }) -} - -export const useInvalidateAllWorkflowTools = () => { - return useInvalid(useAllWorkflowToolsKey) -} - -const useAllMCPToolsKey = [NAME_SPACE, 'MCPTools'] -export const useAllMCPTools = (enabled = true) => { - return useQuery({ - queryKey: useAllMCPToolsKey, - queryFn: () => - narrowResponse(consoleClient.workspaces.current.tools.mcp.get()), - enabled, - }) -} - -export const useInvalidateAllMCPTools = () => { - return useInvalid(useAllMCPToolsKey) -} - -const useInvalidToolsKeyMap: Partial> = { - [CollectionType.builtIn]: useAllBuiltInToolsKey, - [CollectionType.custom]: useAllCustomToolsKey, - [CollectionType.workflow]: useAllWorkflowToolsKey, - [CollectionType.mcp]: useAllMCPToolsKey, -} -export const useInvalidToolsByType = (type?: CollectionProviderType) => { - const queryKey = type ? useInvalidToolsKeyMap[type] : undefined - return useInvalid(queryKey) -} - -export const useCreateMCP = () => { - return useMutation({ - mutationKey: [NAME_SPACE, 'create-mcp'], - mutationFn: (payload: { - name: string - server_url: string - server_identifier: string - icon_type: AppIconType - icon: string - icon_background?: string - timeout?: number - sse_read_timeout?: number - headers?: Record - identity_mode?: 'off' | 'idp_token' - }) => { - return narrowResponse( - consoleClient.workspaces.current.toolProvider.mcp.post({ body: { ...payload } }), - ) - }, - }) -} - -export const useUpdateMCP = ({ onSuccess }: { onSuccess?: () => void }) => { - return useMutation({ - mutationKey: [NAME_SPACE, 'update-mcp'], - mutationFn: (payload: { - name: string - server_url: string - server_identifier: string - icon_type: AppIconType - icon: string - icon_background?: string - provider_id: string - timeout?: number - sse_read_timeout?: number - headers?: Record - identity_mode?: 'off' | 'idp_token' - }) => { - return consoleClient.workspaces.current.toolProvider.mcp.put({ body: { ...payload } }) - }, - onSuccess, - }) -} - -export const useDeleteMCP = ({ onSuccess }: { onSuccess?: () => void }) => { - return useMutation({ - mutationKey: [NAME_SPACE, 'delete-mcp'], - mutationFn: (id: string) => { - return consoleClient.workspaces.current.toolProvider.mcp.delete({ body: { provider_id: id } }) - }, - onSuccess, - }) -} - -export const useAuthorizeMCP = () => { - return useMutation({ - mutationKey: [NAME_SPACE, 'authorize-mcp'], - mutationFn: (payload: { provider_id: string }) => { - return consoleClient.workspaces.current.toolProvider.mcp.auth.post({ body: payload }) - }, - }) -} - -export const useMCPTools = (providerID: string) => { - return useQuery({ - enabled: !!providerID, - queryKey: [NAME_SPACE, 'get-MCP-provider-tool', providerID], - queryFn: () => - narrowResponse<{ tools: Tool[] }>( - consoleClient.workspaces.current.toolProvider.mcp.tools.byProviderId.get({ - params: { provider_id: providerID }, - }), - ), - }) -} -export const useInvalidateMCPTools = () => { - const queryClient = useQueryClient() - return (providerID: string) => { - queryClient.invalidateQueries({ - queryKey: [NAME_SPACE, 'get-MCP-provider-tool', providerID], - }) - } -} - -export const useUpdateMCPTools = () => { - return useMutation({ - mutationFn: (providerID: string) => - narrowResponse<{ tools: Tool[] }>( - consoleClient.workspaces.current.toolProvider.mcp.update.byProviderId.get({ - params: { provider_id: providerID }, - }), - ), - }) -} - -export const useMCPServerDetail = (appID: string, enabled = true) => { - return useQuery({ - queryKey: [NAME_SPACE, 'MCPServerDetail', appID], - queryFn: () => - narrowResponse( - consoleClient.apps.byAppId.server.get({ params: { app_id: appID } }), - ), - enabled, - }) -} - -export const useInvalidateMCPServerDetail = () => { - const queryClient = useQueryClient() - return (appID: string) => { - return queryClient.invalidateQueries({ - queryKey: [NAME_SPACE, 'MCPServerDetail', appID], - }) - } -} - -export const useCreateMCPServer = () => { - return useMutation({ - mutationKey: [NAME_SPACE, 'create-mcp-server'], - mutationFn: (payload: { - appID: string - description?: string - parameters: Record - }) => { - const { appID, ...rest } = payload - return consoleClient.apps.byAppId.server.post({ params: { app_id: appID }, body: rest }) - }, - }) -} - -export const useUpdateMCPServer = () => { - return useMutation({ - mutationKey: [NAME_SPACE, 'update-mcp-server'], - mutationFn: (payload: { - appID: string - id: string - description?: string - status?: AppMcpServerStatus - parameters: Record - }) => { - const { appID, ...rest } = payload - return consoleClient.apps.byAppId.server.put({ params: { app_id: appID }, body: rest }) - }, - }) -} - -export const useRefreshMCPServerCode = () => { - return useMutation({ - mutationKey: [NAME_SPACE, 'refresh-mcp-server-code'], - mutationFn: (appID: string) => { - return consoleClient.apps.byAppId.server.refresh.post({ - params: { - app_id: appID, - }, - }) - }, - }) -} - -export const useBuiltinTools = (providerName: string) => { - return useQuery({ - enabled: !!providerName, - queryKey: [NAME_SPACE, 'builtin-provider-tools', providerName], - queryFn: () => - narrowResponse( - consoleClient.workspaces.current.toolProvider.builtin.byProvider.tools.get({ - params: { provider: providerName }, - }), - ), - }) -} - -const useRAGRecommendedPluginListKey = [NAME_SPACE, 'rag-recommended-plugins'] - -export const useRAGRecommendedPlugins = (type: 'tool' | 'datasource' | 'all' = 'all') => { - return useQuery({ - queryKey: [...useRAGRecommendedPluginListKey, type], - queryFn: () => - narrowResponse( - consoleClient.rag.pipelines.recommendedPlugins.get({ query: { type } }), - ), - }) -} - -export const useInvalidateRAGRecommendedPlugins = () => { - const queryClient = useQueryClient() - return (type: 'tool' | 'datasource' | 'all' = 'all') => { - queryClient.invalidateQueries({ - queryKey: [...useRAGRecommendedPluginListKey, type], - }) - } -} - -// App Triggers API hooks -export type AppTrigger = { - id: string - trigger_type: 'trigger-webhook' | 'trigger-schedule' | 'trigger-plugin' - title: string - node_id: string - provider_name: string - icon: string - status: 'enabled' | 'disabled' | 'unauthorized' - created_at: string - updated_at: string -} - -export const useAppTriggers = ( - appId: string | undefined, - options?: Omit, 'queryKey' | 'queryFn'>, -) => { - return useQuery<{ data: AppTrigger[] }>({ - queryKey: [NAME_SPACE, 'app-triggers', appId], - queryFn: () => - narrowResponse<{ data: AppTrigger[] }>( - consoleClient.apps.byAppId.triggers.get({ params: { app_id: appId! } }), - ), - enabled: !!appId, - ...options, // Merge additional options while maintaining backward compatibility - }) -} - -export const useInvalidateAppTriggers = () => { - const queryClient = useQueryClient() - return (appId: string) => { - return queryClient.invalidateQueries({ - queryKey: [NAME_SPACE, 'app-triggers', appId], - }) - } -} - -const workflowToolDetailByAppIDKey = (appId: string) => [ - NAME_SPACE, - 'workflowToolDetailByAppID', - appId, -] - -export const useWorkflowToolDetailByAppID = (appId: string, enabled = true) => { - return useQuery({ - queryKey: workflowToolDetailByAppIDKey(appId), - queryFn: () => - narrowResponse( - consoleClient.workspaces.current.toolProvider.workflow.get.get({ - query: { workflow_app_id: appId }, - }), - ), - enabled: enabled && !!appId, - }) -} - -export const useInvalidateWorkflowToolDetailByAppID = () => { - const queryClient = useQueryClient() - return (appId: string) => { - queryClient.invalidateQueries({ - queryKey: workflowToolDetailByAppIDKey(appId), - }) - } -}