feat: add file tree upload progress

Users need a visible upload path from the explorer itself, not only drag and
 drop behavior with no progress feedback. Routing picker and drop uploads
 through one XHR-backed hook keeps progress, validation, refresh, and success
 counts consistent for every upload source.

The 200MB limit is mirrored in the client, multer, and nginx template so large
 uploads fail predictably instead of being blocked by whichever layer sees the
 request first. The server also returns explicit requested and uploaded counts
 so partial or multi-file batches can render accurate status text.
This commit is contained in:
Haileyesus
2026-06-08 14:52:09 +03:00
parent dd77649053
commit c235b05e1d
7 changed files with 535 additions and 107 deletions

View File

@@ -1,7 +1,11 @@
import { ChevronDown, Eye, FileText, FolderPlus, List, RefreshCw, Search, TableProperties, X } from 'lucide-react';
import { useRef } from 'react';
import type { ChangeEvent } from 'react';
import { ChevronDown, Eye, FileText, FolderPlus, List, Loader2, RefreshCw, Search, TableProperties, Upload, X } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { Button, Input } from '../../../shared/view/ui';
import { cn } from '../../../lib/utils';
import { MAX_FILE_UPLOAD_SIZE_LABEL } from '../constants/constants';
import type { FileTreeViewMode } from '../types/types';
type FileTreeHeaderProps = {
@@ -12,11 +16,14 @@ type FileTreeHeaderProps = {
// Toolbar actions
onNewFile?: () => void;
onNewFolder?: () => void;
onUploadFiles?: (files: FileList) => void;
onRefresh?: () => void;
onCollapseAll?: () => void;
// Loading state
loading?: boolean;
operationLoading?: boolean;
isUploading?: boolean;
uploadProgress?: number | null;
};
export default function FileTreeHeader({
@@ -26,12 +33,24 @@ export default function FileTreeHeader({
onSearchQueryChange,
onNewFile,
onNewFolder,
onUploadFiles,
onRefresh,
onCollapseAll,
loading,
operationLoading,
isUploading,
uploadProgress,
}: FileTreeHeaderProps) {
const { t } = useTranslation();
const uploadInputRef = useRef<HTMLInputElement>(null);
const handleUploadInputChange = (event: ChangeEvent<HTMLInputElement>) => {
const { files } = event.target;
if (files && files.length > 0) {
onUploadFiles?.(files);
}
event.target.value = '';
};
return (
<div className="space-y-2 border-b border-border px-3 pb-2 pt-3">
@@ -40,6 +59,50 @@ export default function FileTreeHeader({
<h3 className="text-sm font-medium text-foreground">{t('fileTree.files')}</h3>
<div className="flex items-center gap-0.5">
{/* Action buttons */}
{onUploadFiles && (
<>
<input
ref={uploadInputRef}
type="file"
multiple
className="hidden"
onChange={handleUploadInputChange}
tabIndex={-1}
aria-hidden="true"
/>
<Button
variant="ghost"
size="sm"
className="relative h-7 w-7 p-0"
onClick={() => uploadInputRef.current?.click()}
title={
isUploading
? t('fileTree.uploadingFiles', 'Uploading files')
: t('fileTree.uploadFiles', 'Upload files (max {{size}} each)', {
size: MAX_FILE_UPLOAD_SIZE_LABEL,
})
}
aria-label={t('fileTree.uploadFiles', 'Upload files (max {{size}} each)', {
size: MAX_FILE_UPLOAD_SIZE_LABEL,
})}
disabled={operationLoading}
>
{isUploading ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<Upload className="h-3.5 w-3.5" />
)}
{isUploading && typeof uploadProgress === 'number' && (
<span className="absolute bottom-0.5 left-1/2 h-0.5 w-4 -translate-x-1/2 overflow-hidden rounded-full bg-primary/20">
<span
className="block h-full rounded-full bg-primary transition-[width] duration-150"
style={{ width: `${uploadProgress}%` }}
/>
</span>
)}
</Button>
</>
)}
{onNewFile && (
<Button
variant="ghost"