upload button
This commit is contained in:
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)}
|
||||
|
||||
Reference in New Issue
Block a user