mirror of
https://github.com/langgenius/dify.git
synced 2026-10-16 07:20:34 +08:00
refactor(web): use generated tool options directly
Remove the forwarding hooks in service/use-tools. Consumers call the generated tool list, MCP provider, MCP server, app trigger, workflow tool detail and RAG recommendation queryOptions and mutationOptions directly, adapt responses to the tool UI types with select adapters in tools/utils/query-select, and invalidate generated keys inline. The provider-type to tool list key mapping lives in tools/utils/tool-list-query-key. Specs answer generated operations through mockConsoleOperationHooks and assert invalidation through cache state. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
6a889f484e
commit
791829fdb0
@@ -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
|
||||
|
||||
@@ -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<string, unknown>) => {
|
||||
@@ -112,8 +118,9 @@ function createDeferredPromise<T>() {
|
||||
|
||||
function renderCard(cardAppInfo: AppDetailWithSite = appInfo, workflow?: PublishedWorkflow) {
|
||||
const queryClient = createTestQueryClient()
|
||||
const invalidateQueries = vi.spyOn(queryClient, 'invalidateQueries')
|
||||
|
||||
return render(
|
||||
const view = render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MCPAccessPointCard
|
||||
appInfo={cardAppInfo}
|
||||
@@ -124,6 +131,7 @@ function renderCard(cardAppInfo: AppDetailWithSite = appInfo, workflow?: Publish
|
||||
/>
|
||||
</QueryClientProvider>,
|
||||
)
|
||||
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')
|
||||
})
|
||||
|
||||
@@ -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(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<TriggerAccessPointCard appInfo={appInfo} availability={availability} canManageAccessPoint />
|
||||
</QueryClientProvider>,
|
||||
)
|
||||
return queryClient
|
||||
}
|
||||
|
||||
function createDeferredPromise<T>() {
|
||||
@@ -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')
|
||||
})
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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')
|
||||
|
||||
+9
-6
@@ -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<typeof ToolPickerType>
|
||||
let singleToolSelection: ToolDefaultValue | null = null
|
||||
|
||||
@@ -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 || []),
|
||||
|
||||
@@ -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<typeof import('@/next/navigation')>()),
|
||||
|
||||
@@ -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[])
|
||||
: []
|
||||
|
||||
+9
-6
@@ -10,12 +10,15 @@ import { useGetProviderIcon } from '../use-provider-icon'
|
||||
|
||||
const request = vi.hoisted(() => vi.fn<(url: string) => Promise<Response>>())
|
||||
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) => ({
|
||||
|
||||
+25
-10
@@ -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<string>([
|
||||
@@ -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,
|
||||
|
||||
@@ -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<string, unknown>) => {
|
||||
const normalizeAgentTool = (
|
||||
tool: Record<string, unknown>,
|
||||
deletedTools: TrialAppDetailResponse['deleted_tools'] | undefined,
|
||||
collectionList: ReturnType<typeof useAllToolProviders>['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<Props> = ({ 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,
|
||||
|
||||
@@ -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[],
|
||||
|
||||
@@ -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'
|
||||
|
||||
+17
-17
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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', () => ({
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+14
-11
@@ -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: [] } } }),
|
||||
|
||||
@@ -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])
|
||||
|
||||
+12
-9
@@ -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(<Harness />)
|
||||
return renderWithConsoleQuery(<Harness />)
|
||||
}
|
||||
|
||||
describe('MultipleToolSelector focus restoration', () => {
|
||||
|
||||
+8
-5
@@ -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
|
||||
|
||||
@@ -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<HTMLButtonElement>(null)
|
||||
const toolItemTriggerByKeyRef = useRefWithInit(() => new Map<string, HTMLButtonElement>())
|
||||
const pendingFocusTargetRef = React.useRef<{ toolKey?: string } | null>(null)
|
||||
|
||||
+101
-64
@@ -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({
|
||||
|
||||
+24
-14
@@ -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<TabType>('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(
|
||||
|
||||
@@ -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'),
|
||||
}))
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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<ComponentProps<typeof MCPModal>['onConfirm']>[0]
|
||||
|
||||
type Props = Readonly<{
|
||||
handleCreate: (provider: ToolWithProvider) => Promise<void> | 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<typeof createMCP>[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 {
|
||||
|
||||
@@ -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' } })
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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<Props> = ({
|
||||
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<Props> = ({
|
||||
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<Props> = ({
|
||||
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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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<string, string>
|
||||
} = {
|
||||
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<string, string>
|
||||
} = {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,9 +44,8 @@ type MCPModalConfirmPayload = {
|
||||
headers?: Record<string, string>
|
||||
is_dynamic_registration?: boolean
|
||||
authentication?: {
|
||||
client_id?: string
|
||||
client_id: string
|
||||
client_secret?: string
|
||||
grant_type?: string
|
||||
}
|
||||
configuration: {
|
||||
timeout: number
|
||||
|
||||
@@ -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: '',
|
||||
}))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
+175
-112
@@ -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<unknown>(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<typeof createDefaultOptions>) => 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<typeof createDefaultOptions>) => 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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -46,12 +46,11 @@ vi.mock('@/next/navigation', async (importOriginal) => ({
|
||||
useSearchParams: () => ({ get: () => null }),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-tools', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/service/use-tools')>()
|
||||
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', () => ({
|
||||
|
||||
+10
-7
@@ -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<typeof import('@/next/navigation')>()),
|
||||
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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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 ?? [] }),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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([]),
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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<string, string>; 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(<BlockSelectorPanels {...baseProps} defaultTab={TabType.Tools} />)
|
||||
const { queryClient } = render(
|
||||
<BlockSelectorPanels {...baseProps} defaultTab={TabType.Tools} />,
|
||||
)
|
||||
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', () => {
|
||||
|
||||
@@ -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<typeof useMarketplacePlugins>)
|
||||
mockUseAllBuiltInTools.mockReturnValue({ data: builtInTools } as ReturnType<
|
||||
typeof useAllBuiltInTools
|
||||
>)
|
||||
mockUseAllCustomTools.mockImplementation(
|
||||
(enabled = true) =>
|
||||
({
|
||||
data: enabled ? customTools : [],
|
||||
}) as ReturnType<typeof useAllCustomTools>,
|
||||
)
|
||||
mockUseAllWorkflowTools.mockImplementation(
|
||||
(enabled = true) =>
|
||||
({
|
||||
data: enabled ? workflowTools : [],
|
||||
}) as ReturnType<typeof useAllWorkflowTools>,
|
||||
)
|
||||
mockUseAllMCPTools.mockReturnValue({ data: mcpTools } as ReturnType<typeof useAllMCPTools>)
|
||||
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<typeof useAllBuiltInTools>)
|
||||
})
|
||||
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<typeof useAllBuiltInTools>)
|
||||
})
|
||||
rerender(<ToolPickerContent {...props} />)
|
||||
|
||||
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<typeof useFeaturedToolsRecommendations>)
|
||||
|
||||
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 },
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
})
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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: () => <div className="h-24 animate-pulse rounded-lg bg-background-section" />,
|
||||
@@ -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() })
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -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' }))
|
||||
|
||||
|
||||
@@ -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<typeof import('@tanstack/react-query')>()
|
||||
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 }),
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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', () => ({
|
||||
|
||||
@@ -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', () => ({
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -19,11 +19,14 @@ const workflowToolsState = vi.hoisted(
|
||||
() => [] as Array<{ id: string; author: string; description: Record<string, string> }>,
|
||||
)
|
||||
|
||||
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> = {}): Node =>
|
||||
({
|
||||
|
||||
@@ -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 () =>
|
||||
|
||||
@@ -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<ToolWithProvider>[],
|
||||
}),
|
||||
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<ToolWithProvider>[],
|
||||
}),
|
||||
'workspaces.current.tools.api.get': () => ({ data: [] }),
|
||||
'workspaces.current.tools.workflow.get': () => ({ data: [] }),
|
||||
'workspaces.current.tools.mcp.get': () => ({ data: [] }),
|
||||
})
|
||||
})
|
||||
|
||||
const createNode = (overrides: Partial<Node> = {}): Node => ({
|
||||
id: 'node-1',
|
||||
|
||||
@@ -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(() => ({
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
({
|
||||
|
||||
@@ -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<WorkflowProps> = 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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: () => <div data-testid="node-actions-change-block" />,
|
||||
@@ -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 = <T,>(data: T): UseQueryResult<T, Error> =>
|
||||
({
|
||||
|
||||
@@ -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)
|
||||
|
||||
+10
-9
@@ -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 }),
|
||||
|
||||
@@ -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 }),
|
||||
|
||||
+8
-5
@@ -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: () => ({
|
||||
|
||||
@@ -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<BasePanelProps> = ({ 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],
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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 = <T>({
|
||||
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'
|
||||
|
||||
@@ -55,13 +55,6 @@ vi.mock('@langgenius/dify-ui/status-dot', () => ({
|
||||
StatusDot: ({ status }: any) => <div>{`indicator:${status}`}</div>,
|
||||
}))
|
||||
|
||||
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<typeof import('@tanstack/react-query')>()
|
||||
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 }),
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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 }) => <div>{`indicator:${status}`}</div>,
|
||||
|
||||
@@ -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<HTMLDivElement>(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 = [
|
||||
|
||||
@@ -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(),
|
||||
|
||||
+19
-10
@@ -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()
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 || [],
|
||||
|
||||
@@ -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<typeof import('../../../hooks/use-workflow')>()
|
||||
|
||||
@@ -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 || [],
|
||||
|
||||
+9
-7
@@ -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',
|
||||
|
||||
+16
-4
@@ -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<Props> = ({ 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) => {
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
+9
-6
@@ -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),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user