upload queue

This commit is contained in:
2025-11-10 02:05:17 +01:00
parent ac24297a7b
commit b4a1821a55
7 changed files with 597 additions and 37 deletions
@@ -29,8 +29,6 @@ const useDocumentUploads = ({
currentFolderName,
ensureFolderData,
refreshCurrentFolder,
notifyApiError,
setStatusMessage,
setLoading,
shellRef,
}) => {
@@ -40,12 +38,13 @@ const useDocumentUploads = ({
});
const dragCounterRef = useRef(0);
const folderPathCacheRef = useRef(new Map());
const queueIdRef = useRef(0);
const [uploadQueue, setUploadQueue] = useState([]);
const uploadFile = useCallback(
async (file, targetFolderId) => {
if (!file || file.size === 0) {
setStatusMessage('Skipped empty file.', 'error');
return null;
return { document: null, duplicate: false, statusCode: null, conflictDocumentId: null };
}
const formData = new FormData();
@@ -54,29 +53,74 @@ 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) {
if (error.response?.status === 409) {
setStatusMessage(`${file.name} already exists on the server.`, 'info');
return null;
try {
const { data, status } = await apiClient.post('/documents', formData);
const duplicate = data?.reused || status === 200;
const document = data?.document ?? data ?? null;
return {
document,
duplicate,
statusCode: status ?? (duplicate ? 200 : 201),
conflictDocumentId: null,
};
} catch (error) {
if (error.response?.status === 409) {
const conflictId = error.response?.data?.details?.conflict_document_id ?? null;
let conflictDocument = null;
if (conflictId) {
try {
const { data } = await apiClient.get(`/documents/${conflictId}`);
conflictDocument = data?.document ?? data ?? null;
} catch (fetchError) {
console.warn('[Uploads] failed to fetch conflict document', fetchError);
}
}
return {
document: conflictDocument,
duplicate: true,
statusCode: 409,
conflictDocumentId: conflictId,
};
}
const message = error.response?.data?.error || `Failed to upload ${file.name}.`;
const wrapped = Object.assign(new Error(message), { response: error.response });
throw wrapped;
}
const message = error.response?.data?.error || `Failed to upload ${file.name}.`;
notifyApiError(error, message);
throw error;
}
},
[apiClient, notifyApiError, setStatusMessage],
},
[apiClient],
);
const appendQueueItems = useCallback((entries, targetFolderId) => {
const baseId = Date.now();
const items = entries.map(({ file }) => {
queueIdRef.current += 1;
return {
id: `upload-${baseId}-${queueIdRef.current}`,
name: file?.name || 'Unnamed file',
size: file?.size ?? null,
folderId: targetFolderId ?? selectedFolder ?? 'root',
status: 'pending',
error: null,
code: null,
document: null,
conflictDocumentId: null,
};
});
if (items.length) {
setUploadQueue((current) => [...current, ...items]);
}
return items;
}, [selectedFolder]);
const updateQueueItem = useCallback((id, patch) => {
if (!id) {
return;
}
setUploadQueue((current) =>
current.map((item) => (item.id === id ? { ...item, ...patch } : item)),
);
}, []);
const ensureFolderPathOnServer = useCallback(
async (baseFolderId, segments) => {
const trimmedSegments = segments.map((segment) => segment.trim()).filter(Boolean);
@@ -226,13 +270,23 @@ const useDocumentUploads = ({
const uploadFileEntries = useCallback(
async (entries, targetFolderId) => {
if (!token) {
setStatusMessage('Please log in before uploading.', 'error');
if (!entries || !entries.length) {
console.warn('[Uploads] No files to upload.');
return;
}
if (!entries || !entries.length) {
setStatusMessage('No files to upload.', 'info');
const queueItems = appendQueueItems(entries, targetFolderId);
if (!token) {
queueItems.forEach((item) => {
const patch = {
status: 'error',
error: 'Please log in before uploading.',
code: null,
};
updateQueueItem(item.id, patch);
Object.assign(item, patch);
});
return;
}
@@ -244,7 +298,14 @@ const useDocumentUploads = ({
const baseFolderId =
targetFolderId && targetFolderId !== 'root' ? targetFolderId : null;
for (const { file, segments } of entries) {
for (let index = 0; index < entries.length; index += 1) {
const { file, segments } = entries[index];
const queueItem = queueItems[index];
if (queueItem) {
const patch = { status: 'uploading', error: null, code: null };
updateQueueItem(queueItem.id, patch);
Object.assign(queueItem, patch);
}
// eslint-disable-next-line no-await-in-loop
const destinationId = segments.length
? await ensureFolderPathOnServer(baseFolderId, segments)
@@ -254,8 +315,34 @@ const useDocumentUploads = ({
destinationId ??
(targetFolderId && targetFolderId !== 'root' ? targetFolderId : 'root');
// eslint-disable-next-line no-await-in-loop
await uploadFile(file, uploadTarget);
try {
// eslint-disable-next-line no-await-in-loop
const { duplicate, statusCode, document, conflictDocumentId } = await uploadFile(
file,
uploadTarget,
);
if (queueItem) {
const patch = {
status: duplicate ? 'duplicate' : 'success',
code: statusCode ?? null,
document: document || queueItem.document,
conflictDocumentId: conflictDocumentId ?? queueItem.conflictDocumentId,
};
updateQueueItem(queueItem.id, patch);
Object.assign(queueItem, patch);
}
} catch (error) {
if (queueItem) {
const patch = {
status: 'error',
error: error.response?.data?.error || error.message || 'Upload failed.',
code: error.response?.status ?? null,
};
updateQueueItem(queueItem.id, patch);
Object.assign(queueItem, patch);
}
continue;
}
}
await refreshCurrentFolder();
@@ -268,7 +355,20 @@ const useDocumentUploads = ({
await ensureFolderData(targetFolderId, { force: true, prefetchDepth: 1 });
}
} catch (error) {
notifyApiError(error, 'Failed to upload files.');
const message = error.message || 'Failed to upload files.';
queueItems.forEach((item) => {
if (item.status === 'success' || item.status === 'duplicate' || item.status === 'error') {
return;
}
const patch = {
status: 'error',
error: message,
code: error.response?.status ?? null,
};
updateQueueItem(item.id, patch);
Object.assign(item, patch);
});
console.error('[Uploads] batch failed', error);
} finally {
setLoading(false);
}
@@ -280,9 +380,9 @@ const useDocumentUploads = ({
refreshCurrentFolder,
selectedFolder,
ensureFolderData,
notifyApiError,
setStatusMessage,
setLoading,
appendQueueItems,
updateQueueItem,
],
);
@@ -292,14 +392,13 @@ const useDocumentUploads = ({
try {
extracted = await extractFilesFromDataTransfer(dataTransfer);
} catch (error) {
const message = error.message || 'Failed to process dropped files.';
notifyApiError(error, message);
console.error('[Uploads] Failed to process dropped files.', error);
return;
}
await uploadFileEntries(extracted, targetFolderId);
},
[extractFilesFromDataTransfer, uploadFileEntries, notifyApiError],
[extractFilesFromDataTransfer, uploadFileEntries],
);
const handleFileSelection = useCallback(
@@ -326,6 +425,11 @@ const useDocumentUploads = ({
const resetUploadsState = useCallback(() => {
dragCounterRef.current = 0;
setDropOverlayState({ active: false, folderName: DEFAULT_FOLDER_NAME });
setUploadQueue([]);
}, []);
const clearUploadQueue = useCallback(() => {
setUploadQueue([]);
}, []);
return {
@@ -337,6 +441,8 @@ const useDocumentUploads = ({
uploadFile,
extractFilesFromDataTransfer,
resetUploadsState,
uploadQueue,
clearUploadQueue,
};
};
@@ -534,6 +534,8 @@ const useDocumentsWorkspace = ({
dropOverlayState,
handleFileDrop,
handleFileSelection,
uploadQueue,
clearUploadQueue,
resetUploadsState,
} = useDocumentUploads({
apiClient: api,
@@ -624,6 +626,7 @@ const useDocumentsWorkspace = ({
assetManager.reset();
resetPreviewState();
resetUploadsState();
clearUploadQueue();
breadcrumbFetchRef.current = new Set();
detailFolderFetchRef.current = new Set();
bootstrapInitializedRef.current = false;
@@ -654,6 +657,7 @@ const useDocumentsWorkspace = ({
setActivePreviewId,
resetPreviewState,
resetUploadsState,
clearUploadQueue,
]);
useEffect(() => {
@@ -1373,6 +1377,7 @@ const useDocumentsWorkspace = ({
handleTenantSelect,
openSettings,
handleFileSelection,
uploadQueue,
});
@@ -1478,6 +1483,8 @@ const useDocumentsWorkspace = ({
openSettings,
detailPanelOpen,
openDetailPanel,
uploadQueue,
clearUploadQueue,
}),
[
token,
@@ -1528,6 +1535,8 @@ const useDocumentsWorkspace = ({
openSettings,
detailPanelOpen,
openDetailPanel,
uploadQueue,
clearUploadQueue,
],
);