mirror of
https://github.com/langgenius/dify.git
synced 2026-03-01 22:08:41 +08:00
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.
This commit is contained in:
@ -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<SandboxFileDownloadTicket>>(),
|
||||
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<typeof import('@tanstack/react-query')>(),
|
||||
useQuery: (options: unknown) => mocks.mockUseQuery(options),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-sandbox-file', async importOriginal => ({
|
||||
...(await importOriginal<typeof import('@/service/use-sandbox-file')>()),
|
||||
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> = {}): SandboxFileTreeNode => ({
|
||||
id: 'node-1',
|
||||
name: 'report.txt',
|
||||
const createFlatFileNode = (overrides: Partial<SandboxFileNode> = {}): 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(<ArtifactsSection />)
|
||||
|
||||
@ -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(<ArtifactsSection />)
|
||||
@ -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(<ArtifactsSection />)
|
||||
@ -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(<ArtifactsSection />)
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user