mirror of
https://github.com/langgenius/dify.git
synced 2026-03-06 15:45:14 +00:00
refactor(web): replace query hooks with queryOptions factories (#32520)
This commit is contained in:
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
@@ -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<typeof import('@tanstack/react-query')>(),
|
||||
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<AppSSO>,
|
||||
})
|
||||
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<string, AppAssetTreeView>
|
||||
}
|
||||
|
||||
@@ -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<string, AppAssetTreeView>
|
||||
}
|
||||
|
||||
@@ -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<string>
|
||||
}
|
||||
|
||||
@@ -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<string>
|
||||
}
|
||||
|
||||
|
||||
@@ -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<string, AppAssetTreeView> => {
|
||||
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<string> => {
|
||||
if (!data?.children)
|
||||
return new Set()
|
||||
|
||||
@@ -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<typeof import('@tanstack/react-query')>(),
|
||||
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)
|
||||
|
||||
@@ -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<typeof useGetAppAssetFileContent>['data']
|
||||
downloadUrlData: ReturnType<typeof useGetAppAssetFileDownloadUrl>['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'
|
||||
|
||||
@@ -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<typeof import('@tanstack/react-query')>(),
|
||||
useQuery: (options: unknown) => mocks.mockUseQuery(options),
|
||||
}))
|
||||
|
||||
const renderPanel = () => {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ArtifactContentPanel />
|
||||
</QueryClientProvider>,
|
||||
)
|
||||
}
|
||||
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(<ArtifactContentPanel />)
|
||||
|
||||
// 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(<ArtifactContentPanel />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('workflow.skillSidebar.loadError')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render preview panel when ticket contains download url', () => {
|
||||
// Act
|
||||
renderPanel()
|
||||
render(<ArtifactContentPanel />)
|
||||
|
||||
// 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(<ArtifactContentPanel />)
|
||||
|
||||
// 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(<ArtifactContentPanel />)
|
||||
|
||||
// Assert
|
||||
expect(mocks.useSandboxFileDownloadUrl).toHaveBeenCalledWith('app-1', undefined)
|
||||
expect(mocks.mockDownloadUrlOptions).toHaveBeenCalledWith('app-1', undefined)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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<typeof import('@tanstack/react-query')>(),
|
||||
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(<ArtifactsTab {...headerProps} />)
|
||||
|
||||
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])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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<TData = AppAssetTreeResponse> = {
|
||||
select?: (data: AppAssetTreeResponse) => TData
|
||||
export function appAssetTreeOptions(appId: string) {
|
||||
return consoleQuery.appAsset.tree.queryOptions({
|
||||
input: { params: { appId } },
|
||||
enabled: !!appId,
|
||||
})
|
||||
}
|
||||
|
||||
export function useGetAppAssetTree<TData = AppAssetTreeResponse>(
|
||||
appId: string,
|
||||
options?: UseGetAppAssetTreeOptions<TData>,
|
||||
) {
|
||||
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(),
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
Reference in New Issue
Block a user