refactor(web): replace query hooks with queryOptions factories (#32520)

This commit is contained in:
yyh
2026-02-25 00:31:25 +08:00
committed by GitHub
parent f13f5a8882
commit bad48e5deb
11 changed files with 185 additions and 257 deletions

View File

@@ -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,
})

View File

@@ -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>
}

View File

@@ -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()

View File

@@ -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)

View File

@@ -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'

View File

@@ -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)
})
})
})

View File

@@ -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 (

View File

@@ -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])
})
})
})

View File

@@ -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')

View 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(),

View File

@@ -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(() => {