This commit is contained in:
Rodolfo Martinez 2025-12-05 12:24:07 -05:00
parent 81f60acd06
commit 8380cb8e6a
36 changed files with 5342 additions and 4434 deletions

View file

@ -17,6 +17,8 @@ import {
useUIXTheme,
Breadcrumb,
Card,
Select,
Checkbox,
} from "../../../../components/UIX";
import {
CloudArrowUpIcon,
@ -63,7 +65,8 @@ const FileUpload = () => {
preSelectedCollectionId || "",
);
const [availableCollections, setAvailableCollections] = useState([]);
const [isLoadingCollections, setIsLoadingCollections] = useState(false);
const [isLoadingCollections, setIsLoadingCollections] = useState(true); // Start with loading true
const [isCollectionsInitialized, setIsCollectionsInitialized] = useState(false);
const [isUploading, setIsUploading] = useState(false);
const [isDragging, setIsDragging] = useState(false);
const [error, setError] = useState("");
@ -104,19 +107,24 @@ const FileUpload = () => {
}, [authManager]);
const loadCollections = useCallback(async () => {
if (!listCollectionManager) return;
setIsLoadingCollections(true);
try {
const result = await listCollectionManager.listCollections(false);
if (
result.collections &&
result.collections.length > 0 &&
isMountedRef.current
) {
setAvailableCollections(result.collections);
if (isMountedRef.current) {
if (result.collections && result.collections.length > 0) {
setAvailableCollections(result.collections);
}
setIsCollectionsInitialized(true);
}
} catch (err) {
if (isMountedRef.current) {
if (import.meta.env.DEV) {
console.error("[FileUpload] Could not load folders:", err);
}
setError("Could not load folders");
setIsCollectionsInitialized(true);
}
} finally {
if (isMountedRef.current) {
@ -125,11 +133,12 @@ const FileUpload = () => {
}
}, [listCollectionManager]);
// Load collections when manager becomes available
useEffect(() => {
if (createCollectionManager && listCollectionManager) {
if (createCollectionManager && listCollectionManager && !isCollectionsInitialized) {
loadCollections();
}
}, [createCollectionManager, listCollectionManager, loadCollections]);
}, [createCollectionManager, listCollectionManager, isCollectionsInitialized, loadCollections]);
const handleDragOver = useCallback((e) => {
e.preventDefault();
@ -665,6 +674,14 @@ const FileUpload = () => {
errorFiles,
} = fileStats;
// Memoize collection options for Select component
const collectionOptions = useMemo(() => {
return availableCollections.map((collection) => ({
value: collection.id,
label: collection.name || "Unnamed Folder",
}));
}, [availableCollections]);
// Build breadcrumb
const breadcrumbItems = [
{
@ -756,39 +773,73 @@ const FileUpload = () => {
{/* Content */}
<div className="px-6 pb-6 pt-5">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div className={`grid grid-cols-1 gap-8 ${preSelectedCollectionId ? "lg:grid-cols-3" : ""}`}>
{/* Upload Area */}
<div className="lg:col-span-2 space-y-6">
{/* Collection Selector (only show if no pre-selected collection) */}
<div className={`space-y-6 ${preSelectedCollectionId ? "lg:col-span-2" : ""}`}>
{/* Collection Selector with Upload Button (only show if no pre-selected collection) */}
{!preSelectedCollectionId && (
<div>
<label
className={`block text-sm font-medium mb-2 ${getThemeClasses("text-primary")}`}
>
Select destination folder
</label>
<select
<div className="flex items-end gap-3">
<Select
label="Select destination folder"
value={selectedCollection}
onChange={(e) => setSelectedCollection(e.target.value)}
onChange={setSelectedCollection}
options={collectionOptions}
disabled={isLoadingCollections || isUploading}
className={`w-full px-4 py-3 border ${getThemeClasses("border-secondary")} rounded-lg ${getThemeClasses("bg-card")} ${getThemeClasses("text-primary")} focus:ring-2 ${getThemeClasses("focus:ring-primary")} ${getThemeClasses("focus:border-primary")} transition-all duration-200`}
>
<option value="">Choose a folder...</option>
{availableCollections.map((collection) => (
<option key={collection.id} value={collection.id}>
{collection.name || "Unnamed Folder"}
</option>
))}
</select>
placeholder="Choose a folder..."
size="md"
className="flex-1"
/>
<Button
onClick={startUpload}
disabled={
!selectedCollection ||
!fileManager ||
files.length === 0 ||
isUploading ||
pendingFiles.length === 0 ||
!gdprConsent
}
variant="primary"
className="flex-shrink-0 h-[50px]"
>
<span className="inline-flex items-center justify-center gap-2">
{isUploading ? (
<>
<div className="animate-spin rounded-full h-5 w-5 border-2 border-white border-t-transparent"></div>
<span>
Uploading {uploadingFiles.length} of {files.length}...
</span>
</>
) : (
<>
<ArrowUpTrayIcon className="h-5 w-5" />
<span>
Upload {pendingFiles.length} File
{pendingFiles.length !== 1 ? "s" : ""}
</span>
</>
)}
</span>
</Button>
</div>
)}
{/* Drop Zone */}
<div
role="button"
tabIndex={0}
aria-label={isDragging ? "Drop files here to upload" : "Click or drag files here to upload"}
aria-disabled={isUploading}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
onClick={() => !isUploading && fileInputRef.current?.click()}
onKeyDown={(e) => {
if ((e.key === "Enter" || e.key === " ") && !isUploading) {
e.preventDefault();
fileInputRef.current?.click();
}
}}
className={`${getThemeClasses("bg-muted")} rounded-lg border-2 border-dashed p-12 text-center cursor-pointer transition-all duration-300 ${
isDragging
? `${getThemeClasses("border-primary")} ${getThemeClasses("bg-accent-light")} scale-105 shadow-lg`
@ -833,6 +884,8 @@ const FileUpload = () => {
onChange={handleFileSelect}
disabled={isUploading}
className="sr-only"
aria-label="Select files to upload"
id="file-upload-input"
/>
</div>
@ -860,11 +913,15 @@ const FileUpload = () => {
</div>
<div
role="list"
aria-label="Selected files for upload"
className={`divide-y ${getThemeClasses("border-secondary")} max-h-96 overflow-y-auto`}
>
{files.map((file) => (
<div
key={file.id}
role="listitem"
aria-label={`${file.name}, ${formatFileSize(file.size)}, status: ${file.status}`}
className={`p-4 ${getThemeClasses("hover:bg-muted")} transition-colors duration-200`}
>
<div className="flex items-center space-x-4">
@ -919,16 +976,19 @@ const FileUpload = () => {
</div>
<div className="flex-shrink-0">
{file.status === "pending" && (
<button
<Button
onClick={(e) => {
e.stopPropagation();
removeFile(file.id);
}}
disabled={isUploading}
className={`p-2 ${getThemeClasses("text-muted")} hover:opacity-80 ${getThemeClasses("alert-error-bg")} rounded-lg transition-all duration-200`}
variant="ghost"
size="sm"
aria-label={`Remove ${file.name}`}
className={`${getThemeClasses("hover:text-error")}`}
>
<XMarkIcon className="h-5 w-5" />
</button>
</Button>
)}
{file.status === "uploading" && (
<div className="p-2">
@ -960,7 +1020,8 @@ const FileUpload = () => {
)}
</div>
{/* Sidebar */}
{/* Sidebar - only show as separate column when collection is pre-selected */}
{preSelectedCollectionId && (
<div className="space-y-6">
{files.length > 0 && (
<div
@ -1077,12 +1138,11 @@ const FileUpload = () => {
<div
className={`p-4 ${getThemeClasses("bg-accent-light")} rounded-lg border ${getThemeClasses("border-accent")}`}
>
<label className="flex items-start space-x-3 cursor-pointer">
<input
type="checkbox"
<div className="flex items-start space-x-3">
<Checkbox
checked={gdprConsent}
onChange={(e) => setGdprConsent(e.target.checked)}
className={`mt-1 h-4 w-4 rounded ${getThemeClasses("checkbox-focus")}`}
onChange={setGdprConsent}
className="mt-0.5"
/>
<div className="flex-1">
<p
@ -1097,10 +1157,11 @@ const FileUpload = () => {
you share with can decrypt them.
</p>
</div>
</label>
</div>
</div>
)}
{/* Upload button in sidebar */}
<Button
onClick={startUpload}
disabled={
@ -1134,7 +1195,150 @@ const FileUpload = () => {
</span>
</Button>
</div>
)}
</div>
{/* Options section - show below content when no pre-selected collection */}
{!preSelectedCollectionId && files.length > 0 && (
<div className="mt-6 grid grid-cols-1 md:grid-cols-3 gap-6">
{/* Upload Status */}
<div
className={`${getThemeClasses("bg-card")} rounded-lg border ${getThemeClasses("border-secondary")} shadow-sm p-6`}
>
<h3
className={`font-semibold mb-4 ${getThemeClasses("text-primary")}`}
>
Upload Status
</h3>
<div className="space-y-3">
<div className="flex items-center justify-between">
<span
className={`text-sm ${getThemeClasses("text-secondary")}`}
>
Pending
</span>
<span
className={`text-sm font-medium ${getThemeClasses("text-primary")}`}
>
{pendingFiles.length}
</span>
</div>
<div className="flex items-center justify-between">
<span
className={`text-sm ${getThemeClasses("text-secondary")}`}
>
Uploading
</span>
<span
className={`text-sm font-medium ${getThemeClasses("text-info")}`}
>
{uploadingFiles.length}
</span>
</div>
<div className="flex items-center justify-between">
<span
className={`text-sm ${getThemeClasses("text-secondary")}`}
>
Completed
</span>
<span
className={`text-sm font-medium ${getThemeClasses("text-success")}`}
>
{completedFiles.length}
</span>
</div>
{errorFiles.length > 0 && (
<div className="flex items-center justify-between">
<span
className={`text-sm ${getThemeClasses("text-secondary")}`}
>
Errors
</span>
<span
className={`text-sm font-medium ${getThemeClasses("text-error")}`}
>
{errorFiles.length}
</span>
</div>
)}
</div>
{completedFiles.length + errorFiles.length ===
files.length &&
files.length > 0 &&
!isUploading &&
pendingFiles.length === 0 && (
<div
className={`mt-4 p-3 ${getThemeClasses("alert-success-bg")} border ${getThemeClasses("alert-success-border")} rounded-lg`}
>
<p
className={`text-sm ${getThemeClasses("text-success")} flex items-center`}
>
<CheckCircleIcon className="h-4 w-4 mr-2" />
All uploads processed!
</p>
</div>
)}
</div>
{/* Tag Selection */}
{pendingFiles.length > 0 && (
<div
className={`${getThemeClasses("bg-card")} rounded-lg border ${getThemeClasses("border-secondary")} shadow-sm p-6`}
>
<div className="flex items-center gap-2 mb-4">
<TagIcon
className={`h-5 w-5 ${getThemeClasses("text-secondary")}`}
/>
<h3
className={`font-semibold ${getThemeClasses("text-primary")}`}
>
Add Tags
</h3>
</div>
<p
className={`text-xs ${getThemeClasses("text-secondary")} mb-3`}
>
Tags will be applied to all uploaded files
</p>
<TagSelector
value={selectedTagIds}
onChange={setSelectedTagIds}
disabled={isUploading}
label=""
placeholder="Select tags..."
/>
</div>
)}
{/* Consent & Security Notice */}
{pendingFiles.length > 0 && (
<div
className={`p-4 ${getThemeClasses("bg-accent-light")} rounded-lg border ${getThemeClasses("border-accent")} h-fit`}
>
<div className="flex items-start space-x-3">
<Checkbox
checked={gdprConsent}
onChange={setGdprConsent}
className="mt-0.5"
/>
<div className="flex-1">
<p
className={`text-sm font-medium ${getThemeClasses("text-primary")}`}
>
I consent to encrypted upload
</p>
<p
className={`text-xs ${getThemeClasses("text-secondary")} mt-1`}
>
Files are encrypted end-to-end and only you and those
you share with can decrypt them.
</p>
</div>
</div>
</div>
)}
</div>
)}
</div>
</Card>
</div>