/** * Upload dropzone for DMS file browser. * Supports drag-and-drop and click-to-select file uploads with progress. */ import React, { useCallback, useRef, useState } from 'react'; import clsx from 'clsx'; import { useTranslation } from 'react-i18next'; import { uploadFile } from '@/api/dms'; import { useToast } from '@/components/ui/Toast'; export interface UploadDropzoneProps { folderId: string | null; onUploaded: () => void; } interface UploadProgress { fileName: string; progress: number; error: string | null; } export function UploadDropzone({ folderId, onUploaded }: UploadDropzoneProps) { const { t } = useTranslation(); const toast = useToast(); const inputRef = useRef(null); const [isDragging, setIsDragging] = useState(false); const [uploads, setUploads] = useState([]); const [isUploading, setIsUploading] = useState(false); const handleFiles = useCallback(async (fileList: FileList) => { const files = Array.from(fileList); if (files.length === 0) return; setIsUploading(true); const progressEntries = files.map((f) => ({ fileName: f.name, progress: 0, error: null as string | null })); setUploads(progressEntries); let allSuccess = true; for (let i = 0; i < files.length; i++) { const file = files[i]; try { setUploads((prev) => prev.map((u, idx) => idx === i ? { ...u, progress: 50 } : u)); await uploadFile(file, folderId); setUploads((prev) => prev.map((u, idx) => idx === i ? { ...u, progress: 100 } : u)); } catch (err) { allSuccess = false; const errorMsg = err instanceof Error ? err.message : String(err); setUploads((prev) => prev.map((u, idx) => idx === i ? { ...u, error: errorMsg } : u)); } } setIsUploading(false); if (allSuccess) { toast.success(t('dms.uploadSuccess')); } else { toast.error(t('dms.uploadError')); } onUploaded(); setTimeout(() => setUploads([]), 3000); }, [folderId, onUploaded, toast, t]); const handleDragOver = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragging(true); }, []); const handleDragLeave = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragging(false); }, []); const handleDrop = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragging(false); if (e.dataTransfer.files.length > 0) { handleFiles(e.dataTransfer.files); } }, [handleFiles]); const handleClick = useCallback(() => { inputRef.current?.click(); }, []); const handleInputChange = useCallback((e: React.ChangeEvent) => { if (e.target.files && e.target.files.length > 0) { handleFiles(e.target.files); e.target.value = ''; } }, [handleFiles]); return (
{ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleClick(); } }} tabIndex={0} role="button" aria-label={t('dms.uploadDropHere')} >

{t('dms.uploadDropHere')}

{uploads.length > 0 && (
{uploads.map((u, i) => (
{u.fileName} {u.error ? ( {u.error} ) : ( {u.progress}% )}
))}
)} {isUploading && (
{t('dms.uploading')}...
)}
); }