cleanup
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
const useBulkDocumentActions = ({
|
||||
api,
|
||||
resolveTargetDocumentIds,
|
||||
correspondentLookupByName,
|
||||
handleCorrespondentCreate,
|
||||
refreshCurrentFolder,
|
||||
setStatusMessage,
|
||||
selectedDocumentIds,
|
||||
selectedFolderIds,
|
||||
handleDocumentsDelete,
|
||||
handleFolderDelete,
|
||||
clearDocumentSelection,
|
||||
setLoading,
|
||||
}) => {
|
||||
const handleBulkCorrespondentAdd = useCallback(
|
||||
async ({ name, input, documentIds }) => {
|
||||
const trimmed = typeof name === 'string' ? name.trim() : '';
|
||||
if (!trimmed) {
|
||||
setStatusMessage('Correspondent name is required.', 'error');
|
||||
return;
|
||||
}
|
||||
const targets = resolveTargetDocumentIds(documentIds);
|
||||
if (!targets.length) {
|
||||
setStatusMessage('Select documents before assigning correspondents.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
let target = correspondentLookupByName.get(trimmed.toLowerCase()) || null;
|
||||
if (!target) {
|
||||
try {
|
||||
target = await handleCorrespondentCreate({ name: trimmed });
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!target?.id) {
|
||||
setStatusMessage('Unable to resolve correspondent.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await api.post('/documents/bulk/correspondents', {
|
||||
document_ids: targets,
|
||||
assignments: [
|
||||
{
|
||||
correspondent_id: target.id,
|
||||
},
|
||||
],
|
||||
action: 'add',
|
||||
});
|
||||
|
||||
const { assigned = 0, removed = 0 } = response.data || {};
|
||||
|
||||
await refreshCurrentFolder();
|
||||
const assignedSuffix = assigned === 1 ? '' : 's';
|
||||
if (removed > 0) {
|
||||
const removedSuffix = removed === 1 ? '' : 's';
|
||||
setStatusMessage(
|
||||
`Correspondent assigned (${assigned}) and replaced ${removed} link${removedSuffix}.`,
|
||||
'success',
|
||||
);
|
||||
} else {
|
||||
setStatusMessage(
|
||||
`Correspondent assigned to ${assigned} document${assignedSuffix}.`,
|
||||
'success',
|
||||
);
|
||||
}
|
||||
|
||||
if (input) {
|
||||
input.value = '';
|
||||
}
|
||||
},
|
||||
[
|
||||
api,
|
||||
correspondentLookupByName,
|
||||
handleCorrespondentCreate,
|
||||
refreshCurrentFolder,
|
||||
resolveTargetDocumentIds,
|
||||
setStatusMessage,
|
||||
],
|
||||
);
|
||||
|
||||
const handleBulkCorrespondentRemove = useCallback(
|
||||
async ({ assignments = [], documentIds }) => {
|
||||
if (!assignments.length) {
|
||||
setStatusMessage('Select a correspondent to remove.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const targets = resolveTargetDocumentIds(documentIds);
|
||||
|
||||
if (!targets.length) {
|
||||
setStatusMessage('Select documents before removing correspondents.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedAssignments = assignments.map((entry) => ({
|
||||
correspondent_id: entry.correspondent_id,
|
||||
}));
|
||||
|
||||
const response = await api.post('/documents/bulk/correspondents', {
|
||||
document_ids: targets,
|
||||
assignments: normalizedAssignments,
|
||||
action: 'remove',
|
||||
});
|
||||
|
||||
const { assigned = 0, removed = 0 } = response.data || {};
|
||||
await refreshCurrentFolder();
|
||||
|
||||
if (removed > 0) {
|
||||
const removedSuffix = removed === 1 ? '' : 's';
|
||||
setStatusMessage(
|
||||
`Correspondent removed from ${removed} link${removedSuffix}.`,
|
||||
'success',
|
||||
);
|
||||
} else if (assigned > 0) {
|
||||
const assignedSuffix = assigned === 1 ? '' : 's';
|
||||
setStatusMessage(`Correspondent updated ${assigned} link${assignedSuffix}.`, 'info');
|
||||
} else {
|
||||
setStatusMessage('No correspondents changed.', 'info');
|
||||
}
|
||||
},
|
||||
[api, refreshCurrentFolder, resolveTargetDocumentIds, setStatusMessage],
|
||||
);
|
||||
|
||||
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) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
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');
|
||||
}, [
|
||||
clearDocumentSelection,
|
||||
handleDocumentsDelete,
|
||||
handleFolderDelete,
|
||||
selectedDocumentIds,
|
||||
selectedFolderIds,
|
||||
setLoading,
|
||||
setStatusMessage,
|
||||
]);
|
||||
|
||||
return {
|
||||
handleBulkCorrespondentAdd,
|
||||
handleBulkCorrespondentRemove,
|
||||
handleDeleteSelection,
|
||||
};
|
||||
};
|
||||
|
||||
export default useBulkDocumentActions;
|
||||
@@ -0,0 +1,176 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
const useDocumentsPanelProps = ({
|
||||
currentFolderName,
|
||||
breadcrumbs,
|
||||
refreshCurrentFolder,
|
||||
currentSubfolders,
|
||||
documents,
|
||||
searchResults,
|
||||
isFilterActive,
|
||||
folderClickHandlers,
|
||||
selectFolder,
|
||||
handleFolderDragStart,
|
||||
handleFolderDragEnd,
|
||||
draggedFolderId,
|
||||
handleFolderRename,
|
||||
openDocumentPreview,
|
||||
handleDocumentTitleUpdate,
|
||||
selectedDocumentIds,
|
||||
selectedFolderIds,
|
||||
focusedRowKey,
|
||||
draggedDocumentIds,
|
||||
handleDocumentDragStart,
|
||||
handleDocumentDragEnd,
|
||||
searchLoading,
|
||||
tagLookupById,
|
||||
activeCorrespondentFilters,
|
||||
selectedEntries,
|
||||
setFocusedRowKey,
|
||||
ensureAssetUrl,
|
||||
getDocumentAsset,
|
||||
toggleTagFilter,
|
||||
toggleCorrespondentFilter,
|
||||
handleDocumentTagDrop,
|
||||
documentsViewMode,
|
||||
documentsSortField,
|
||||
documentsSortDirection,
|
||||
handleDocumentsSortFieldChange,
|
||||
handleDocumentsSortDirectionToggle,
|
||||
searchIncludeDescendants,
|
||||
toggleSearchIncludeDescendants,
|
||||
handleDocumentsViewModeChange,
|
||||
clearDocumentSelection,
|
||||
handleDeleteSelection,
|
||||
handleEntryPointerCore,
|
||||
inspectDocument,
|
||||
handleEntrySelection,
|
||||
tags,
|
||||
correspondents,
|
||||
documentLookup,
|
||||
handleBulkTagAddFromDetail,
|
||||
handleBulkTagRemoveFromDetail,
|
||||
handleBulkCorrespondentAdd,
|
||||
handleBulkCorrespondentRemove,
|
||||
handleBulkSelectionReanalyze,
|
||||
folderOptions,
|
||||
moveDocumentsToFolder,
|
||||
}) =>
|
||||
useMemo(
|
||||
() => ({
|
||||
currentFolderName,
|
||||
breadcrumbs,
|
||||
onRefresh: refreshCurrentFolder,
|
||||
subfolders: currentSubfolders,
|
||||
documents,
|
||||
searchResults,
|
||||
isFilterActive,
|
||||
onFolderSelect: selectFolder,
|
||||
onFolderDrop: folderClickHandlers.onDrop,
|
||||
onFolderDragOver: folderClickHandlers.onDragOver,
|
||||
onFolderDragLeave: folderClickHandlers.onDragLeave,
|
||||
onFolderDragStart: handleFolderDragStart,
|
||||
onFolderDragEnd: handleFolderDragEnd,
|
||||
draggedFolderId,
|
||||
onFolderRename: handleFolderRename,
|
||||
onDocumentOpen: openDocumentPreview,
|
||||
onDocumentRename: handleDocumentTitleUpdate,
|
||||
selectedDocumentIds,
|
||||
selectedFolderIds,
|
||||
focusedRowKey,
|
||||
draggingDocumentIds: draggedDocumentIds,
|
||||
onDocumentDragStart: handleDocumentDragStart,
|
||||
onDocumentDragEnd: handleDocumentDragEnd,
|
||||
isSearchLoading: searchLoading,
|
||||
tagLookupById,
|
||||
activeCorrespondentIds: activeCorrespondentFilters,
|
||||
selectedEntries,
|
||||
onFocusedRowChange: setFocusedRowKey,
|
||||
ensureAssetUrl,
|
||||
getDocumentAsset,
|
||||
onTagClick: toggleTagFilter,
|
||||
onCorrespondentClick: toggleCorrespondentFilter,
|
||||
onDocumentTagDrop: handleDocumentTagDrop,
|
||||
viewMode: documentsViewMode,
|
||||
sortField: documentsSortField,
|
||||
sortDirection: documentsSortDirection,
|
||||
onSortFieldChange: handleDocumentsSortFieldChange,
|
||||
onSortDirectionToggle: handleDocumentsSortDirectionToggle,
|
||||
searchIncludeDescendants,
|
||||
onToggleSearchIncludeDescendants: toggleSearchIncludeDescendants,
|
||||
onViewModeChange: handleDocumentsViewModeChange,
|
||||
onClearSelection: clearDocumentSelection,
|
||||
onDeleteSelection: handleDeleteSelection,
|
||||
onEntryPointer: handleEntryPointerCore,
|
||||
onInspectDocument: inspectDocument,
|
||||
onEntrySelection: handleEntrySelection,
|
||||
tags,
|
||||
correspondents,
|
||||
documentLookup,
|
||||
onBulkTagAdd: handleBulkTagAddFromDetail,
|
||||
onBulkTagRemove: handleBulkTagRemoveFromDetail,
|
||||
onBulkCorrespondentAdd: handleBulkCorrespondentAdd,
|
||||
onBulkCorrespondentRemove: handleBulkCorrespondentRemove,
|
||||
onBulkReanalyze: handleBulkSelectionReanalyze,
|
||||
folderOptions,
|
||||
onMoveDocumentsToFolder: moveDocumentsToFolder,
|
||||
}),
|
||||
[
|
||||
activeCorrespondentFilters,
|
||||
breadcrumbs,
|
||||
clearDocumentSelection,
|
||||
correspondents,
|
||||
currentFolderName,
|
||||
currentSubfolders,
|
||||
documents,
|
||||
documentsSortDirection,
|
||||
documentsSortField,
|
||||
documentsViewMode,
|
||||
documentLookup,
|
||||
draggedDocumentIds,
|
||||
draggedFolderId,
|
||||
focusedRowKey,
|
||||
folderClickHandlers,
|
||||
handleBulkCorrespondentAdd,
|
||||
handleBulkCorrespondentRemove,
|
||||
handleBulkSelectionReanalyze,
|
||||
handleBulkTagAddFromDetail,
|
||||
handleBulkTagRemoveFromDetail,
|
||||
handleDeleteSelection,
|
||||
handleDocumentDragEnd,
|
||||
handleDocumentDragStart,
|
||||
handleDocumentTagDrop,
|
||||
handleDocumentTitleUpdate,
|
||||
handleDocumentsSortDirectionToggle,
|
||||
handleDocumentsSortFieldChange,
|
||||
handleDocumentsViewModeChange,
|
||||
handleEntryPointerCore,
|
||||
handleEntrySelection,
|
||||
handleFolderDragEnd,
|
||||
handleFolderDragStart,
|
||||
handleFolderRename,
|
||||
inspectDocument,
|
||||
isFilterActive,
|
||||
moveDocumentsToFolder,
|
||||
openDocumentPreview,
|
||||
refreshCurrentFolder,
|
||||
searchIncludeDescendants,
|
||||
searchLoading,
|
||||
searchResults,
|
||||
selectedDocumentIds,
|
||||
selectedEntries,
|
||||
selectedFolderIds,
|
||||
selectFolder,
|
||||
setFocusedRowKey,
|
||||
tagLookupById,
|
||||
tags,
|
||||
toggleCorrespondentFilter,
|
||||
toggleSearchIncludeDescendants,
|
||||
toggleTagFilter,
|
||||
ensureAssetUrl,
|
||||
getDocumentAsset,
|
||||
folderOptions,
|
||||
],
|
||||
);
|
||||
|
||||
export default useDocumentsPanelProps;
|
||||
@@ -0,0 +1,122 @@
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
|
||||
const useDocumentsSelection = ({
|
||||
showingSearchResults,
|
||||
currentSubfolders,
|
||||
visibleDocuments,
|
||||
resolveFolderRowKey,
|
||||
resolveDocumentRowKey,
|
||||
configureSelectionEnvironment,
|
||||
visibleRowKeySet,
|
||||
selectedEntries,
|
||||
selectionAnchorRef,
|
||||
promoteSelectionOrderRaw,
|
||||
setFocusedDocumentId,
|
||||
setActivePreviewId,
|
||||
clearSelection,
|
||||
focusedDocumentId,
|
||||
setFocusedRowKey,
|
||||
focusedRowKey,
|
||||
isFolderRowKey,
|
||||
}) => {
|
||||
const navigableRows = useMemo(() => {
|
||||
const entries = [];
|
||||
if (!showingSearchResults) {
|
||||
currentSubfolders.forEach((folder) => {
|
||||
const key = resolveFolderRowKey(folder.id);
|
||||
if (key) {
|
||||
entries.push({ key, type: 'folder', id: folder.id });
|
||||
}
|
||||
});
|
||||
}
|
||||
visibleDocuments.forEach((doc) => {
|
||||
const key = resolveDocumentRowKey(doc.id);
|
||||
if (key) {
|
||||
entries.push({ key, type: 'document', id: doc.id });
|
||||
}
|
||||
});
|
||||
return entries;
|
||||
}, [showingSearchResults, currentSubfolders, visibleDocuments, resolveFolderRowKey, resolveDocumentRowKey]);
|
||||
|
||||
const navigableRowKeys = useMemo(
|
||||
() => navigableRows.map((entry) => entry.key),
|
||||
[navigableRows],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
configureSelectionEnvironment({
|
||||
visibleRowKeySet,
|
||||
navigableRowKeys,
|
||||
});
|
||||
}, [configureSelectionEnvironment, visibleRowKeySet, navigableRowKeys]);
|
||||
|
||||
const promoteSelectionOrder = useCallback(
|
||||
(docId) => {
|
||||
if (!docId) return;
|
||||
promoteSelectionOrderRaw(docId);
|
||||
const rowKey = resolveDocumentRowKey(docId);
|
||||
if (rowKey) {
|
||||
selectionAnchorRef.current = rowKey;
|
||||
}
|
||||
setFocusedDocumentId(docId);
|
||||
setActivePreviewId(docId);
|
||||
},
|
||||
[promoteSelectionOrderRaw, resolveDocumentRowKey, selectionAnchorRef, setFocusedDocumentId, setActivePreviewId],
|
||||
);
|
||||
|
||||
const clearDocumentSelection = useCallback(() => {
|
||||
clearSelection();
|
||||
}, [clearSelection]);
|
||||
|
||||
const prevFocusedDocIdRef = useRef(focusedDocumentId);
|
||||
useEffect(() => {
|
||||
const previous = prevFocusedDocIdRef.current;
|
||||
if (previous === focusedDocumentId) {
|
||||
return;
|
||||
}
|
||||
prevFocusedDocIdRef.current = focusedDocumentId;
|
||||
if (focusedDocumentId) {
|
||||
setFocusedRowKey(resolveDocumentRowKey(focusedDocumentId));
|
||||
} else {
|
||||
setFocusedRowKey((current) => (isFolderRowKey(current) ? current : null));
|
||||
}
|
||||
}, [focusedDocumentId, resolveDocumentRowKey, setFocusedRowKey, isFolderRowKey]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!navigableRowKeys.length) {
|
||||
if (focusedRowKey) {
|
||||
setFocusedRowKey(null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (focusedRowKey && navigableRowKeys.includes(focusedRowKey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const docKey = focusedDocumentId ? resolveDocumentRowKey(focusedDocumentId) : null;
|
||||
if (docKey && navigableRowKeys.includes(docKey)) {
|
||||
setFocusedRowKey(docKey);
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedKey = selectedEntries.find((key) => navigableRowKeys.includes(key));
|
||||
if (selectedKey) {
|
||||
setFocusedRowKey(selectedKey);
|
||||
return;
|
||||
}
|
||||
|
||||
if (focusedRowKey) {
|
||||
setFocusedRowKey(null);
|
||||
}
|
||||
}, [focusedRowKey, focusedDocumentId, navigableRowKeys, selectedEntries, setFocusedRowKey, resolveDocumentRowKey]);
|
||||
|
||||
return {
|
||||
navigableRows,
|
||||
navigableRowKeys,
|
||||
promoteSelectionOrder,
|
||||
clearDocumentSelection,
|
||||
};
|
||||
};
|
||||
|
||||
export default useDocumentsSelection;
|
||||
Reference in New Issue
Block a user