upload button

This commit is contained in:
2025-11-10 00:32:41 +01:00
parent 20cf597c5c
commit ac24297a7b
5 changed files with 153 additions and 51 deletions
@@ -2,6 +2,26 @@ import { useCallback, useRef, useState } from 'react';
import useFileDrop from './useFileDrop'; import useFileDrop from './useFileDrop';
import { DEFAULT_FOLDER_NAME, hasFiles } from '../../app/appLayoutUtils'; 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 = ({ const useDocumentUploads = ({
apiClient, apiClient,
token, token,
@@ -34,23 +54,27 @@ const useDocumentUploads = ({
formData.append('folder_id', targetFolderId); formData.append('folder_id', targetFolderId);
} }
try { try {
const { data, status } = await apiClient.post('/documents', formData); const { data, status } = await apiClient.post('/documents', formData);
const duplicate = data?.reused || status === 200; const duplicate = data?.reused || status === 200;
setStatusMessage( setStatusMessage(
duplicate duplicate
? `${file.name} already exists; reused existing document.` ? `${file.name} already exists; reused existing document.`
: `Uploaded ${file.name}`, : `Uploaded ${file.name}`,
duplicate ? 'info' : 'success', duplicate ? 'info' : 'success',
); );
return data; return data;
} catch (error) { } catch (error) {
const message = error.response?.data?.error || `Failed to upload ${file.name}.`; if (error.response?.status === 409) {
notifyApiError(error, message); setStatusMessage(`${file.name} already exists on the server.`, 'info');
throw error; return null;
} }
}, const message = error.response?.data?.error || `Failed to upload ${file.name}.`;
[apiClient, notifyApiError, setStatusMessage], notifyApiError(error, message);
throw error;
}
},
[apiClient, notifyApiError, setStatusMessage],
); );
const ensureFolderPathOnServer = useCallback( const ensureFolderPathOnServer = useCallback(
@@ -200,35 +224,27 @@ const useDocumentUploads = ({
return results; return results;
}, []); }, []);
const handleFileDrop = useCallback( const uploadFileEntries = useCallback(
async (dataTransfer, targetFolderId) => { async (entries, targetFolderId) => {
if (!token) { if (!token) {
setStatusMessage('Please log in before uploading.', 'error'); setStatusMessage('Please log in before uploading.', 'error');
return; return;
} }
if (!entries || !entries.length) {
setStatusMessage('No files to upload.', 'info');
return;
}
setLoading(true); setLoading(true);
try { try {
folderPathCacheRef.current.clear(); 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 = const baseFolderId =
targetFolderId && targetFolderId !== 'root' ? targetFolderId : null; 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 // eslint-disable-next-line no-await-in-loop
const destinationId = segments.length const destinationId = segments.length
? await ensureFolderPathOnServer(baseFolderId, segments) ? await ensureFolderPathOnServer(baseFolderId, segments)
@@ -259,7 +275,6 @@ const useDocumentUploads = ({
}, },
[ [
token, token,
extractFilesFromDataTransfer,
ensureFolderPathOnServer, ensureFolderPathOnServer,
uploadFile, uploadFile,
refreshCurrentFolder, 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({ useFileDrop({
shellRef, shellRef,
token, token,
@@ -294,6 +333,7 @@ const useDocumentUploads = ({
setDropOverlayState, setDropOverlayState,
dragCounterRef, dragCounterRef,
handleFileDrop, handleFileDrop,
handleFileSelection,
uploadFile, uploadFile,
extractFilesFromDataTransfer, extractFilesFromDataTransfer,
resetUploadsState, resetUploadsState,
@@ -533,6 +533,7 @@ const useDocumentsWorkspace = ({
const { const {
dropOverlayState, dropOverlayState,
handleFileDrop, handleFileDrop,
handleFileSelection,
resetUploadsState, resetUploadsState,
} = useDocumentUploads({ } = useDocumentUploads({
apiClient: api, apiClient: api,
@@ -1371,6 +1372,7 @@ const useDocumentsWorkspace = ({
currentTenantId, currentTenantId,
handleTenantSelect, handleTenantSelect,
openSettings, openSettings,
handleFileSelection,
}); });
+56 -18
View File
@@ -16,6 +16,7 @@ import {
SunIcon, SunIcon,
MoonIcon, MoonIcon,
DesktopIcon, DesktopIcon,
UploadIcon,
} from '../ui/icons'; } from '../ui/icons';
import PanelHeader from '../ui/PanelHeader'; import PanelHeader from '../ui/PanelHeader';
import useFloatingMenu from '../ui/useFloatingMenu'; import useFloatingMenu from '../ui/useFloatingMenu';
@@ -187,6 +188,8 @@ const Sidebar = ({
activeTenantId = null, activeTenantId = null,
onSelectTenant, onSelectTenant,
onOpenSettings, onOpenSettings,
onUploadFiles,
loading = false,
}) => { }) => {
const { const {
setCollapsed, setCollapsed,
@@ -197,6 +200,7 @@ const Sidebar = ({
cycleThemeMode, cycleThemeMode,
themeModes, themeModes,
} = useSidebarContext(); } = useSidebarContext();
const uploadInputRef = useRef(null);
const handleCollapse = useCallback(() => { const handleCollapse = useCallback(() => {
setCollapsed(true); setCollapsed(true);
}, [setCollapsed]); }, [setCollapsed]);
@@ -258,6 +262,26 @@ const Sidebar = ({
onCreateFolder?.(); onCreateFolder?.();
}, [creatingFolder, 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(() => { const handleNeutralHueReset = useCallback(() => {
resetNeutralHue(); resetNeutralHue();
}, [resetNeutralHue]); }, [resetNeutralHue]);
@@ -448,6 +472,13 @@ const Sidebar = ({
return ( return (
<aside className="sidebar"> <aside className="sidebar">
<input
type="file"
ref={uploadInputRef}
style={{ display: 'none' }}
multiple
onChange={handleUploadInputChange}
/>
<PanelHeader <PanelHeader
className="sidebar__header" className="sidebar__header"
leading={( leading={(
@@ -530,24 +561,31 @@ const Sidebar = ({
: null} : null}
</> </>
)} )}
actions={ actions={(
handleCollapse <>
? [ <button
( type="button"
<button className="icon-button"
key="collapse" onClick={handleUploadButtonClick}
type="button" disabled={!onUploadFiles || loading}
className="icon-button" title="Upload documents"
onClick={handleCollapse} aria-label="Upload documents"
aria-label="Collapse sidebar" >
title="Collapse sidebar" <UploadIcon size={18} />
> </button>
<SidebarCollapseIcon /> {handleCollapse ? (
</button> <button
), type="button"
] className="icon-button"
: null onClick={handleCollapse}
} aria-label="Collapse sidebar"
title="Collapse sidebar"
>
<SidebarCollapseIcon />
</button>
) : null}
</>
)}
/> />
<div className="panel-body sidebar__body"> <div className="panel-body sidebar__body">
{status && ( {status && (
+12
View File
@@ -35,6 +35,7 @@ const useSidebarProps = ({
currentTenantId, currentTenantId,
handleTenantSelect, handleTenantSelect,
openSettings, openSettings,
handleFileSelection,
}) => }) =>
useMemo( useMemo(
() => ({ () => ({
@@ -76,8 +77,19 @@ const useSidebarProps = ({
activeTenantId: currentTenantId, activeTenantId: currentTenantId,
onSelectTenant: handleTenantSelect, onSelectTenant: handleTenantSelect,
onOpenSettings: openSettings, onOpenSettings: openSettings,
onUploadFiles: (files, targetFolderId) => {
if (!handleFileSelection) {
return;
}
if (!files || files.length === 0) {
return;
}
const folderId = targetFolderId ?? selectedFolder ?? 'root';
handleFileSelection(files, folderId);
},
}), }),
[ [
handleFileSelection,
activeCorrespondentFilters, activeCorrespondentFilters,
activeTagFilters, activeTagFilters,
appStatus, appStatus,
+10
View File
@@ -11,6 +11,7 @@ import {
IconArrowRight, IconArrowRight,
IconArrowUp, IconArrowUp,
IconAnalyze, IconAnalyze,
IconUpload,
IconWindowMaximize, IconWindowMaximize,
IconTextScan2, IconTextScan2,
IconFolderPlus, IconFolderPlus,
@@ -131,6 +132,15 @@ export const ViewGridIcon = ({ className, size = '1em', stroke = 1.6, ...rest })
/> />
); );
export const UploadIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
<IconUpload
className={composeClassName('icon', className)}
size={size}
stroke={stroke}
{...rest}
/>
);
export const ArrowLeftIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => ( export const ArrowLeftIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
<IconArrowLeft <IconArrowLeft
className={composeClassName('icon', className)} className={composeClassName('icon', className)}