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