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