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