This commit is contained in:
2025-10-29 12:02:24 +01:00
parent 2a17f2d333
commit f1f2dee701
2 changed files with 125 additions and 54 deletions
+7 -4
View File
@@ -1104,6 +1104,7 @@ const DetailPanel = ({
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
}; };
const documentIds = Array.from(new Set(selectedDocuments.map((doc) => doc?.id).filter(Boolean)));
return ( return (
<> <>
@@ -1165,8 +1166,10 @@ const DetailPanel = ({
title="Tags" title="Tags"
tags={bulkTagUnion} tags={bulkTagUnion}
emptyMessage="No tags assigned." emptyMessage="No tags assigned."
onRemove={(tag) => onBulkTagRemove?.({ label: tag.label })} onRemove={(tag) => onBulkTagRemove?.({ label: tag.label, documentIds })}
onAdd={({ value, input }) => onBulkTagAdd?.({ label: value, input })} onAdd={({ value, input }) =>
onBulkTagAdd?.({ label: value, input, documentIds })
}
addPlaceholder="Add tag to selection" addPlaceholder="Add tag to selection"
addButtonLabel="Add tag" addButtonLabel="Add tag"
datalistId="tag-catalog-bulk" datalistId="tag-catalog-bulk"
@@ -1178,7 +1181,7 @@ const DetailPanel = ({
entries={bulkCorrespondents} entries={bulkCorrespondents}
onRemove={handleBulkCorrespondentRemove} onRemove={handleBulkCorrespondentRemove}
onAdd={({ name, input }) => onAdd={({ name, input }) =>
onBulkCorrespondentAdd?.({ name, input }) onBulkCorrespondentAdd?.({ name, input, documentIds })
} }
addPlaceholder="Add correspondent to selection" addPlaceholder="Add correspondent to selection"
datalistId="correspondent-catalog-bulk" datalistId="correspondent-catalog-bulk"
@@ -1229,7 +1232,7 @@ const DetailPanel = ({
className="icon-button ghost" className="icon-button ghost"
onClick={(event) => { onClick={(event) => {
event.stopPropagation(); event.stopPropagation();
onBulkReanalyze(); onBulkReanalyze(documentIds);
}} }}
aria-label="Re-run analysis for selection" aria-label="Re-run analysis for selection"
title="Re-run analysis for selection" title="Re-run analysis for selection"
+98 -30
View File
@@ -736,6 +736,8 @@ const AppLayout = () => {
); );
const [detailPanelOpen, setDetailPanelOpen] = useState(() => selectedDocumentIds.length > 0); const [detailPanelOpen, setDetailPanelOpen] = useState(() => selectedDocumentIds.length > 0);
const [detailPanelDocIds, setDetailPanelDocIds] = useState([]);
const [detailPanelDocs, setDetailPanelDocs] = useState([]);
useEffect(() => { useEffect(() => {
if (selectedDocumentIds.length > 0 && !detailPanelOpen) { if (selectedDocumentIds.length > 0 && !detailPanelOpen) {
@@ -771,6 +773,8 @@ const AppLayout = () => {
setDropOverlayState({ active: false, folderName: DEFAULT_FOLDER_NAME }); setDropOverlayState({ active: false, folderName: DEFAULT_FOLDER_NAME });
setActivePreviewId(null); setActivePreviewId(null);
setDetailPanelOpen(false); setDetailPanelOpen(false);
setDetailPanelDocIds([]);
setDetailPanelDocs([]);
assetManager.reset(); assetManager.reset();
setPreviewEntries(() => new Map()); setPreviewEntries(() => new Map());
previewInflightRef.current = new Map(); previewInflightRef.current = new Map();
@@ -2335,14 +2339,28 @@ const AppLayout = () => {
} }
}, [selectedFolder, ensureFolderData, applySelectedFolder, notifyApiError]); }, [selectedFolder, ensureFolderData, applySelectedFolder, notifyApiError]);
const resolveTargetDocumentIds = useCallback(
(candidateIds) => {
const normalized = Array.isArray(candidateIds)
? candidateIds.filter(Boolean)
: [];
if (normalized.length) {
return Array.from(new Set(normalized));
}
return selectedDocumentIds;
},
[selectedDocumentIds],
);
const handleBulkCorrespondentAdd = useCallback( const handleBulkCorrespondentAdd = useCallback(
async ({ name, input }) => { async ({ name, input, documentIds }) => {
const trimmed = (name || '').trim(); const trimmed = (name || '').trim();
if (!trimmed) { if (!trimmed) {
setStatusMessage('Correspondent name is required.', 'error'); setStatusMessage('Correspondent name is required.', 'error');
return; return;
} }
if (!selectedDocumentIds.length) { const targets = resolveTargetDocumentIds(documentIds);
if (!targets.length) {
setStatusMessage('Select documents before assigning correspondents.', 'error'); setStatusMessage('Select documents before assigning correspondents.', 'error');
return; return;
} }
@@ -2362,7 +2380,7 @@ const AppLayout = () => {
} }
const response = await api.post('/documents/bulk/correspondents', { const response = await api.post('/documents/bulk/correspondents', {
document_ids: selectedDocumentIds, document_ids: targets,
assignments: [ assignments: [
{ {
correspondent_id: target.id, correspondent_id: target.id,
@@ -2397,7 +2415,7 @@ const AppLayout = () => {
handleCorrespondentCreate, handleCorrespondentCreate,
api, api,
refreshCurrentFolder, refreshCurrentFolder,
selectedDocumentIds, resolveTargetDocumentIds,
setStatusMessage, setStatusMessage,
], ],
); );
@@ -2409,9 +2427,7 @@ const AppLayout = () => {
return; return;
} }
const targets = Array.isArray(documentIds) && documentIds.length const targets = resolveTargetDocumentIds(documentIds);
? documentIds
: selectedDocumentIds;
if (!targets.length) { if (!targets.length) {
setStatusMessage('Select documents before removing correspondents.', 'error'); setStatusMessage('Select documents before removing correspondents.', 'error');
@@ -2444,7 +2460,7 @@ const AppLayout = () => {
setStatusMessage('No correspondents changed.', 'info'); setStatusMessage('No correspondents changed.', 'info');
} }
}, },
[api, refreshCurrentFolder, selectedDocumentIds, setStatusMessage], [api, refreshCurrentFolder, resolveTargetDocumentIds, setStatusMessage],
); );
useEffect(() => { useEffect(() => {
@@ -2550,12 +2566,13 @@ const AppLayout = () => {
}, []); }, []);
const bulkTagOperation = useCallback( const bulkTagOperation = useCallback(
async ({ labels, action }) => { async ({ labels, action, documentIds }) => {
const normalized = labels.map((label) => label.trim()).filter((label) => label.length > 0); const normalized = labels.map((label) => label.trim()).filter((label) => label.length > 0);
if (!normalized.length) { if (!normalized.length) {
return { ok: false, reason: 'no-labels' }; return { ok: false, reason: 'no-labels' };
} }
if (!selectedDocumentIds.length) { const targetDocumentIds = resolveTargetDocumentIds(documentIds);
if (!targetDocumentIds.length) {
return { ok: false, reason: 'no-selection' }; return { ok: false, reason: 'no-selection' };
} }
@@ -2599,7 +2616,7 @@ const AppLayout = () => {
} }
await api.post('/documents/bulk/tags', { await api.post('/documents/bulk/tags', {
document_ids: selectedDocumentIds, document_ids: targetDocumentIds,
tag_ids: tagIds, tag_ids: tagIds,
action, action,
}); });
@@ -2609,7 +2626,7 @@ const AppLayout = () => {
return { return {
ok: true, ok: true,
tagCount: tagIds.length, tagCount: tagIds.length,
docsCount: selectedDocumentIds.length, docsCount: targetDocumentIds.length,
}; };
} catch (error) { } catch (error) {
const message = const message =
@@ -2618,13 +2635,11 @@ const AppLayout = () => {
notifyApiError(error, message); notifyApiError(error, message);
return { ok: false, reason: 'request-failed' }; return { ok: false, reason: 'request-failed' };
} finally { } finally {
if (!refreshOnly) {
setLoading(false); setLoading(false);
} }
}
}, },
[ [
selectedDocumentIds, resolveTargetDocumentIds,
tags, tags,
api, api,
refreshTags, refreshTags,
@@ -2711,17 +2726,22 @@ const AppLayout = () => {
); );
const handleBulkTagAddFromDetail = useCallback( const handleBulkTagAddFromDetail = useCallback(
async ({ label, input }) => { async ({ label, input, documentIds }) => {
const trimmed = (label || '').trim(); const trimmed = (label || '').trim();
if (!trimmed) { if (!trimmed) {
setStatusMessage('Enter a tag label.', 'error'); setStatusMessage('Enter a tag label.', 'error');
return; return;
} }
if (!selectedDocumentIds.length) { const targetIds = resolveTargetDocumentIds(documentIds);
if (!targetIds.length) {
setStatusMessage('Select documents before assigning tags.', 'error'); setStatusMessage('Select documents before assigning tags.', 'error');
return; return;
} }
const result = await bulkTagOperation({ labels: [trimmed], action: 'add' }); const result = await bulkTagOperation({
labels: [trimmed],
action: 'add',
documentIds: targetIds,
});
if (result?.ok) { if (result?.ok) {
const { tagCount, docsCount } = result; const { tagCount, docsCount } = result;
setStatusMessage( setStatusMessage(
@@ -2735,21 +2755,26 @@ const AppLayout = () => {
} }
} }
}, },
[bulkTagOperation, selectedDocumentIds, setStatusMessage], [bulkTagOperation, resolveTargetDocumentIds, setStatusMessage],
); );
const handleBulkTagRemoveFromDetail = useCallback( const handleBulkTagRemoveFromDetail = useCallback(
async ({ label, input }) => { async ({ label, input, documentIds }) => {
const trimmed = (label || '').trim(); const trimmed = (label || '').trim();
if (!trimmed) { if (!trimmed) {
setStatusMessage('Enter a tag label to remove.', 'error'); setStatusMessage('Enter a tag label to remove.', 'error');
return; return;
} }
if (!selectedDocumentIds.length) { const targetIds = resolveTargetDocumentIds(documentIds);
if (!targetIds.length) {
setStatusMessage('Select documents before removing tags.', 'error'); setStatusMessage('Select documents before removing tags.', 'error');
return; return;
} }
const result = await bulkTagOperation({ labels: [trimmed], action: 'remove' }); const result = await bulkTagOperation({
labels: [trimmed],
action: 'remove',
documentIds: targetIds,
});
if (result?.ok) { if (result?.ok) {
const { docsCount } = result; const { docsCount } = result;
setStatusMessage( setStatusMessage(
@@ -2763,11 +2788,12 @@ const AppLayout = () => {
setStatusMessage(`Tag “${result.label}” not found.`, 'error'); setStatusMessage(`Tag “${result.label}” not found.`, 'error');
} }
}, },
[bulkTagOperation, selectedDocumentIds, setStatusMessage], [bulkTagOperation, resolveTargetDocumentIds, setStatusMessage],
); );
const handleBulkSelectionReanalyze = useCallback(
const handleBulkSelectionReanalyze = useCallback(async () => { async (documentIdsOverride = null) => {
if (!selectedDocumentIds.length) { const targetIds = resolveTargetDocumentIds(documentIdsOverride);
if (!targetIds.length) {
setStatusMessage('Select documents before requesting re-analysis.', 'error'); setStatusMessage('Select documents before requesting re-analysis.', 'error');
return; return;
} }
@@ -2775,10 +2801,10 @@ const AppLayout = () => {
setLoading(true); setLoading(true);
try { try {
const { data } = await api.post('/documents/bulk/reanalyze', { const { data } = await api.post('/documents/bulk/reanalyze', {
document_ids: selectedDocumentIds, document_ids: targetIds,
force: true, force: true,
}); });
const queued = data?.queued ?? selectedDocumentIds.length; const queued = data?.queued ?? targetIds.length;
setStatusMessage( setStatusMessage(
`Queued re-analysis for ${queued} document${queued === 1 ? '' : 's'}.`, `Queued re-analysis for ${queued} document${queued === 1 ? '' : 's'}.`,
'success', 'success',
@@ -2790,7 +2816,9 @@ const AppLayout = () => {
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [selectedDocumentIds, api, notifyApiError, setStatusMessage]); },
[resolveTargetDocumentIds, api, notifyApiError, setStatusMessage],
);
const uploadFile = useCallback( const uploadFile = useCallback(
async (file, targetFolderId) => { async (file, targetFolderId) => {
@@ -4851,6 +4879,44 @@ const AppLayout = () => {
return ordered; return ordered;
}, [selectionOrder, documentLookup, selectedDocumentIds]); }, [selectionOrder, documentLookup, selectedDocumentIds]);
useEffect(() => {
if (!orderedSelectedDocuments.length) {
return;
}
const nextIds = orderedSelectedDocuments
.map((doc) => (doc?.id ? doc.id : null))
.filter(Boolean);
setDetailPanelDocIds((prev) => {
if (nextIds.length === prev.length && nextIds.every((id, index) => id === prev[index])) {
return prev;
}
return nextIds;
});
}, [orderedSelectedDocuments]);
useEffect(() => {
if (!detailPanelDocIds.length) {
setDetailPanelDocs([]);
return;
}
setDetailPanelDocs((prevDocs) => {
const prevMap = new Map((prevDocs || []).map((doc) => [doc.id, doc]));
const next = detailPanelDocIds
.map((id) => documentLookup.get(id) || prevMap.get(id) || null)
.filter(Boolean);
if (next.length === prevDocs.length && next.every((doc, index) => doc === prevDocs[index])) {
return prevDocs;
}
return next;
});
}, [detailPanelDocIds, documentLookup]);
const detailPanelSelectedDocuments = detailPanelDocs;
const lastScrolledDetailDocRef = useRef(null); const lastScrolledDetailDocRef = useRef(null);
useEffect(() => { useEffect(() => {
@@ -5288,10 +5354,12 @@ const AppLayout = () => {
const handleDetailPanelClose = useCallback(() => { const handleDetailPanelClose = useCallback(() => {
setDetailPanelOpen(false); setDetailPanelOpen(false);
clearDocumentSelection(); clearDocumentSelection();
setDetailPanelDocIds([]);
setDetailPanelDocs([]);
}, [clearDocumentSelection]); }, [clearDocumentSelection]);
const detailPanelProps = { const detailPanelProps = {
selectedDocuments: orderedSelectedDocuments, selectedDocuments: detailPanelSelectedDocuments,
tags, tags,
tagLookupById, tagLookupById,
onTagAdd: handleTagAdd, onTagAdd: handleTagAdd,