detailspanel, selection handling, single document details panel

This commit is contained in:
2025-11-07 10:46:04 +01:00
parent 5b88ecf2dd
commit 30b90a5bf5
5 changed files with 292 additions and 761 deletions
+3 -16
View File
@@ -4358,17 +4358,12 @@ const AppLayout = () => {
const {
detailPanelOpen,
detailPanelSelectedDocuments,
detailPanelDocument,
openDetailPanel,
closeDetailPanel,
} = useDetailPanel({
selectedDocumentIds,
documentLookup,
orderedSelectedDocuments,
selectionOrder,
documentsViewMode,
getRowId,
isDocumentRowKey,
});
detailPanelControlRef.current = {
@@ -4900,17 +4895,13 @@ const AppLayout = () => {
const detailPanelProps = useMemo(
() => ({
selectedDocuments: detailPanelSelectedDocuments,
document: detailPanelDocument,
tags,
tagLookupById,
onTagAdd: handleTagAdd,
onTagRemove: handleTagRemove,
previewEntry: selectedPreviewEntry,
onOpenPreview: openDocumentPreview,
onBulkTagAdd: handleBulkTagAddFromDetail,
onBulkTagRemove: handleBulkTagRemoveFromDetail,
onBulkCorrespondentAdd: handleBulkCorrespondentAdd,
onBulkCorrespondentRemove: handleBulkCorrespondentRemove,
onPromoteSelection: promoteSelectionOrder,
activePreviewId,
onUpdateTitle: handleDocumentTitleUpdate,
@@ -4929,14 +4920,10 @@ const AppLayout = () => {
[
activePreviewId,
correspondents,
detailPanelSelectedDocuments,
detailPanelDocument,
ensureAssetUrl,
ensurePreviewData,
getDocumentAsset,
handleBulkCorrespondentAdd,
handleBulkCorrespondentRemove,
handleBulkTagAddFromDetail,
handleBulkTagRemoveFromDetail,
handleCorrespondentAdd,
handleCorrespondentRemove,
handleDetailPanelClose,
+44 -117
View File
@@ -3,147 +3,74 @@ import { useCallback, useEffect, useRef, useState } from 'react';
export const useDetailPanel = ({
documentLookup,
orderedSelectedDocuments,
selectionOrder,
documentsViewMode,
getRowId,
isDocumentRowKey,
}) => {
const [detailPanelOpen, setDetailPanelOpen] = useState(false);
const [detailPanelDocIds, setDetailPanelDocIds] = useState([]);
const [detailPanelDocs, setDetailPanelDocs] = useState([]);
const lastScrolledDetailDocRef = useRef(null);
const [detailPanelDocId, setDetailPanelDocId] = useState(null);
const [detailPanelDocument, setDetailPanelDocument] = useState(null);
const latestOrderedDocsRef = useRef([]);
useEffect(() => {
latestOrderedDocsRef.current = orderedSelectedDocuments;
if (detailPanelOpen && orderedSelectedDocuments.length) {
const snapshotIds = orderedSelectedDocuments
.map((doc) => doc?.id)
.filter((id) => typeof id === 'string' || typeof id === 'number');
if (snapshotIds.length) {
setDetailPanelDocIds(snapshotIds);
const nextDoc = orderedSelectedDocuments[orderedSelectedDocuments.length - 1];
if (nextDoc?.id) {
setDetailPanelDocId(nextDoc.id);
setDetailPanelDocument(nextDoc);
}
}
}, [orderedSelectedDocuments, detailPanelOpen]);
const resolveDocsForIds = useCallback(
(ids, fallbackDocs = []) => {
if (!ids?.length) {
return [];
}
const fallbackMap = new Map((fallbackDocs || []).map((doc) => [doc?.id, doc]));
return ids
.map((id) => documentLookup.get(id) || fallbackMap.get(id) || null)
.filter(Boolean);
},
[documentLookup],
);
useEffect(() => {
if (!detailPanelDocIds.length) {
setDetailPanelDocs((prev) => (prev.length ? [] : prev));
return;
}
setDetailPanelDocs((prevDocs) => {
const resolved = resolveDocsForIds(detailPanelDocIds, prevDocs);
if (resolved.length === prevDocs.length && resolved.every((doc, index) => doc === prevDocs[index])) {
return prevDocs;
if (!detailPanelDocId) {
if (!detailPanelOpen) {
setDetailPanelDocument(null);
}
return resolved;
});
}, [detailPanelDocIds, resolveDocsForIds]);
useEffect(() => {
if (!detailPanelOpen) {
lastScrolledDetailDocRef.current = null;
return;
}
if (!orderedSelectedDocuments.length) {
lastScrolledDetailDocRef.current = null;
return;
const resolved = documentLookup.get(detailPanelDocId);
if (resolved && resolved !== detailPanelDocument) {
setDetailPanelDocument(resolved);
}
let lastSelectedId = null;
for (let index = selectionOrder.length - 1; index >= 0; index -= 1) {
const key = selectionOrder[index];
if (isDocumentRowKey(key)) {
lastSelectedId = getRowId(key);
if (lastSelectedId) {
break;
}
}
}
if (!lastSelectedId && orderedSelectedDocuments.length) {
const fallbackDoc = orderedSelectedDocuments[orderedSelectedDocuments.length - 1];
lastSelectedId = fallbackDoc?.id || null;
}
if (!lastSelectedId || lastScrolledDetailDocRef.current === lastSelectedId) {
return;
}
if (typeof document === 'undefined') {
return;
}
const targetElement =
document.getElementById(`document-row-${lastSelectedId}`)
|| document.getElementById(`document-card-${lastSelectedId}`);
if (!targetElement) {
return;
}
lastScrolledDetailDocRef.current = lastSelectedId;
requestAnimationFrame(() => {
targetElement.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
});
}, [
detailPanelOpen,
orderedSelectedDocuments,
selectionOrder,
documentsViewMode,
getRowId,
isDocumentRowKey,
]);
const detailPanelSelectedDocuments = detailPanelDocs;
}, [detailPanelDocId, documentLookup, detailPanelDocument, detailPanelOpen]);
const openDetailPanel = useCallback(
({ documentIds: explicitIds, documents: explicitDocs } = {}) => {
let sourceDocs = Array.isArray(explicitDocs) ? explicitDocs : null;
let snapshotIds = Array.isArray(explicitIds)
? explicitIds.filter((id) => typeof id === 'string' || typeof id === 'number')
: null;
({ documentId, document, documentIds, documents } = {}) => {
let targetDoc = document || null;
let targetId = documentId ?? document?.id ?? null;
if (!snapshotIds?.length) {
if (!sourceDocs || !sourceDocs.length) {
sourceDocs = latestOrderedDocsRef.current;
}
snapshotIds = Array.isArray(sourceDocs)
? sourceDocs
.map((doc) => doc?.id)
.filter((id) => typeof id === 'string' || typeof id === 'number')
: [];
if (!targetDoc && Array.isArray(documents) && documents.length) {
targetDoc = documents[documents.length - 1];
targetId = targetDoc?.id ?? targetId;
}
const uniqueIds = [];
snapshotIds.forEach((id) => {
if (!uniqueIds.includes(id)) {
uniqueIds.push(id);
if (!targetDoc && Array.isArray(documentIds) && documentIds.length) {
targetId = documentIds[documentIds.length - 1];
}
if (!targetDoc && targetId != null) {
targetDoc = documentLookup.get(String(targetId)) || null;
}
if (!targetDoc) {
const fallbackDocs = latestOrderedDocsRef.current;
const fallbackDoc = Array.isArray(fallbackDocs) && fallbackDocs.length
? fallbackDocs[fallbackDocs.length - 1]
: null;
if (fallbackDoc) {
targetDoc = fallbackDoc;
targetId = fallbackDoc.id;
}
});
}
const resolvedDocs = resolveDocsForIds(uniqueIds, sourceDocs || latestOrderedDocsRef.current);
if (!targetDoc && targetId == null) {
return;
}
setDetailPanelDocIds(uniqueIds);
setDetailPanelDocs(resolvedDocs);
setDetailPanelOpen(true);
setDetailPanelDocId(targetDoc?.id || targetId || null);
setDetailPanelDocument(targetDoc || null);
setDetailPanelOpen(Boolean(targetDoc || targetId));
},
[resolveDocsForIds],
[documentLookup],
);
const closeDetailPanel = useCallback(() => {
@@ -152,7 +79,7 @@ export const useDetailPanel = ({
return {
detailPanelOpen,
detailPanelSelectedDocuments,
detailPanelDocument,
openDetailPanel,
closeDetailPanel,
setDetailPanelOpen,