From 6408300c3502340ed18d964ec1ea87557d796427 Mon Sep 17 00:00:00 2001 From: yyh Date: Thu, 26 Mar 2026 14:59:06 +0800 Subject: [PATCH] feat: implement batch upload operation and integrate with create operations --- .../operations/use-batch-upload-operation.ts | 147 ++++++++++++++++++ .../operations/use-create-operations.ts | 4 +- web/service/use-app-asset.ts | 12 -- 3 files changed, 149 insertions(+), 14 deletions(-) create mode 100644 web/app/components/workflow/skill/hooks/file-tree/operations/use-batch-upload-operation.ts diff --git a/web/app/components/workflow/skill/hooks/file-tree/operations/use-batch-upload-operation.ts b/web/app/components/workflow/skill/hooks/file-tree/operations/use-batch-upload-operation.ts new file mode 100644 index 0000000000..6a7803ece6 --- /dev/null +++ b/web/app/components/workflow/skill/hooks/file-tree/operations/use-batch-upload-operation.ts @@ -0,0 +1,147 @@ +'use client' + +import type { BatchUploadNodeInput, BatchUploadNodeOutput } from '@/types/app-asset' +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { consoleClient, consoleQuery } from '@/service/client' +import { uploadToPresignedUrl } from '@/service/upload-to-presigned-url' +import { useBatchUpload } from '@/service/use-app-asset' + +type BatchUploadOperationVariables = { + appId: string + tree: BatchUploadNodeInput[] + files: Map + parentId?: string | null + onProgress?: (uploaded: number, total: number) => void +} + +type BatchUploadTask = { + file: File + url: string +} + +const uploadBatchUploadTasks = async ({ + tasks, + onProgress, +}: { + tasks: BatchUploadTask[] + onProgress?: (uploaded: number, total: number) => void +}) => { + let completed = 0 + const total = tasks.length + + await Promise.all( + tasks.map(async (task) => { + await uploadToPresignedUrl({ + file: task.file, + uploadUrl: task.url, + }) + completed++ + onProgress?.(completed, total) + }), + ) +} + +const createBatchUploadTreeInParent = async ({ + appId, + tree, + files, + parentId, + pathPrefix = '', +}: { + appId: string + tree: BatchUploadNodeInput[] + files: Map + parentId: string + pathPrefix?: string +}): Promise<{ nodes: BatchUploadNodeOutput[], tasks: BatchUploadTask[] }> => { + const nodes: BatchUploadNodeOutput[] = [] + const tasks: BatchUploadTask[] = [] + + for (const inputNode of tree) { + const sourcePath = pathPrefix ? `${pathPrefix}/${inputNode.name}` : inputNode.name + + if (inputNode.node_type === 'folder') { + const folder = await consoleClient.appAsset.createFolder({ + params: { appId }, + body: { name: inputNode.name, parent_id: parentId }, + }) + + const childrenResult = await createBatchUploadTreeInParent({ + appId, + tree: inputNode.children ?? [], + files, + parentId: folder.id, + pathPrefix: sourcePath, + }) + + nodes.push({ + id: folder.id, + name: folder.name, + node_type: folder.node_type, + size: folder.size, + children: childrenResult.nodes, + }) + tasks.push(...childrenResult.tasks) + continue + } + + const file = files.get(sourcePath) + if (!file) + throw new Error(`Missing file for batch upload path: ${sourcePath}`) + + const { node, upload_url } = await consoleClient.appAsset.getFileUploadUrl({ + params: { appId }, + body: { + name: inputNode.name, + size: inputNode.size ?? file.size, + parent_id: parentId, + }, + }) + + nodes.push({ + id: node.id, + name: node.name, + node_type: node.node_type, + size: node.size, + children: [], + upload_url, + }) + tasks.push({ file, url: upload_url }) + } + + return { nodes, tasks } +} + +export function useBatchUploadOperation() { + const queryClient = useQueryClient() + const batchUpload = useBatchUpload() + + return useMutation({ + mutationKey: consoleQuery.appAsset.batchUpload.mutationKey(), + mutationFn: async (variables: BatchUploadOperationVariables): Promise => { + if (!variables.parentId) + return batchUpload.mutateAsync(variables) + + try { + const result = await createBatchUploadTreeInParent({ + appId: variables.appId, + tree: variables.tree, + files: variables.files, + parentId: variables.parentId, + }) + + await uploadBatchUploadTasks({ + tasks: result.tasks, + onProgress: variables.onProgress, + }) + + return result.nodes + } + finally { + await queryClient.invalidateQueries({ + queryKey: consoleQuery.appAsset.tree.key({ type: 'query', input: { params: { appId: variables.appId } } }), + }) + } + }, + }) +} diff --git a/web/app/components/workflow/skill/hooks/file-tree/operations/use-create-operations.ts b/web/app/components/workflow/skill/hooks/file-tree/operations/use-create-operations.ts index de44bfd6a0..a7cb8150de 100644 --- a/web/app/components/workflow/skill/hooks/file-tree/operations/use-create-operations.ts +++ b/web/app/components/workflow/skill/hooks/file-tree/operations/use-create-operations.ts @@ -5,12 +5,12 @@ import type { SkillEditorSliceShape } from '@/app/components/workflow/store/work import type { BatchUploadNodeInput } from '@/types/app-asset' import { useCallback, useRef } from 'react' import { - useBatchUpload, useCreateAppAssetFolder, useUploadFileWithPresignedUrl, } from '@/service/use-app-asset' import { prepareSkillUploadFile } from '../../../utils/skill-upload-utils' import { useSkillTreeUpdateEmitter } from '../data/use-skill-tree-collaboration' +import { useBatchUploadOperation } from './use-batch-upload-operation' type UseCreateOperationsOptions = { parentId: string | null @@ -34,7 +34,7 @@ export function useCreateOperations({ const { isPending: isCreateFolderPending } = useCreateAppAssetFolder() const { mutateAsync: uploadFileAsync, isPending: isUploadFilePending } = useUploadFileWithPresignedUrl() - const { mutateAsync: batchUploadAsync, isPending: isBatchUploadPending } = useBatchUpload() + const { mutateAsync: batchUploadAsync, isPending: isBatchUploadPending } = useBatchUploadOperation() const emitTreeUpdate = useSkillTreeUpdateEmitter() const handleNewFile = useCallback(() => { diff --git a/web/service/use-app-asset.ts b/web/service/use-app-asset.ts index 9acddb907e..a900cf5e64 100644 --- a/web/service/use-app-asset.ts +++ b/web/service/use-app-asset.ts @@ -271,7 +271,6 @@ export const useBatchUpload = () => { appId, tree, files, - parentId, onProgress, }: { appId: string @@ -285,17 +284,6 @@ export const useBatchUpload = () => { body: { children: tree }, }) - if (parentId) { - await Promise.all( - response.children.map(node => - consoleClient.appAsset.moveNode({ - params: { appId, nodeId: node.id }, - body: { parent_id: parentId }, - }), - ), - ) - } - const uploadTasks: Array<{ path: string, file: File, url: string }> = [] const extractUploads = (nodes: BatchUploadNodeOutput[], pathPrefix: string = '') => {