selection

This commit is contained in:
2025-11-03 02:31:46 +01:00
parent 1c261bf0a2
commit 152f824e2e
15 changed files with 1331 additions and 164 deletions
+326 -81
View File
@@ -784,7 +784,66 @@ const AppLayout = () => {
[mapDocumentCaches],
);
const removeDocumentsFromCaches = useCallback(
(documentIds) => {
if (!documentIds || documentIds.length === 0) {
return;
}
const idSet = new Set(documentIds);
setDocuments((prev) => prev.filter((doc) => !idSet.has(doc.id)));
setSearchResults((prev) => {
if (!Array.isArray(prev)) {
return prev;
}
const filtered = prev.filter((doc) => !idSet.has(doc.id));
return filtered.length === prev.length ? prev : filtered;
});
setFolderContents((prev) => {
if (!prev.size) {
return prev;
}
let changed = false;
const next = new Map();
prev.forEach((contents, key) => {
const docs = Array.isArray(contents?.documents) ? contents.documents : null;
if (!docs || docs.length === 0) {
next.set(key, contents);
return;
}
const filtered = docs.filter((doc) => !idSet.has(doc.id));
if (filtered.length !== docs.length) {
changed = true;
next.set(key, { ...contents, documents: filtered });
} else {
next.set(key, contents);
}
});
return changed ? next : prev;
});
setPreviewEntries((prev) => {
if (!prev.size) {
return prev;
}
let changed = false;
const next = new Map(prev);
idSet.forEach((id) => {
if (next.delete(id)) {
changed = true;
}
});
return changed ? next : prev;
});
idSet.forEach((id) => {
previewInflightRef.current.delete(id);
});
},
[setDocuments, setSearchResults, setFolderContents, setPreviewEntries],
);
const folderOptions = useMemo(() => {
const cache = new Map();
@@ -3020,6 +3079,59 @@ const AppLayout = () => {
[navigate, selectedFolder],
);
const handleDocumentsDelete = useCallback(
async (documentIds, { showMessage = true, manageLoading = true } = {}) => {
if (!documentIds || documentIds.length === 0) {
return false;
}
if (!token) {
setStatusMessage('Log in to manage documents.', 'error');
return false;
}
if (manageLoading) {
setLoading(true);
}
try {
await Promise.all(
documentIds.map((documentId) => api.delete(`/documents/${documentId}`)),
);
removeDocumentsFromCaches(documentIds);
if (documentIds.includes(previewDocumentId)) {
closeDocumentPreview();
}
if (showMessage) {
const message = documentIds.length === 1 ? 'Document deleted.' : 'Documents deleted.';
setStatusMessage(message, 'success');
}
return true;
} catch (error) {
const message = error.response?.data?.error || 'Failed to delete document.';
notifyApiError(error, message);
return false;
} finally {
if (manageLoading) {
setLoading(false);
}
}
},
[
token,
previewDocumentId,
closeDocumentPreview,
removeDocumentsFromCaches,
notifyApiError,
setStatusMessage,
setLoading,
],
);
const handleDocumentListFocus = useCallback(() => {
if (focusedRowKey && navigableRowKeys.includes(focusedRowKey)) {
return;
@@ -3409,6 +3521,180 @@ const AppLayout = () => {
],
);
const handleFolderDelete = useCallback(
async (folderId, { showMessage = true, manageLoading = true } = {}) => {
if (!token) {
if (showMessage) {
setStatusMessage('Log in to manage folders.', 'error');
}
return false;
}
if (!folderId || folderId === 'root') {
if (showMessage) {
setStatusMessage('The root folder cannot be removed.', 'error');
}
return false;
}
if (manageLoading) {
setLoading(true);
}
try {
const contents = await ensureFolderData(folderId, {
force: true,
prefetchDepth: 1,
});
const hasChildren = (contents.subfolders || []).length > 0;
const hasDocs = (contents.documents || []).length > 0;
if (hasChildren || hasDocs) {
if (showMessage) {
setStatusMessage('Folder must be empty before it can be deleted.', 'error');
}
return false;
}
await api.delete(`/folders/${folderId}`);
setFolderNodes((prev) => {
const next = new Map(prev);
const node = next.get(folderId);
next.delete(folderId);
if (node) {
const parentId = node.parentId || 'root';
const parentNode = next.get(parentId);
if (parentNode) {
const remaining = parentNode.children.filter((id) => id !== folderId);
next.set(parentId, {
...parentNode,
children: remaining,
hasChildren: remaining.length > 0,
});
}
}
return next;
});
setFolderContents((prev) => {
const next = new Map(prev);
next.delete(folderId);
return next;
});
if (selectedFolder === folderId) {
const node = folderNodes.get(folderId);
const parentId = node?.parentId || 'root';
setSelectedFolder(parentId);
const parentContents = await ensureFolderData(parentId, {
force: true,
prefetchDepth: 1,
});
applySelectedFolder(parentId, parentContents);
} else if (selectedFolder !== 'root') {
await ensureFolderData(selectedFolder, { force: true, prefetchDepth: 1 });
}
if (showMessage) {
setStatusMessage('Folder deleted.', 'success');
}
return true;
} catch (error) {
const message = error.response?.data?.error || 'Failed to delete folder.';
notifyApiError(error, message);
if (showMessage) {
setStatusMessage(message, 'error');
}
return false;
} finally {
if (manageLoading) {
setLoading(false);
}
}
},
[
token,
ensureFolderData,
selectedFolder,
folderNodes,
applySelectedFolder,
notifyApiError,
setStatusMessage,
setLoading,
],
);
const handleDeleteSelection = useCallback(async () => {
const docIds = Array.isArray(selectedDocumentIds) ? selectedDocumentIds : [];
const folderIds = Array.isArray(selectedFolderIds) ? selectedFolderIds : [];
if (docIds.length === 0 && folderIds.length === 0) {
return;
}
const parts = [];
if (docIds.length) {
parts.push(`${docIds.length} document${docIds.length === 1 ? '' : 's'}`);
}
if (folderIds.length) {
parts.push(`${folderIds.length} folder${folderIds.length === 1 ? '' : 's'}`);
}
const descriptor = parts.join(' and ');
const confirmation = parts.length === 1
? `Delete ${descriptor}? Folders must be empty before deletion. You can restore documents later from trash.`
: `Delete ${descriptor}? Folders must be empty before deletion. You can restore documents later from trash.`;
if (!window.confirm(confirmation)) {
return;
}
setLoading(true);
let docsOk = true;
let foldersOk = true;
try {
if (docIds.length) {
docsOk = await handleDocumentsDelete(docIds, { showMessage: false, manageLoading: false });
}
if (folderIds.length) {
for (const folderId of folderIds) {
const success = await handleFolderDelete(folderId, { showMessage: false, manageLoading: false });
if (!success) {
foldersOk = false;
}
}
}
} finally {
setLoading(false);
}
if (!docsOk || !foldersOk) {
setStatusMessage('Some items could not be deleted. Ensure folders are empty before deletion.', 'error');
return;
}
clearDocumentSelection();
const successParts = [];
if (docIds.length) {
successParts.push(docIds.length === 1 ? 'Document deleted.' : 'Documents deleted.');
}
if (folderIds.length) {
successParts.push(folderIds.length === 1 ? 'Folder deleted.' : 'Folders deleted.');
}
setStatusMessage(successParts.join(' '), 'success');
}, [
selectedDocumentIds,
selectedFolderIds,
handleDocumentsDelete,
handleFolderDelete,
clearDocumentSelection,
setLoading,
setStatusMessage,
]);
const handleDocumentTagDrop = useCallback(
async (documentId, tag) => {
if (!documentId || !tag?.id) {
@@ -3434,83 +3720,6 @@ const AppLayout = () => {
[handleDocumentTagAttach, handleTagRemove],
);
const handleFolderDelete = useCallback(
async (folderId) => {
if (!token) {
setStatusMessage('Log in to manage folders.', 'error');
return;
}
if (folderId === 'root') {
setStatusMessage('The root folder cannot be removed.', 'error');
return;
}
setLoading(true);
try {
const contents = await ensureFolderData(folderId, {
force: true,
prefetchDepth: 1,
});
const hasChildren = (contents.subfolders || []).length > 0;
const hasDocs = (contents.documents || []).length > 0;
if (hasChildren || hasDocs) {
setStatusMessage('Folder must be empty before it can be deleted.', 'error');
return;
}
await api.delete(`/folders/${folderId}`);
setFolderNodes((prev) => {
const next = new Map(prev);
const node = next.get(folderId);
next.delete(folderId);
if (node) {
const parentId = node.parentId || 'root';
const parentNode = next.get(parentId);
if (parentNode) {
const remaining = parentNode.children.filter((id) => id !== folderId);
next.set(parentId, {
...parentNode,
children: remaining,
hasChildren: remaining.length > 0,
});
}
}
return next;
});
setFolderContents((prev) => {
const next = new Map(prev);
next.delete(folderId);
return next;
});
if (selectedFolder === folderId) {
const node = folderNodes.get(folderId);
const parentId = node?.parentId || 'root';
setSelectedFolder(parentId);
const parentContents = await ensureFolderData(parentId, {
force: true,
prefetchDepth: 1,
});
applySelectedFolder(parentId, parentContents);
} else if (selectedFolder !== 'root') {
await ensureFolderData(selectedFolder, { force: true, prefetchDepth: 1 });
}
setStatusMessage('Folder deleted.', 'success');
} catch (error) {
const message = error.response?.data?.error || 'Failed to delete folder.';
notifyApiError(error, message);
} finally {
setLoading(false);
}
},
[
token,
ensureFolderData,
selectedFolder,
folderNodes,
applySelectedFolder,
notifyApiError,
setStatusMessage,
],
);
const handleFolderRename = useCallback(
async (folderId, nextName) => {
if (!token) {
@@ -4507,7 +4716,16 @@ const AppLayout = () => {
onViewModeChange: handleDocumentsViewModeChange,
onClearSelection: clearDocumentSelection,
onEntrySelection: handleEntrySelection,
onDeleteSelection: handleDeleteSelection,
onOpenDetailPanel: openDetailPanel,
tags,
correspondents,
documentLookup,
onBulkTagAdd: handleBulkTagAddFromDetail,
onBulkTagRemove: handleBulkTagRemoveFromDetail,
onBulkCorrespondentAdd: handleBulkCorrespondentAdd,
onBulkCorrespondentRemove: handleBulkCorrespondentRemove,
onBulkReanalyze: handleBulkSelectionReanalyze,
}),
[
activeCorrespondentFilters,
@@ -4532,6 +4750,7 @@ const AppLayout = () => {
handleFolderDragStart,
handleFolderRename,
handleEntrySelection,
handleDeleteSelection,
isFilterActive,
openDetailPanel,
openDocumentPreview,
@@ -4547,6 +4766,14 @@ const AppLayout = () => {
toggleTagFilter,
ensureAssetUrl,
getDocumentAsset,
tags,
correspondents,
documentLookup,
handleBulkTagAddFromDetail,
handleBulkTagRemoveFromDetail,
handleBulkCorrespondentAdd,
handleBulkCorrespondentRemove,
handleBulkSelectionReanalyze,
],
);
@@ -4638,12 +4865,10 @@ const AppLayout = () => {
tagLookupById,
onTagAdd: handleTagAdd,
onTagRemove: handleTagRemove,
onRegenerateThumbnails: handleThumbnailRegeneration,
previewEntry: selectedPreviewEntry,
onOpenPreview: openDocumentPreview,
onBulkTagAdd: handleBulkTagAddFromDetail,
onBulkTagRemove: handleBulkTagRemoveFromDetail,
onBulkReanalyze: handleBulkSelectionReanalyze,
onBulkCorrespondentAdd: handleBulkCorrespondentAdd,
onBulkCorrespondentRemove: handleBulkCorrespondentRemove,
onPromoteSelection: promoteSelectionOrder,
@@ -4670,7 +4895,6 @@ const AppLayout = () => {
getDocumentAsset,
handleBulkCorrespondentAdd,
handleBulkCorrespondentRemove,
handleBulkSelectionReanalyze,
handleBulkTagAddFromDetail,
handleBulkTagRemoveFromDetail,
handleCorrespondentAdd,
@@ -4680,7 +4904,6 @@ const AppLayout = () => {
handleDocumentIssuedUpdate,
handleTagAdd,
handleTagRemove,
handleThumbnailRegeneration,
openDocumentPreview,
promoteSelectionOrder,
resolveFolderPath,
@@ -4786,6 +5009,7 @@ const AppLayout = () => {
tenantId: currentTenantId,
viewId: deskViewId,
selectedDocumentIds,
selectedFolderIds,
onClearSelection: clearDocumentSelection,
detailPanelOpen,
onCloseDetailPanel: handleDetailPanelClose,
@@ -4795,6 +5019,16 @@ const AppLayout = () => {
ensureAssetUrl,
getDocumentAsset,
activeTagIds: activeTagFilters,
onDeleteSelection: handleDeleteSelection,
tags,
correspondents,
documentLookup,
tagLookupById,
onBulkTagAdd: handleBulkTagAddFromDetail,
onBulkTagRemove: handleBulkTagRemoveFromDetail,
onBulkCorrespondentAdd: handleBulkCorrespondentAdd,
onBulkCorrespondentRemove: handleBulkCorrespondentRemove,
onBulkReanalyze: handleBulkSelectionReanalyze,
}),
[
documents,
@@ -4815,6 +5049,7 @@ const AppLayout = () => {
deskViewId,
deskHelpOpen,
selectedDocumentIds,
selectedFolderIds,
clearDocumentSelection,
detailPanelOpen,
handleDetailPanelClose,
@@ -4824,6 +5059,16 @@ const AppLayout = () => {
ensureAssetUrl,
getDocumentAsset,
activeTagFilters,
handleDeleteSelection,
tags,
correspondents,
documentLookup,
tagLookupById,
handleBulkTagAddFromDetail,
handleBulkTagRemoveFromDetail,
handleBulkCorrespondentAdd,
handleBulkCorrespondentRemove,
handleBulkSelectionReanalyze,
],
);
+17 -9
View File
@@ -21,7 +21,6 @@ const DocumentsRouteContent = () => {
previewWorkspaceEntry,
previewDocumentId,
closeDocumentPreview,
handleThumbnailRegeneration,
ensurePreviewData,
resolveApiPath,
ensureAssetUrl,
@@ -84,7 +83,6 @@ const DocumentsRouteContent = () => {
getDocumentAsset,
resolveApiPath,
notifyApiError,
handleThumbnailRegeneration,
closeDocumentPreview,
parentBreadcrumb,
onNavigateParent: handleNavigateParent,
@@ -141,13 +139,23 @@ const DocumentsRouteContent = () => {
<DocumentsLayout sidebarProps={sidebarPropsWithActions}>
<div className={mainContentClass}>
{header ? (
<PanelHeader
className="main-content__header"
leading={header.leading}
title={headerTitle}
titleTag="h2"
actions={header.actions}
/>
<div className="main-content__header-wrapper">
{(header.selectionLabel || header.floatingActions) ? (
<div className="panel-floating" aria-live="polite" aria-atomic="true">
{header.selectionLabel ? (
<span className="panel-floating__label">{header.selectionLabel}</span>
) : null}
{header.floatingActions || null}
</div>
) : null}
<PanelHeader
className="main-content__header"
leading={header.leading}
title={headerTitle}
titleTag="h2"
actions={header.actions}
/>
</div>
) : null}
<div className={bodyClass}>{surface.content}</div>
{surface.detail || null}
-3
View File
@@ -20,7 +20,6 @@ export const useWorkspaceSurface = ({
getDocumentAsset,
resolveApiPath,
notifyApiError,
handleThumbnailRegeneration,
closeDocumentPreview,
parentBreadcrumb,
onNavigateParent,
@@ -92,7 +91,6 @@ export const useWorkspaceSurface = ({
getDocumentAsset,
resolveApiPath,
notifyApiError,
onRegenerate: handleThumbnailRegeneration,
onClose: closeDocumentPreview,
renderSidebarToggle,
tagLookupById,
@@ -117,7 +115,6 @@ export const useWorkspaceSurface = ({
getDocumentAsset,
resolveApiPath,
notifyApiError,
handleThumbnailRegeneration,
closeDocumentPreview,
renderSidebarToggle,
detailPanelProps,