selection
This commit is contained in:
+326
-81
@@ -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,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user