diff --git a/web/app/components/workflow/skill/editor/skill-editor/plugins/file-reference-block/file-preview-panel.tsx b/web/app/components/workflow/skill/editor/skill-editor/plugins/file-reference-block/file-preview-panel.tsx index 4358582ed9..2b7f153b08 100644 --- a/web/app/components/workflow/skill/editor/skill-editor/plugins/file-reference-block/file-preview-panel.tsx +++ b/web/app/components/workflow/skill/editor/skill-editor/plugins/file-reference-block/file-preview-panel.tsx @@ -1,5 +1,6 @@ import type { FileAppearanceType } from '@/app/components/base/file-uploader/types' import type { AppAssetTreeView } from '@/types/app-asset' +import { useQuery } from '@tanstack/react-query' import * as React from 'react' import { useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' @@ -11,7 +12,7 @@ import SkillEditor from '@/app/components/workflow/skill/editor/skill-editor' import { useFileTypeInfo } from '@/app/components/workflow/skill/hooks/use-file-type-info' import { getFileIconType } from '@/app/components/workflow/skill/utils/file-utils' import ReadOnlyFilePreview from '@/app/components/workflow/skill/viewer/read-only-file-preview' -import { useGetAppAssetFileContent, useGetAppAssetFileDownloadUrl } from '@/service/use-app-asset' +import { appAssetFileContentOptions, appAssetFileDownloadUrlOptions } from '@/service/use-app-asset' import { cn } from '@/utils/classnames' type FilePreviewPanelProps = { @@ -36,7 +37,8 @@ const FilePreviewPanel = ({ resourceId, currentNode, className, style, onClose } data: fileContent, isLoading: isContentLoading, error: contentError, - } = useGetAppAssetFileContent(appId, resourceId, { + } = useQuery({ + ...appAssetFileContentOptions(appId, resourceId), enabled: isMarkdownPreview, }) @@ -44,7 +46,8 @@ const FilePreviewPanel = ({ resourceId, currentNode, className, style, onClose } data: downloadUrlData, isLoading: isDownloadLoading, error: downloadError, - } = useGetAppAssetFileDownloadUrl(appId, resourceId, { + } = useQuery({ + ...appAssetFileDownloadUrlOptions(appId, resourceId), enabled: isReadOnlyPreview, }) diff --git a/web/app/components/workflow/skill/hooks/file-tree/data/use-skill-asset-tree.spec.tsx b/web/app/components/workflow/skill/hooks/file-tree/data/use-skill-asset-tree.spec.tsx index 64c767b289..f196914af5 100644 --- a/web/app/components/workflow/skill/hooks/file-tree/data/use-skill-asset-tree.spec.tsx +++ b/web/app/components/workflow/skill/hooks/file-tree/data/use-skill-asset-tree.spec.tsx @@ -8,12 +8,22 @@ import { useSkillAssetTreeData, } from './use-skill-asset-tree' -const { mockUseGetAppAssetTree } = vi.hoisted(() => ({ - mockUseGetAppAssetTree: vi.fn(), +const { mockUseQuery, mockAppAssetTreeOptions } = vi.hoisted(() => ({ + mockUseQuery: vi.fn(), + mockAppAssetTreeOptions: vi.fn().mockReturnValue({ + queryKey: ['test', 'tree'], + queryFn: vi.fn(), + enabled: true, + }), +})) + +vi.mock('@tanstack/react-query', async importOriginal => ({ + ...await importOriginal(), + useQuery: (options: unknown) => mockUseQuery(options), })) vi.mock('@/service/use-app-asset', () => ({ - useGetAppAssetTree: (...args: unknown[]) => mockUseGetAppAssetTree(...args), + appAssetTreeOptions: (...args: unknown[]) => mockAppAssetTreeOptions(...args), })) const createTreeNode = ( @@ -34,22 +44,21 @@ describe('useSkillAssetTree', () => { useAppStore.setState({ appDetail: { id: 'app-1' } as App & Partial, }) - mockUseGetAppAssetTree.mockReturnValue({ + mockUseQuery.mockReturnValue({ data: null, isPending: false, error: null, }) }) - // Scenario: should pass app id from app store to the data query hook. describe('useSkillAssetTreeData', () => { it('should request tree data with current app id', () => { const expectedResult = { data: { children: [] }, isPending: false } - mockUseGetAppAssetTree.mockReturnValue(expectedResult) + mockUseQuery.mockReturnValue(expectedResult) const { result } = renderHook(() => useSkillAssetTreeData()) - expect(mockUseGetAppAssetTree).toHaveBeenCalledWith('app-1') + expect(mockAppAssetTreeOptions).toHaveBeenCalledWith('app-1') expect(result.current).toBe(expectedResult) }) @@ -58,16 +67,15 @@ describe('useSkillAssetTree', () => { renderHook(() => useSkillAssetTreeData()) - expect(mockUseGetAppAssetTree).toHaveBeenCalledWith('') + expect(mockAppAssetTreeOptions).toHaveBeenCalledWith('') }) }) - // Scenario: should expose a select transform that builds node lookup maps. describe('useSkillAssetNodeMap', () => { it('should build a map including nested nodes', () => { renderHook(() => useSkillAssetNodeMap()) - const options = mockUseGetAppAssetTree.mock.calls[0][1] as { + const options = mockUseQuery.mock.calls[0][0] as { select: (data: AppAssetTreeResponse) => Map } @@ -97,7 +105,7 @@ describe('useSkillAssetTree', () => { it('should return an empty map when tree response has no children', () => { renderHook(() => useSkillAssetNodeMap()) - const options = mockUseGetAppAssetTree.mock.calls[0][1] as { + const options = mockUseQuery.mock.calls[0][0] as { select: (data: AppAssetTreeResponse) => Map } @@ -107,12 +115,11 @@ describe('useSkillAssetTree', () => { }) }) - // Scenario: should expose root-level existing skill folder names. describe('useExistingSkillNames', () => { it('should collect only root folder names', () => { renderHook(() => useExistingSkillNames()) - const options = mockUseGetAppAssetTree.mock.calls[0][1] as { + const options = mockUseQuery.mock.calls[0][0] as { select: (data: AppAssetTreeResponse) => Set } @@ -153,7 +160,7 @@ describe('useSkillAssetTree', () => { it('should return an empty set when tree response has no children', () => { renderHook(() => useExistingSkillNames()) - const options = mockUseGetAppAssetTree.mock.calls[0][1] as { + const options = mockUseQuery.mock.calls[0][0] as { select: (data: AppAssetTreeResponse) => Set } diff --git a/web/app/components/workflow/skill/hooks/file-tree/data/use-skill-asset-tree.ts b/web/app/components/workflow/skill/hooks/file-tree/data/use-skill-asset-tree.ts index 9386370db8..58bb22ab74 100644 --- a/web/app/components/workflow/skill/hooks/file-tree/data/use-skill-asset-tree.ts +++ b/web/app/components/workflow/skill/hooks/file-tree/data/use-skill-asset-tree.ts @@ -1,33 +1,23 @@ import type { AppAssetTreeResponse, AppAssetTreeView } from '@/types/app-asset' +import { useQuery } from '@tanstack/react-query' import { useStore as useAppStore } from '@/app/components/app/store' -import { useGetAppAssetTree } from '@/service/use-app-asset' +import { appAssetTreeOptions } from '@/service/use-app-asset' import { buildNodeMap } from '../../../utils/tree-utils' -/** - * Get the current app ID from the app store. - * Used internally by skill asset tree hooks. - */ function useSkillAppId(): string { const appDetail = useAppStore(s => s.appDetail) return appDetail?.id || '' } -/** - * Hook to get the asset tree data for the current skill app. - * Returns the raw tree data along with loading and error states. - */ export function useSkillAssetTreeData() { const appId = useSkillAppId() - return useGetAppAssetTree(appId) + return useQuery(appAssetTreeOptions(appId)) } -/** - * Hook to get the node map (id -> node) for the current skill app. - * Uses TanStack Query's select option to compute and cache the map. - */ export function useSkillAssetNodeMap() { const appId = useSkillAppId() - return useGetAppAssetTree(appId, { + return useQuery({ + ...appAssetTreeOptions(appId), select: (data: AppAssetTreeResponse): Map => { if (!data?.children) return new Map() @@ -36,13 +26,10 @@ export function useSkillAssetNodeMap() { }) } -/** - * Hook to get the set of root-level folder names in the skill asset tree. - * Useful for checking whether a skill template has already been added. - */ export function useExistingSkillNames() { const appId = useSkillAppId() - return useGetAppAssetTree(appId, { + return useQuery({ + ...appAssetTreeOptions(appId), select: (data: AppAssetTreeResponse): Set => { if (!data?.children) return new Set() diff --git a/web/app/components/workflow/skill/hooks/use-skill-file-data.spec.tsx b/web/app/components/workflow/skill/hooks/use-skill-file-data.spec.tsx index ee562908b8..6e91f45518 100644 --- a/web/app/components/workflow/skill/hooks/use-skill-file-data.spec.tsx +++ b/web/app/components/workflow/skill/hooks/use-skill-file-data.spec.tsx @@ -1,28 +1,32 @@ import { renderHook } from '@testing-library/react' import { useSkillFileData } from './use-skill-file-data' -const { - mockUseGetAppAssetFileContent, - mockUseGetAppAssetFileDownloadUrl, -} = vi.hoisted(() => ({ - mockUseGetAppAssetFileContent: vi.fn(), - mockUseGetAppAssetFileDownloadUrl: vi.fn(), +const { mockUseQuery, mockContentOptions, mockDownloadUrlOptions } = vi.hoisted(() => ({ + mockUseQuery: vi.fn(), + mockContentOptions: vi.fn().mockReturnValue({ + queryKey: ['test', 'content'], + queryFn: vi.fn(), + }), + mockDownloadUrlOptions: vi.fn().mockReturnValue({ + queryKey: ['test', 'downloadUrl'], + queryFn: vi.fn(), + }), +})) + +vi.mock('@tanstack/react-query', async importOriginal => ({ + ...await importOriginal(), + useQuery: (options: unknown) => mockUseQuery(options), })) vi.mock('@/service/use-app-asset', () => ({ - useGetAppAssetFileContent: (...args: unknown[]) => mockUseGetAppAssetFileContent(...args), - useGetAppAssetFileDownloadUrl: (...args: unknown[]) => mockUseGetAppAssetFileDownloadUrl(...args), + appAssetFileContentOptions: (...args: unknown[]) => mockContentOptions(...args), + appAssetFileDownloadUrlOptions: (...args: unknown[]) => mockDownloadUrlOptions(...args), })) describe('useSkillFileData', () => { beforeEach(() => { vi.clearAllMocks() - mockUseGetAppAssetFileContent.mockReturnValue({ - data: undefined, - isLoading: false, - error: null, - }) - mockUseGetAppAssetFileDownloadUrl.mockReturnValue({ + mockUseQuery.mockReturnValue({ data: undefined, isLoading: false, error: null, @@ -33,51 +37,62 @@ describe('useSkillFileData', () => { it('should disable both queries when mode is none', () => { const { result } = renderHook(() => useSkillFileData('app-1', 'node-1', 'none')) - expect(mockUseGetAppAssetFileContent).toHaveBeenCalledWith('app-1', 'node-1', { enabled: false }) - expect(mockUseGetAppAssetFileDownloadUrl).toHaveBeenCalledWith('app-1', 'node-1', { enabled: false }) + expect(mockContentOptions).toHaveBeenCalledWith('app-1', 'node-1') + expect(mockDownloadUrlOptions).toHaveBeenCalledWith('app-1', 'node-1') + expect(mockUseQuery.mock.calls[0][0].enabled).toBe(false) + expect(mockUseQuery.mock.calls[1][0].enabled).toBe(false) expect(result.current.isLoading).toBe(false) expect(result.current.error).toBeNull() }) it('should fetch content data when mode is content', () => { const contentError = new Error('content-error') - mockUseGetAppAssetFileContent.mockReturnValue({ - data: { content: 'hello' }, - isLoading: true, - error: contentError, - }) - mockUseGetAppAssetFileDownloadUrl.mockReturnValue({ - data: { download_url: 'https://example.com/file' }, - isLoading: true, - error: new Error('download-error'), - }) + mockUseQuery + .mockReturnValueOnce({ + data: { content: 'hello' }, + isLoading: true, + error: contentError, + }) + .mockReturnValueOnce({ + data: { download_url: 'https://example.com/file' }, + isLoading: true, + error: new Error('download-error'), + }) const { result } = renderHook(() => useSkillFileData('app-1', 'node-1', 'content')) - expect(mockUseGetAppAssetFileContent).toHaveBeenCalledWith('app-1', 'node-1', { enabled: true }) - expect(mockUseGetAppAssetFileDownloadUrl).toHaveBeenCalledWith('app-1', 'node-1', { enabled: false }) + expect(mockUseQuery.mock.calls[0][0].enabled).toBe(true) + expect(mockUseQuery.mock.calls[1][0].enabled).toBe(false) expect(result.current.fileContent).toEqual({ content: 'hello' }) expect(result.current.isLoading).toBe(true) expect(result.current.error).toBe(contentError) }) + it('should disable content query when nodeId is null even if mode is content', () => { + const { result } = renderHook(() => useSkillFileData('app-1', null, 'content')) + + expect(mockUseQuery.mock.calls[0][0].enabled).toBe(false) + expect(result.current.isLoading).toBe(false) + }) + it('should fetch download URL data when mode is download', () => { const downloadError = new Error('download-error') - mockUseGetAppAssetFileContent.mockReturnValue({ - data: { content: 'hello' }, - isLoading: true, - error: new Error('content-error'), - }) - mockUseGetAppAssetFileDownloadUrl.mockReturnValue({ - data: { download_url: 'https://example.com/file' }, - isLoading: true, - error: downloadError, - }) + mockUseQuery + .mockReturnValueOnce({ + data: { content: 'hello' }, + isLoading: true, + error: new Error('content-error'), + }) + .mockReturnValueOnce({ + data: { download_url: 'https://example.com/file' }, + isLoading: true, + error: downloadError, + }) const { result } = renderHook(() => useSkillFileData('app-1', 'node-1', 'download')) - expect(mockUseGetAppAssetFileContent).toHaveBeenCalledWith('app-1', 'node-1', { enabled: false }) - expect(mockUseGetAppAssetFileDownloadUrl).toHaveBeenCalledWith('app-1', 'node-1', { enabled: true }) + expect(mockUseQuery.mock.calls[0][0].enabled).toBe(false) + expect(mockUseQuery.mock.calls[1][0].enabled).toBe(true) expect(result.current.downloadUrlData).toEqual({ download_url: 'https://example.com/file' }) expect(result.current.isLoading).toBe(true) expect(result.current.error).toBe(downloadError) diff --git a/web/app/components/workflow/skill/hooks/use-skill-file-data.ts b/web/app/components/workflow/skill/hooks/use-skill-file-data.ts index 34ea7d46b9..83467f00f7 100644 --- a/web/app/components/workflow/skill/hooks/use-skill-file-data.ts +++ b/web/app/components/workflow/skill/hooks/use-skill-file-data.ts @@ -1,40 +1,29 @@ -import { useGetAppAssetFileContent, useGetAppAssetFileDownloadUrl } from '@/service/use-app-asset' +import { useQuery } from '@tanstack/react-query' +import { appAssetFileContentOptions, appAssetFileDownloadUrlOptions } from '@/service/use-app-asset' export type SkillFileDataMode = 'none' | 'content' | 'download' -export type SkillFileDataResult = { - fileContent: ReturnType['data'] - downloadUrlData: ReturnType['data'] - isLoading: boolean - error: Error | null -} - -/** - * Hook to fetch file data for skill documents. - * Uses explicit mode to control data fetching: - * - 'content': fetch editable file content - * - 'download': fetch non-editable file download URL - * - 'none': skip file-related requests while node metadata is unresolved - */ export function useSkillFileData( appId: string, nodeId: string | null | undefined, mode: SkillFileDataMode, -): SkillFileDataResult { +) { const { data: fileContent, isLoading: isContentLoading, error: contentError, - } = useGetAppAssetFileContent(appId, nodeId || '', { - enabled: mode === 'content', + } = useQuery({ + ...appAssetFileContentOptions(appId, nodeId || ''), + enabled: mode === 'content' && !!appId && !!nodeId, }) const { data: downloadUrlData, isLoading: isDownloadUrlLoading, error: downloadUrlError, - } = useGetAppAssetFileDownloadUrl(appId, nodeId || '', { - enabled: mode === 'download' && !!nodeId, + } = useQuery({ + ...appAssetFileDownloadUrlOptions(appId, nodeId || ''), + enabled: mode === 'download' && !!appId && !!nodeId, }) const isLoading = mode === 'content' diff --git a/web/app/components/workflow/skill/skill-body/panels/artifact-content-panel.spec.tsx b/web/app/components/workflow/skill/skill-body/panels/artifact-content-panel.spec.tsx index 6ee99e066c..d1c162b338 100644 --- a/web/app/components/workflow/skill/skill-body/panels/artifact-content-panel.spec.tsx +++ b/web/app/components/workflow/skill/skill-body/panels/artifact-content-panel.spec.tsx @@ -1,4 +1,3 @@ -import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { render, screen } from '@testing-library/react' import ArtifactContentPanel from './artifact-content-panel' @@ -12,39 +11,32 @@ const mocks = vi.hoisted(() => ({ activeTabId: 'artifact:/assets/report.bin', appId: 'app-1', } as WorkflowStoreState, - useSandboxFileDownloadUrl: vi.fn(), + mockUseQuery: vi.fn(), + mockDownloadUrlOptions: vi.fn().mockReturnValue({ + queryKey: ['sandboxFile', 'downloadFile'], + queryFn: vi.fn(), + }), })) vi.mock('@/app/components/workflow/store', () => ({ useStore: (selector: (state: WorkflowStoreState) => unknown) => selector(mocks.workflowState), })) -vi.mock('@/service/use-sandbox-file', () => ({ - useSandboxFileDownloadUrl: (...args: unknown[]) => mocks.useSandboxFileDownloadUrl(...args), +vi.mock('@tanstack/react-query', async importOriginal => ({ + ...await importOriginal(), + useQuery: (options: unknown) => mocks.mockUseQuery(options), })) -const renderPanel = () => { - const queryClient = new QueryClient({ - defaultOptions: { - queries: { - retry: false, - }, - }, - }) - - return render( - - - , - ) -} +vi.mock('@/service/use-sandbox-file', () => ({ + sandboxFileDownloadUrlOptions: (...args: unknown[]) => mocks.mockDownloadUrlOptions(...args), +})) describe('ArtifactContentPanel', () => { beforeEach(() => { vi.clearAllMocks() mocks.workflowState.activeTabId = 'artifact:/assets/report.bin' mocks.workflowState.appId = 'app-1' - mocks.useSandboxFileDownloadUrl.mockReturnValue({ + mocks.mockUseQuery.mockReturnValue({ data: { download_url: 'https://example.com/report.bin' }, isLoading: false, }) @@ -52,38 +44,30 @@ describe('ArtifactContentPanel', () => { describe('Rendering', () => { it('should show loading indicator when download ticket is loading', () => { - // Arrange - mocks.useSandboxFileDownloadUrl.mockReturnValue({ + mocks.mockUseQuery.mockReturnValue({ data: undefined, isLoading: true, }) - // Act - renderPanel() + render() - // Assert expect(screen.getByRole('status')).toBeInTheDocument() }) it('should show load error message when download url is unavailable', () => { - // Arrange - mocks.useSandboxFileDownloadUrl.mockReturnValue({ + mocks.mockUseQuery.mockReturnValue({ data: { download_url: '' }, isLoading: false, }) - // Act - renderPanel() + render() - // Assert expect(screen.getByText('workflow.skillSidebar.loadError')).toBeInTheDocument() }) it('should render preview panel when ticket contains download url', () => { - // Act - renderPanel() + render() - // Assert expect(screen.getByText('report.bin')).toBeInTheDocument() expect(screen.getByRole('button', { name: /common\.operation\.download/i })).toBeInTheDocument() }) @@ -91,25 +75,19 @@ describe('ArtifactContentPanel', () => { describe('Data flow', () => { it('should request ticket using app id and artifact path when tab is selected', () => { - // Act - renderPanel() + render() - // Assert - expect(mocks.useSandboxFileDownloadUrl).toHaveBeenCalledTimes(1) - expect(mocks.useSandboxFileDownloadUrl).toHaveBeenCalledWith('app-1', '/assets/report.bin') + expect(mocks.mockDownloadUrlOptions).toHaveBeenCalledWith('app-1', '/assets/report.bin') }) }) describe('Edge Cases', () => { - it('should request ticket with undefined path when active tab id is null', () => { - // Arrange + it('should pass undefined path to options factory when active tab id is null', () => { mocks.workflowState.activeTabId = null - // Act - renderPanel() + render() - // Assert - expect(mocks.useSandboxFileDownloadUrl).toHaveBeenCalledWith('app-1', undefined) + expect(mocks.mockDownloadUrlOptions).toHaveBeenCalledWith('app-1', undefined) }) }) }) diff --git a/web/app/components/workflow/skill/skill-body/panels/artifact-content-panel.tsx b/web/app/components/workflow/skill/skill-body/panels/artifact-content-panel.tsx index 76a0baf37e..8bf716c675 100644 --- a/web/app/components/workflow/skill/skill-body/panels/artifact-content-panel.tsx +++ b/web/app/components/workflow/skill/skill-body/panels/artifact-content-panel.tsx @@ -1,10 +1,11 @@ 'use client' +import { useQuery } from '@tanstack/react-query' import * as React from 'react' import { useTranslation } from 'react-i18next' import Loading from '@/app/components/base/loading' import { useStore } from '@/app/components/workflow/store' -import { useSandboxFileDownloadUrl } from '@/service/use-sandbox-file' +import { sandboxFileDownloadUrlOptions } from '@/service/use-sandbox-file' import { getArtifactPath } from '../../constants' import { getFileExtension } from '../../utils/file-utils' import ReadOnlyFilePreview from '../../viewer/read-only-file-preview' @@ -18,7 +19,7 @@ const ArtifactContentPanel = () => { const fileName = path?.split('/').pop() ?? '' const extension = getFileExtension(fileName) - const { data: ticket, isLoading } = useSandboxFileDownloadUrl(appId, path) + const { data: ticket, isLoading } = useQuery(sandboxFileDownloadUrlOptions(appId, path)) if (isLoading) { return ( diff --git a/web/app/components/workflow/variable-inspect/artifacts-tab.spec.tsx b/web/app/components/workflow/variable-inspect/artifacts-tab.spec.tsx index 9ea8a0dc00..78ca2be526 100644 --- a/web/app/components/workflow/variable-inspect/artifacts-tab.spec.tsx +++ b/web/app/components/workflow/variable-inspect/artifacts-tab.spec.tsx @@ -26,14 +26,24 @@ const mocks = vi.hoisted(() => ({ hasFiles: false, isLoading: false, fetchDownloadUrl: vi.fn(), - useSandboxFileDownloadUrl: vi.fn(), + mockUseQuery: vi.fn(), + mockDownloadUrlOptions: vi.fn().mockReturnValue({ + queryKey: ['sandboxFile', 'downloadFile'], + queryFn: vi.fn(), + }), })) vi.mock('../store', () => ({ useStore: (selector: (state: MockStoreState) => unknown) => selector(mocks.storeState), })) +vi.mock('@tanstack/react-query', async importOriginal => ({ + ...await importOriginal(), + useQuery: (options: unknown) => mocks.mockUseQuery(options), +})) + vi.mock('@/service/use-sandbox-file', () => ({ + sandboxFileDownloadUrlOptions: (...args: unknown[]) => mocks.mockDownloadUrlOptions(...args), useSandboxFilesTree: () => ({ data: mocks.treeData, flatData: mocks.flatData, @@ -44,7 +54,6 @@ vi.mock('@/service/use-sandbox-file', () => ({ mutateAsync: mocks.fetchDownloadUrl, isPending: false, }), - useSandboxFileDownloadUrl: (...args: unknown[]) => mocks.useSandboxFileDownloadUrl(...args), })) vi.mock('@/context/i18n', () => ({ @@ -98,7 +107,7 @@ describe('ArtifactsTab', () => { mocks.flatData = [createFlatFileNode()] mocks.hasFiles = true mocks.isLoading = false - mocks.useSandboxFileDownloadUrl.mockReturnValue({ + mocks.mockUseQuery.mockReturnValue({ data: undefined, isLoading: false, }) @@ -116,11 +125,7 @@ describe('ArtifactsTab', () => { fireEvent.click(screen.getByRole('button', { name: 'a.txt' })) await waitFor(() => { - expect(mocks.useSandboxFileDownloadUrl).toHaveBeenCalledWith( - 'app-1', - 'a.txt', - { retry: false }, - ) + expect(mocks.mockDownloadUrlOptions).toHaveBeenCalledWith('app-1', 'a.txt') }) mocks.treeData = undefined @@ -130,12 +135,8 @@ describe('ArtifactsTab', () => { rerender() await waitFor(() => { - const lastCall = mocks.useSandboxFileDownloadUrl.mock.calls.at(-1) - expect(lastCall).toEqual([ - 'app-1', - undefined, - { retry: false }, - ]) + const lastCall = mocks.mockDownloadUrlOptions.mock.calls.at(-1) + expect(lastCall).toEqual(['app-1', undefined]) }) }) }) diff --git a/web/app/components/workflow/variable-inspect/artifacts-tab.tsx b/web/app/components/workflow/variable-inspect/artifacts-tab.tsx index 3ca4d40dfe..e90808062a 100644 --- a/web/app/components/workflow/variable-inspect/artifacts-tab.tsx +++ b/web/app/components/workflow/variable-inspect/artifacts-tab.tsx @@ -1,6 +1,7 @@ import type { InspectHeaderProps } from './inspect-layout' import type { DocPathWithoutLang } from '@/types/doc-paths' import type { SandboxFileTreeNode } from '@/types/sandbox-file' +import { useQuery } from '@tanstack/react-query' import { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import ActionButton from '@/app/components/base/action-button' @@ -10,7 +11,7 @@ import Loading from '@/app/components/base/loading' import ArtifactsTree from '@/app/components/workflow/skill/file-tree/artifacts/artifacts-tree' import ReadOnlyFilePreview from '@/app/components/workflow/skill/viewer/read-only-file-preview' import { useDocLink } from '@/context/i18n' -import { useDownloadSandboxFile, useSandboxFileDownloadUrl, useSandboxFilesTree } from '@/service/use-sandbox-file' +import { sandboxFileDownloadUrlOptions, useDownloadSandboxFile, useSandboxFilesTree } from '@/service/use-sandbox-file' import { cn } from '@/utils/classnames' import { downloadUrl } from '@/utils/download' import { useStore } from '../store' @@ -83,11 +84,10 @@ const ArtifactsTab = (headerProps: InspectHeaderProps) => { return selectedExists ? selectedFile.path : undefined }, [flatData, selectedFile]) - const { data: downloadUrlData, isLoading: isDownloadUrlLoading } = useSandboxFileDownloadUrl( - appId, - selectedFilePath, - { retry: false }, - ) + const { data: downloadUrlData, isLoading: isDownloadUrlLoading } = useQuery({ + ...sandboxFileDownloadUrlOptions(appId, selectedFilePath), + retry: false, + }) const handleFileSelect = useCallback((node: SandboxFileTreeNode) => { if (node.node_type === 'file') diff --git a/web/service/use-app-asset.ts b/web/service/use-app-asset.ts index b39b869011..45c2c97566 100644 --- a/web/service/use-app-asset.ts +++ b/web/service/use-app-asset.ts @@ -1,6 +1,5 @@ import type { AppAssetNode, - AppAssetTreeResponse, BatchUploadNodeInput, BatchUploadNodeOutput, CreateFolderPayload, @@ -12,26 +11,36 @@ import type { } from '@/types/app-asset' import { useMutation, - useQuery, useQueryClient, } from '@tanstack/react-query' import { consoleClient, consoleQuery } from '@/service/client' import { upload } from './base' import { uploadToPresignedUrl } from './upload-to-presigned-url' -type UseGetAppAssetTreeOptions = { - select?: (data: AppAssetTreeResponse) => TData +export function appAssetTreeOptions(appId: string) { + return consoleQuery.appAsset.tree.queryOptions({ + input: { params: { appId } }, + enabled: !!appId, + }) } -export function useGetAppAssetTree( - appId: string, - options?: UseGetAppAssetTreeOptions, -) { - return useQuery({ - queryKey: consoleQuery.appAsset.tree.queryKey({ input: { params: { appId } } }), - queryFn: () => consoleClient.appAsset.tree({ params: { appId } }), - enabled: !!appId, - select: options?.select, +export function appAssetFileContentOptions(appId: string, nodeId: string) { + return consoleQuery.appAsset.getFileContent.queryOptions({ + input: { params: { appId, nodeId } }, + select: (data) => { + try { + return JSON.parse(data.content) + } + catch { + return { content: data.content } + } + }, + }) +} + +export function appAssetFileDownloadUrlOptions(appId: string, nodeId: string) { + return consoleQuery.appAsset.getFileDownloadUrl.queryOptions({ + input: { params: { appId, nodeId } }, }) } @@ -53,31 +62,6 @@ export const useCreateAppAssetFolder = () => { }) } -export const useGetAppAssetFileContent = (appId: string, nodeId: string, options?: { enabled?: boolean }) => { - return useQuery({ - queryKey: consoleQuery.appAsset.getFileContent.queryKey({ input: { params: { appId, nodeId } } }), - queryFn: () => consoleClient.appAsset.getFileContent({ params: { appId, nodeId } }), - select: (data) => { - try { - const result = JSON.parse(data.content) - return result - } - catch { - return { content: data.content } - } - }, - enabled: (options?.enabled ?? true) && !!appId && !!nodeId, - }) -} - -export const useGetAppAssetFileDownloadUrl = (appId: string, nodeId: string, options?: { enabled?: boolean }) => { - return useQuery({ - queryKey: consoleQuery.appAsset.getFileDownloadUrl.queryKey({ input: { params: { appId, nodeId } } }), - queryFn: () => consoleClient.appAsset.getFileDownloadUrl({ params: { appId, nodeId } }), - enabled: (options?.enabled ?? true) && !!appId && !!nodeId, - }) -} - export const useUpdateAppAssetFileContent = () => { return useMutation({ mutationKey: consoleQuery.appAsset.updateFileContent.mutationKey(), diff --git a/web/service/use-sandbox-file.ts b/web/service/use-sandbox-file.ts index aa7b91705d..51773879bf 100644 --- a/web/service/use-sandbox-file.ts +++ b/web/service/use-sandbox-file.ts @@ -1,68 +1,23 @@ import type { - SandboxFileListQuery, SandboxFileNode, SandboxFileTreeNode, } from '@/types/sandbox-file' -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { skipToken, useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useCallback, useMemo } from 'react' import { consoleClient, consoleQuery } from '@/service/client' -type UseGetSandboxFilesOptions = { - path?: string - recursive?: boolean - enabled?: boolean - refetchInterval?: number | false -} - -type UseSandboxFileDownloadUrlOptions = { - enabled?: boolean - retry?: boolean | number +export function sandboxFileDownloadUrlOptions(appId: string | undefined, path: string | undefined) { + return consoleQuery.sandboxFile.downloadFile.queryOptions({ + input: appId && path + ? { params: { appId }, body: { path } } + : skipToken, + }) } type InvalidateSandboxFilesOptions = { refetchDownloadFile?: boolean } -export function useGetSandboxFiles( - appId: string | undefined, - options?: UseGetSandboxFilesOptions, -) { - const query: SandboxFileListQuery = { - path: options?.path, - recursive: options?.recursive, - } - - return useQuery({ - queryKey: consoleQuery.sandboxFile.listFiles.queryKey({ - input: { params: { appId: appId! }, query }, - }), - queryFn: () => consoleClient.sandboxFile.listFiles({ - params: { appId: appId! }, - query, - }), - enabled: !!appId && (options?.enabled ?? true), - refetchInterval: options?.refetchInterval, - }) -} - -export function useSandboxFileDownloadUrl( - appId: string | undefined, - path: string | undefined, - options?: UseSandboxFileDownloadUrlOptions, -) { - return useQuery({ - queryKey: consoleQuery.sandboxFile.downloadFile.queryKey({ - input: { params: { appId: appId! }, body: { path: path! } }, - }), - queryFn: () => consoleClient.sandboxFile.downloadFile({ - params: { appId: appId! }, - body: { path: path! }, - }), - enabled: !!appId && !!path && (options?.enabled ?? true), - retry: options?.retry, - }) -} - export function useInvalidateSandboxFiles() { const queryClient = useQueryClient() return useCallback((options?: InvalidateSandboxFilesOptions) => { @@ -131,13 +86,21 @@ function buildTreeFromFlatList(nodes: SandboxFileNode[]): SandboxFileTreeNode[] return roots } +type UseSandboxFilesTreeOptions = { + enabled?: boolean + refetchInterval?: number | false +} + export function useSandboxFilesTree( appId: string | undefined, - options?: UseGetSandboxFilesOptions, + options?: UseSandboxFilesTreeOptions, ) { - const { data, isLoading, error, refetch } = useGetSandboxFiles(appId, { - ...options, - recursive: true, + const { data, isLoading, error, refetch } = useQuery({ + ...consoleQuery.sandboxFile.listFiles.queryOptions({ + input: { params: { appId: appId! }, query: { recursive: true } }, + }), + enabled: !!appId && (options?.enabled ?? true), + refetchInterval: options?.refetchInterval, }) const treeData = useMemo(() => {