From ae4645e01b73ea5c3fcd95bcb8efd52cc0086e1e Mon Sep 17 00:00:00 2001 From: chanx <1243304602@qq.com> Date: Fri, 6 Mar 2026 20:17:29 +0800 Subject: [PATCH] Fix: Add folder upload #9743 (#13448) ### What problem does this PR solve? Fix: Add folder upload #9743 ### Type of change - [x] Bug Fix (non-breaking change which fixes an issue) --- .../components/file-upload-dialog/index.tsx | 12 +- web/src/components/file-uploader.tsx | 206 ++++++++++++------ web/src/locales/en.ts | 2 + web/src/locales/zh.ts | 2 + 4 files changed, 155 insertions(+), 67 deletions(-) diff --git a/web/src/components/file-upload-dialog/index.tsx b/web/src/components/file-upload-dialog/index.tsx index ba90d3fa4..d7239e2e8 100644 --- a/web/src/components/file-upload-dialog/index.tsx +++ b/web/src/components/file-upload-dialog/index.tsx @@ -20,8 +20,16 @@ import { Switch } from '../ui/switch'; function buildUploadFormSchema(t: TFunction) { const FormSchema = z.object({ parseOnCreation: z.boolean().optional(), + // Update schema to allow files with path property to handle folder uploads fileList: z - .array(z.instanceof(File)) + .array( + z.instanceof(File).or( + z.object({ + file: z.instanceof(File), + path: z.string(), // Store the relative path for files in folders + }), + ), + ) .min(1, { message: t('fileManager.pleaseUploadAtLeastOneFile') }), }); @@ -72,7 +80,7 @@ function UploadForm({ submit, showParseOnCreation }: UploadFormProps) { )} )} - + {(field) => ( (null); + const reachesMaxFileCount = (files?.length ?? 0) >= maxFileCount; - const onDrop = React.useCallback( + const processFiles = React.useCallback( (acceptedFiles: File[], rejectedFiles: FileRejection[]) => { if (!multiple && maxFileCount === 1 && acceptedFiles.length > 1) { toast.error('Cannot upload more than 1 file at a time'); @@ -216,11 +219,16 @@ export function FileUploader(props: FileUploaderProps) { return; } - const newFiles = acceptedFiles.map((file) => - Object.assign(file, { - preview: URL.createObjectURL(file), - }), - ); + const newFiles = acceptedFiles.map((file) => { + const enhancedFile = file as File & { preview?: string }; + Object.defineProperty(enhancedFile, 'preview', { + value: URL.createObjectURL(file), + writable: true, + enumerable: true, + configurable: true, + }); + return enhancedFile; + }); const updatedFiles = files ? [...files, ...newFiles] : newFiles; @@ -250,10 +258,26 @@ export function FileUploader(props: FileUploaderProps) { }); } }, - [files, maxFileCount, multiple, onUpload, setFiles], ); + const onDrop = React.useCallback( + (acceptedFiles: File[], rejectedFiles: FileRejection[]) => { + processFiles(acceptedFiles, rejectedFiles); + }, + [processFiles], + ); + + const handleFolderSelect = React.useCallback( + (e: React.ChangeEvent) => { + if (!e.target.files) return; + const fileList = Array.from(e.target.files); + processFiles(fileList, []); + e.target.value = ''; + }, + [processFiles], + ); + function onRemove(index: number) { if (!files) return; const newFiles = files.filter((_, i) => i !== index); @@ -276,68 +300,120 @@ export function FileUploader(props: FileUploaderProps) { const isDisabled = disabled || (files?.length ?? 0) >= maxFileCount; - return ( -
- {!(hideDropzoneOnMaxFileCount && reachesMaxFileCount) && ( - 1 || multiple} - disabled={isDisabled} + const renderDropzone = (isFolderMode: boolean = false) => ( + 1 || multiple} + disabled={isDisabled} + noClick={isFolderMode} + noDrag={isFolderMode} + > + {({ getRootProps, getInputProps, isDragActive }) => ( +
- {({ getRootProps, getInputProps, isDragActive }) => ( + {!isFolderMode && } + {isDragActive && !isFolderMode ? ( +
+
+
+

+ {t('fileManager.dropFilesHere', 'Drop the files here')} +

+
+ ) : (
{ + if (isFolderMode && !isDisabled) { + folderInputRef.current?.click(); + } + }} > - - {isDragActive ? ( -
-
-
-

- Drop the files here -

-
- ) : ( -
-
-
-
-

- {title || t('knowledgeDetails.uploadTitle')} -

-

- {description || t('knowledgeDetails.uploadDescription')} - {/* You can upload - {maxFileCount > 1 - ? ` ${maxFileCount === Infinity ? 'multiple' : maxFileCount} - files (up to ${formatBytes(maxSize)} each)` - : ` a file with ${formatBytes(maxSize)}`} */} -

-
-
- )} +
+ {isFolderMode ? ( +
+
+

+ {title || + (isFolderMode + ? t('fileManager.uploadFolderTitle', 'Upload Folder') + : t('knowledgeDetails.uploadTitle'))} +

+

+ {description || + (isFolderMode + ? t( + 'knowledgeDetails.uploadDescription', + 'Select a folder to upload all files inside', + ) + : t('knowledgeDetails.uploadDescription'))} +

+
)} - +
+ )} +
+ ); + + return ( +
+ {!(hideDropzoneOnMaxFileCount && reachesMaxFileCount) && ( + + + + + {t('fileManager.files', 'Files')} + + + + {t('fileManager.folder', 'Folder')} + + + + {renderDropzone(false)} + + + {renderDropzone(true)} + + + )} {files?.length ? ( diff --git a/web/src/locales/en.ts b/web/src/locales/en.ts index 0977e4edd..a071391f1 100644 --- a/web/src/locales/en.ts +++ b/web/src/locales/en.ts @@ -1446,6 +1446,8 @@ Example: Virtual Hosted Style`, hint: 'hint', }, fileManager: { + uploadFolderTitle: 'Upload folder', + folder: 'Folder', files: 'Files', name: 'Name', uploadDate: 'Upload date', diff --git a/web/src/locales/zh.ts b/web/src/locales/zh.ts index 8326a673d..8a278177f 100644 --- a/web/src/locales/zh.ts +++ b/web/src/locales/zh.ts @@ -1200,6 +1200,8 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于 hint: '提示', }, fileManager: { + uploadFolderTitle: '上传文件夹', + folder: '文件夹', files: '文件', name: '名称', uploadDate: '上传日期',