frontend
This commit is contained in:
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user