diff --git a/frontend/src/hooks/documents/useDocumentUploads.js b/frontend/src/hooks/documents/useDocumentUploads.js index a04bd22..373e40f 100644 --- a/frontend/src/hooks/documents/useDocumentUploads.js +++ b/frontend/src/hooks/documents/useDocumentUploads.js @@ -2,6 +2,26 @@ import { useCallback, useRef, useState } from 'react'; import useFileDrop from './useFileDrop'; import { DEFAULT_FOLDER_NAME, hasFiles } from '../../app/appLayoutUtils'; +const mapFilesToEntries = (filesInput) => { + if (!filesInput) { + return []; + } + const files = Array.isArray(filesInput) ? filesInput : Array.from(filesInput); + return files + .filter(Boolean) + .map((file) => { + const relativePath = + typeof file.webkitRelativePath === 'string' ? file.webkitRelativePath : ''; + const segments = relativePath + ? relativePath + .split('/') + .slice(0, -1) + .filter(Boolean) + : []; + return { file, segments }; + }); +}; + const useDocumentUploads = ({ apiClient, token, @@ -34,23 +54,27 @@ const useDocumentUploads = ({ formData.append('folder_id', targetFolderId); } - try { - const { data, status } = await apiClient.post('/documents', formData); - const duplicate = data?.reused || status === 200; - setStatusMessage( - duplicate - ? `${file.name} already exists; reused existing document.` - : `Uploaded ${file.name}`, - duplicate ? 'info' : 'success', - ); - return data; - } catch (error) { - const message = error.response?.data?.error || `Failed to upload ${file.name}.`; - notifyApiError(error, message); - throw error; + try { + const { data, status } = await apiClient.post('/documents', formData); + const duplicate = data?.reused || status === 200; + setStatusMessage( + duplicate + ? `${file.name} already exists; reused existing document.` + : `Uploaded ${file.name}`, + duplicate ? 'info' : 'success', + ); + return data; + } catch (error) { + if (error.response?.status === 409) { + setStatusMessage(`${file.name} already exists on the server.`, 'info'); + return null; } - }, - [apiClient, notifyApiError, setStatusMessage], + const message = error.response?.data?.error || `Failed to upload ${file.name}.`; + notifyApiError(error, message); + throw error; + } + }, + [apiClient, notifyApiError, setStatusMessage], ); const ensureFolderPathOnServer = useCallback( @@ -200,35 +224,27 @@ const useDocumentUploads = ({ return results; }, []); - const handleFileDrop = useCallback( - async (dataTransfer, targetFolderId) => { + const uploadFileEntries = useCallback( + async (entries, targetFolderId) => { if (!token) { setStatusMessage('Please log in before uploading.', 'error'); return; } + if (!entries || !entries.length) { + setStatusMessage('No files to upload.', 'info'); + return; + } + setLoading(true); try { folderPathCacheRef.current.clear(); - let extracted; - try { - extracted = await extractFilesFromDataTransfer(dataTransfer); - } catch (error) { - const message = error.message || 'Failed to process dropped files.'; - notifyApiError(error, message); - return; - } - - if (!extracted.length) { - setStatusMessage('No files to upload.', 'info'); - return; - } const baseFolderId = targetFolderId && targetFolderId !== 'root' ? targetFolderId : null; - for (const { file, segments } of extracted) { + for (const { file, segments } of entries) { // eslint-disable-next-line no-await-in-loop const destinationId = segments.length ? await ensureFolderPathOnServer(baseFolderId, segments) @@ -259,7 +275,6 @@ const useDocumentUploads = ({ }, [ token, - extractFilesFromDataTransfer, ensureFolderPathOnServer, uploadFile, refreshCurrentFolder, @@ -271,6 +286,30 @@ const useDocumentUploads = ({ ], ); + const handleFileDrop = useCallback( + async (dataTransfer, targetFolderId) => { + let extracted; + try { + extracted = await extractFilesFromDataTransfer(dataTransfer); + } catch (error) { + const message = error.message || 'Failed to process dropped files.'; + notifyApiError(error, message); + return; + } + + await uploadFileEntries(extracted, targetFolderId); + }, + [extractFilesFromDataTransfer, uploadFileEntries, notifyApiError], + ); + + const handleFileSelection = useCallback( + async (files, targetFolderId) => { + const entries = mapFilesToEntries(files); + await uploadFileEntries(entries, targetFolderId); + }, + [uploadFileEntries], + ); + useFileDrop({ shellRef, token, @@ -294,6 +333,7 @@ const useDocumentUploads = ({ setDropOverlayState, dragCounterRef, handleFileDrop, + handleFileSelection, uploadFile, extractFilesFromDataTransfer, resetUploadsState, diff --git a/frontend/src/hooks/documents/useDocumentsWorkspace.js b/frontend/src/hooks/documents/useDocumentsWorkspace.js index 86ae836..1c7878e 100644 --- a/frontend/src/hooks/documents/useDocumentsWorkspace.js +++ b/frontend/src/hooks/documents/useDocumentsWorkspace.js @@ -533,6 +533,7 @@ const useDocumentsWorkspace = ({ const { dropOverlayState, handleFileDrop, + handleFileSelection, resetUploadsState, } = useDocumentUploads({ apiClient: api, @@ -1371,6 +1372,7 @@ const useDocumentsWorkspace = ({ currentTenantId, handleTenantSelect, openSettings, + handleFileSelection, }); diff --git a/frontend/src/sidebar/Sidebar.jsx b/frontend/src/sidebar/Sidebar.jsx index ac576af..d258ed8 100644 --- a/frontend/src/sidebar/Sidebar.jsx +++ b/frontend/src/sidebar/Sidebar.jsx @@ -16,6 +16,7 @@ import { SunIcon, MoonIcon, DesktopIcon, + UploadIcon, } from '../ui/icons'; import PanelHeader from '../ui/PanelHeader'; import useFloatingMenu from '../ui/useFloatingMenu'; @@ -187,6 +188,8 @@ const Sidebar = ({ activeTenantId = null, onSelectTenant, onOpenSettings, + onUploadFiles, + loading = false, }) => { const { setCollapsed, @@ -197,6 +200,7 @@ const Sidebar = ({ cycleThemeMode, themeModes, } = useSidebarContext(); + const uploadInputRef = useRef(null); const handleCollapse = useCallback(() => { setCollapsed(true); }, [setCollapsed]); @@ -258,6 +262,26 @@ const Sidebar = ({ onCreateFolder?.(); }, [creatingFolder, onCreateFolder]); + const handleUploadButtonClick = useCallback(() => { + if (!onUploadFiles || !uploadInputRef.current) { + return; + } + uploadInputRef.current.click(); + }, [onUploadFiles]); + + const handleUploadInputChange = useCallback( + (event) => { + const files = event.target?.files; + if (files && files.length && onUploadFiles) { + onUploadFiles(files, selectedFolder); + } + if (event.target) { + event.target.value = ''; + } + }, + [onUploadFiles, selectedFolder], + ); + const handleNeutralHueReset = useCallback(() => { resetNeutralHue(); }, [resetNeutralHue]); @@ -448,6 +472,13 @@ const Sidebar = ({ return (