From f70d89e80b6369faa0dc07302e9ff2d4be0bae76 Mon Sep 17 00:00:00 2001 From: yyh Date: Fri, 27 Feb 2026 12:42:40 +0800 Subject: [PATCH] refactor(web): remove useSandboxFilesTree and derive hasFiles in components Migrate ArtifactsSection to queryOptions + useQuery composition and derive\nfile tree/hasFiles locally from flat data. Remove the now-unused\nuseSandboxFilesTree helper and update related tests to mock the new\nqueryOptions-based flow. --- .../artifacts/artifacts-section.spec.tsx | 72 +++++++++++-------- .../file-tree/artifacts/artifacts-section.tsx | 9 ++- web/service/use-sandbox-file.ts | 22 +----- 3 files changed, 49 insertions(+), 54 deletions(-) diff --git a/web/app/components/workflow/skill/file-tree/artifacts/artifacts-section.spec.tsx b/web/app/components/workflow/skill/file-tree/artifacts/artifacts-section.spec.tsx index a39472dae9..abb5b1076d 100644 --- a/web/app/components/workflow/skill/file-tree/artifacts/artifacts-section.spec.tsx +++ b/web/app/components/workflow/skill/file-tree/artifacts/artifacts-section.spec.tsx @@ -1,4 +1,4 @@ -import type { SandboxFileDownloadTicket, SandboxFileTreeNode } from '@/types/sandbox-file' +import type { SandboxFileDownloadTicket, SandboxFileNode } from '@/types/sandbox-file' import { fireEvent, render, screen, waitFor } from '@testing-library/react' import ArtifactsSection from './artifacts-section' @@ -12,13 +12,17 @@ const mocks = vi.hoisted(() => ({ appId: 'app-1', selectedArtifactPath: null, } as MockStoreState, - treeData: undefined as SandboxFileTreeNode[] | undefined, - hasFiles: false, + flatData: [] as SandboxFileNode[], isLoading: false, isDownloading: false, selectArtifact: vi.fn(), fetchDownloadUrl: vi.fn<(path: string) => Promise>(), downloadUrl: vi.fn(), + mockUseQuery: vi.fn(), + mockTreeOptions: vi.fn().mockReturnValue({ + queryKey: ['sandboxFile', 'listFiles'], + queryFn: vi.fn(), + }), })) vi.mock('@/app/components/workflow/store', () => ({ @@ -30,12 +34,14 @@ vi.mock('@/app/components/workflow/store', () => ({ }), })) -vi.mock('@/service/use-sandbox-file', () => ({ - useSandboxFilesTree: () => ({ - data: mocks.treeData, - hasFiles: mocks.hasFiles, - isLoading: mocks.isLoading, - }), +vi.mock('@tanstack/react-query', async importOriginal => ({ + ...await importOriginal(), + useQuery: (options: unknown) => mocks.mockUseQuery(options), +})) + +vi.mock('@/service/use-sandbox-file', async importOriginal => ({ + ...(await importOriginal()), + sandboxFilesTreeOptions: (...args: unknown[]) => mocks.mockTreeOptions(...args), useDownloadSandboxFile: () => ({ mutateAsync: mocks.fetchDownloadUrl, isPending: mocks.isDownloading, @@ -46,15 +52,12 @@ vi.mock('@/utils/download', () => ({ downloadUrl: (...args: unknown[]) => mocks.downloadUrl(...args), })) -const createNode = (overrides: Partial = {}): SandboxFileTreeNode => ({ - id: 'node-1', - name: 'report.txt', +const createFlatFileNode = (overrides: Partial = {}): SandboxFileNode => ({ path: 'report.txt', - node_type: 'file', + is_dir: false, size: 1, mtime: 1700000000, extension: 'txt', - children: [], ...overrides, }) @@ -63,8 +66,7 @@ describe('ArtifactsSection', () => { vi.clearAllMocks() mocks.storeState.appId = 'app-1' mocks.storeState.selectedArtifactPath = null - mocks.treeData = undefined - mocks.hasFiles = false + mocks.flatData = [] mocks.isLoading = false mocks.isDownloading = false mocks.fetchDownloadUrl.mockResolvedValue({ @@ -72,6 +74,19 @@ describe('ArtifactsSection', () => { expires_in: 3600, export_id: 'abc123def4567890', }) + mocks.mockUseQuery.mockImplementation((options: { queryKey?: unknown }) => { + const treeKey = mocks.mockTreeOptions.mock.results.at(-1)?.value?.queryKey + if (treeKey && options.queryKey === treeKey) { + return { + data: mocks.flatData, + isLoading: mocks.isLoading, + } + } + return { + data: undefined, + isLoading: false, + } + }) }) // Covers collapsed header rendering and visual indicators. @@ -85,8 +100,7 @@ describe('ArtifactsSection', () => { }) it('should show blue dot when collapsed and files exist', () => { - mocks.hasFiles = true - mocks.treeData = [createNode()] + mocks.flatData = [createFlatFileNode()] const { container } = render() @@ -125,10 +139,9 @@ describe('ArtifactsSection', () => { // Covers real tree integration for selecting and downloading artifacts. describe('Artifacts tree interactions', () => { it('should render file rows and select artifact path when a file is clicked', () => { - const selectedFile = createNode({ id: 'selected', name: 'a.txt', path: 'a.txt' }) - const otherFile = createNode({ id: 'other', name: 'b.txt', path: 'b.txt' }) - mocks.hasFiles = true - mocks.treeData = [selectedFile, otherFile] + const selectedFile = createFlatFileNode({ path: 'a.txt', extension: 'txt' }) + const otherFile = createFlatFileNode({ path: 'b.txt', extension: 'txt' }) + mocks.flatData = [selectedFile, otherFile] mocks.storeState.selectedArtifactPath = 'a.txt' render() @@ -143,9 +156,8 @@ describe('ArtifactsSection', () => { }) it('should request download URL and trigger browser download when file download succeeds', async () => { - const file = createNode({ name: 'export.csv', path: 'export.csv', extension: 'csv' }) - mocks.hasFiles = true - mocks.treeData = [file] + const file = createFlatFileNode({ path: 'export.csv', extension: 'csv' }) + mocks.flatData = [file] mocks.fetchDownloadUrl.mockResolvedValue({ download_url: 'https://example.com/download/export.csv', expires_in: 3600, @@ -168,11 +180,10 @@ describe('ArtifactsSection', () => { }) it('should log error and skip browser download when download request fails', async () => { - const file = createNode({ name: 'broken.bin', path: 'broken.bin', extension: 'bin' }) + const file = createFlatFileNode({ path: 'broken.bin', extension: 'bin' }) const error = new Error('request failed') const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined) - mocks.hasFiles = true - mocks.treeData = [file] + mocks.flatData = [file] mocks.fetchDownloadUrl.mockRejectedValue(error) render() @@ -188,9 +199,8 @@ describe('ArtifactsSection', () => { }) it('should disable download buttons when a download request is pending', () => { - const file = createNode({ name: 'asset.png', path: 'asset.png', extension: 'png' }) - mocks.hasFiles = true - mocks.treeData = [file] + const file = createFlatFileNode({ path: 'asset.png', extension: 'png' }) + mocks.flatData = [file] mocks.isDownloading = true render() diff --git a/web/app/components/workflow/skill/file-tree/artifacts/artifacts-section.tsx b/web/app/components/workflow/skill/file-tree/artifacts/artifacts-section.tsx index 31671e20e2..89af8c6b11 100644 --- a/web/app/components/workflow/skill/file-tree/artifacts/artifacts-section.tsx +++ b/web/app/components/workflow/skill/file-tree/artifacts/artifacts-section.tsx @@ -1,12 +1,13 @@ 'use client' import type { SandboxFileTreeNode } from '@/types/sandbox-file' +import { useQuery } from '@tanstack/react-query' import * as React from 'react' -import { useCallback, useState } from 'react' +import { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import FolderSpark from '@/app/components/base/icons/src/vender/workflow/FolderSpark' import { useStore, useWorkflowStore } from '@/app/components/workflow/store' -import { useDownloadSandboxFile, useSandboxFilesTree } from '@/service/use-sandbox-file' +import { buildTreeFromFlatList, sandboxFilesTreeOptions, useDownloadSandboxFile } from '@/service/use-sandbox-file' import { cn } from '@/utils/classnames' import { downloadUrl } from '@/utils/download' import ArtifactsTree from './artifacts-tree' @@ -21,7 +22,9 @@ const ArtifactsSection = ({ className }: ArtifactsSectionProps) => { const [isExpanded, setIsExpanded] = useState(false) - const { data: treeData, hasFiles, isLoading } = useSandboxFilesTree(appId) + const { data: flatData, isLoading } = useQuery(sandboxFilesTreeOptions(appId)) + const treeData = useMemo(() => flatData ? buildTreeFromFlatList(flatData) : undefined, [flatData]) + const hasFiles = (flatData?.length ?? 0) > 0 const { mutateAsync: fetchDownloadUrl, isPending: isDownloading } = useDownloadSandboxFile(appId) const storeApi = useWorkflowStore() diff --git a/web/service/use-sandbox-file.ts b/web/service/use-sandbox-file.ts index 4fd59bc235..a22ef8e309 100644 --- a/web/service/use-sandbox-file.ts +++ b/web/service/use-sandbox-file.ts @@ -2,8 +2,8 @@ import type { SandboxFileNode, SandboxFileTreeNode, } from '@/types/sandbox-file' -import { skipToken, useMutation, useQuery, useQueryClient } from '@tanstack/react-query' -import { useCallback, useMemo } from 'react' +import { skipToken, useMutation, useQueryClient } from '@tanstack/react-query' +import { useCallback } from 'react' import { consoleClient, consoleQuery } from '@/service/client' export function sandboxFileDownloadUrlOptions(appId: string | undefined, path: string | undefined) { @@ -93,21 +93,3 @@ export function buildTreeFromFlatList(nodes: SandboxFileNode[]): SandboxFileTree return roots } - -export function useSandboxFilesTree(appId: string | undefined) { - const { data, isLoading, error } = useQuery(sandboxFilesTreeOptions(appId)) - - const treeData = useMemo(() => { - if (!data) - return undefined - return buildTreeFromFlatList(data) - }, [data]) - - return { - data: treeData, - flatData: data, - hasFiles: (data?.length ?? 0) > 0, - isLoading, - error, - } -}