From dd60c49bb852db58a382d8c770180e62efeff139 Mon Sep 17 00:00:00 2001 From: Shannon Sands Date: Tue, 9 Jun 2026 12:21:09 +1000 Subject: [PATCH] Add dashboard file drop upload panel --- web/src/pages/FilesPage.tsx | 78 ++++++++++++++++++++++++++++++++++++- 1 file changed, 77 insertions(+), 1 deletion(-) diff --git a/web/src/pages/FilesPage.tsx b/web/src/pages/FilesPage.tsx index d2947e098e1..702666b8f13 100644 --- a/web/src/pages/FilesPage.tsx +++ b/web/src/pages/FilesPage.tsx @@ -1,4 +1,10 @@ -import { useCallback, useEffect, useRef, useState } from "react"; +import { + useCallback, + useEffect, + useRef, + useState, + type DragEvent as ReactDragEvent, +} from "react"; import { ArrowUp, Download, @@ -69,15 +75,21 @@ function displayPath(path: string | null | undefined): string { return path?.trim() || "Files"; } +function transferHasFiles(event: ReactDragEvent): boolean { + return Array.from(event.dataTransfer.types).includes("Files"); +} + export default function FilesPage() { const { toast, showToast } = useToast(); const { setAfterTitle, setEnd } = usePageHeader(); const fileInputRef = useRef(null); + const dragDepthRef = useRef(0); const [currentPath, setCurrentPath] = useState(undefined); const [pathInput, setPathInput] = useState(""); const [listing, setListing] = useState(null); const [loading, setLoading] = useState(false); const [uploading, setUploading] = useState(false); + const [draggingFiles, setDraggingFiles] = useState(false); const [creating, setCreating] = useState(false); const [deleting, setDeleting] = useState(false); const [folderName, setFolderName] = useState(""); @@ -86,6 +98,7 @@ export default function FilesPage() { const activePath = listing?.path ?? currentPath ?? ""; const canChangePath = listing?.can_change_path ?? false; + const canUpload = Boolean(activePath) && !uploading; const headerPath = displayPath(listing?.locked_root ?? listing?.path ?? currentPath); const load = useCallback( @@ -191,6 +204,36 @@ export default function FilesPage() { } }; + const handleDragEnter = (event: ReactDragEvent) => { + if (!canUpload || !transferHasFiles(event)) return; + event.preventDefault(); + dragDepthRef.current += 1; + setDraggingFiles(true); + }; + + const handleDragOver = (event: ReactDragEvent) => { + if (!canUpload || !transferHasFiles(event)) return; + event.preventDefault(); + event.dataTransfer.dropEffect = "copy"; + }; + + const handleDragLeave = (event: ReactDragEvent) => { + if (!canUpload || !transferHasFiles(event)) return; + event.preventDefault(); + dragDepthRef.current = Math.max(0, dragDepthRef.current - 1); + if (dragDepthRef.current === 0) { + setDraggingFiles(false); + } + }; + + const handleDrop = (event: ReactDragEvent) => { + if (!canUpload) return; + event.preventDefault(); + dragDepthRef.current = 0; + setDraggingFiles(false); + void uploadFiles(event.dataTransfer.files); + }; + const downloadFile = async (entry: ManagedFileEntry) => { if (entry.is_directory) return; try { @@ -288,6 +331,39 @@ export default function FilesPage() { + + {error && (