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 { 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,
@@ -533,6 +533,7 @@ const useDocumentsWorkspace = ({
const {
dropOverlayState,
handleFileDrop,
handleFileSelection,
resetUploadsState,
} = useDocumentUploads({
apiClient: api,
@@ -1371,6 +1372,7 @@ const useDocumentsWorkspace = ({
currentTenantId,
handleTenantSelect,
openSettings,
handleFileSelection,
});
+56 -18
View File
@@ -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 (
<aside className="sidebar">
<input
type="file"
ref={uploadInputRef}
style={{ display: 'none' }}
multiple
onChange={handleUploadInputChange}
/>
<PanelHeader
className="sidebar__header"
leading={(
@@ -530,24 +561,31 @@ const Sidebar = ({
: null}
</>
)}
actions={
handleCollapse
? [
(
<button
key="collapse"
type="button"
className="icon-button"
onClick={handleCollapse}
aria-label="Collapse sidebar"
title="Collapse sidebar"
>
<SidebarCollapseIcon />
</button>
),
]
: null
}
actions={(
<>
<button
type="button"
className="icon-button"
onClick={handleUploadButtonClick}
disabled={!onUploadFiles || loading}
title="Upload documents"
aria-label="Upload documents"
>
<UploadIcon size={18} />
</button>
{handleCollapse ? (
<button
type="button"
className="icon-button"
onClick={handleCollapse}
aria-label="Collapse sidebar"
title="Collapse sidebar"
>
<SidebarCollapseIcon />
</button>
) : null}
</>
)}
/>
<div className="panel-body sidebar__body">
{status && (
+12
View File
@@ -35,6 +35,7 @@ const useSidebarProps = ({
currentTenantId,
handleTenantSelect,
openSettings,
handleFileSelection,
}) =>
useMemo(
() => ({
@@ -76,8 +77,19 @@ const useSidebarProps = ({
activeTenantId: currentTenantId,
onSelectTenant: handleTenantSelect,
onOpenSettings: openSettings,
onUploadFiles: (files, targetFolderId) => {
if (!handleFileSelection) {
return;
}
if (!files || files.length === 0) {
return;
}
const folderId = targetFolderId ?? selectedFolder ?? 'root';
handleFileSelection(files, folderId);
},
}),
[
handleFileSelection,
activeCorrespondentFilters,
activeTagFilters,
appStatus,
+10
View File
@@ -11,6 +11,7 @@ import {
IconArrowRight,
IconArrowUp,
IconAnalyze,
IconUpload,
IconWindowMaximize,
IconTextScan2,
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 }) => (
<IconArrowLeft
className={composeClassName('icon', className)}