frontend
This commit is contained in:
@@ -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"
|
||||||
|
|||||||
+118
-50
@@ -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,34 +2788,37 @@ 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(
|
||||||
|
async (documentIdsOverride = null) => {
|
||||||
|
const targetIds = resolveTargetDocumentIds(documentIdsOverride);
|
||||||
|
if (!targetIds.length) {
|
||||||
|
setStatusMessage('Select documents before requesting re-analysis.', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const handleBulkSelectionReanalyze = useCallback(async () => {
|
setLoading(true);
|
||||||
if (!selectedDocumentIds.length) {
|
try {
|
||||||
setStatusMessage('Select documents before requesting re-analysis.', 'error');
|
const { data } = await api.post('/documents/bulk/reanalyze', {
|
||||||
return;
|
document_ids: targetIds,
|
||||||
}
|
force: true,
|
||||||
|
});
|
||||||
setLoading(true);
|
const queued = data?.queued ?? targetIds.length;
|
||||||
try {
|
setStatusMessage(
|
||||||
const { data } = await api.post('/documents/bulk/reanalyze', {
|
`Queued re-analysis for ${queued} document${queued === 1 ? '' : 's'}.`,
|
||||||
document_ids: selectedDocumentIds,
|
'success',
|
||||||
force: true,
|
);
|
||||||
});
|
} catch (error) {
|
||||||
const queued = data?.queued ?? selectedDocumentIds.length;
|
const message =
|
||||||
setStatusMessage(
|
error.response?.data?.error || 'Failed to queue document re-analysis.';
|
||||||
`Queued re-analysis for ${queued} document${queued === 1 ? '' : 's'}.`,
|
notifyApiError(error, message);
|
||||||
'success',
|
} finally {
|
||||||
);
|
setLoading(false);
|
||||||
} catch (error) {
|
}
|
||||||
const message =
|
},
|
||||||
error.response?.data?.error || 'Failed to queue document re-analysis.';
|
[resolveTargetDocumentIds, api, notifyApiError, setStatusMessage],
|
||||||
notifyApiError(error, message);
|
);
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, [selectedDocumentIds, 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,
|
||||||
|
|||||||
Reference in New Issue
Block a user