detailpanel

This commit is contained in:
2025-11-01 19:39:51 +01:00
parent 366f716b1c
commit 03bc311403
5 changed files with 142 additions and 52 deletions
+31 -32
View File
@@ -484,6 +484,8 @@ const AppLayout = () => {
[selectedRowKeys],
);
const selectionCount = selectedDocumentIds.length;
const selectedFolderIds = useMemo(
() =>
selectedRowKeys
@@ -493,30 +495,9 @@ const AppLayout = () => {
[selectedRowKeys],
);
const [detailPanelOpen, setDetailPanelOpen] = useState(() => selectedDocumentIds.length > 0);
const [detailPanelOpen, setDetailPanelOpen] = useState(false);
const [detailPanelDocIds, setDetailPanelDocIds] = useState([]);
const [detailPanelDocs, setDetailPanelDocs] = useState([]);
const detailPanelAutoOpenRef = useRef(true);
const detailPanelSelectionTokenRef = useRef(selectedDocumentIds.join('|'));
useEffect(() => {
const token = selectedDocumentIds.join('|');
if (token !== detailPanelSelectionTokenRef.current) {
detailPanelSelectionTokenRef.current = token;
detailPanelAutoOpenRef.current = true;
}
if (selectedDocumentIds.length === 0) {
if (detailPanelOpen) {
setDetailPanelOpen(false);
}
return;
}
if (!detailPanelOpen && detailPanelAutoOpenRef.current && !previewDocumentId) {
setDetailPanelOpen(true);
}
}, [selectedDocumentIds, detailPanelOpen, previewDocumentId]);
const resetWorkspaceState = useCallback(() => {
const rootNode = createRootNode();
@@ -1164,16 +1145,30 @@ const AppLayout = () => {
applySelection(nextKeys, { anchor: anchorKey, interactedKeys });
},
[applySelection, navigableRowKeys, selectedRowKeys, setFocusedRowKey, visibleRowKeySet],
[
applySelection,
navigableRowKeys,
selectedRowKeys,
setFocusedRowKey,
visibleRowKeySet,
],
);
const openDetailPanel = useCallback(() => {
setDetailPanelOpen(true);
}, []);
const handleDocumentRowClick = useCallback(
(documentId, event) => {
const rowKey = resolveDocumentRowKey(documentId);
if (!rowKey) return;
const hadSelection = selectionCount > 0;
handleRowSelection(rowKey, event);
if (!hadSelection) {
openDetailPanel();
}
},
[handleRowSelection],
[handleRowSelection, openDetailPanel, selectionCount],
);
const clearDocumentSelection = useCallback(() => {
@@ -3397,7 +3392,6 @@ const AppLayout = () => {
const openDocumentPreview = useCallback(
(documentId, { replace = false } = {}) => {
if (!documentId) return;
detailPanelAutoOpenRef.current = false;
setDetailPanelOpen(false);
previewReturnPathRef.current = `${location.pathname}${location.search}`;
navigate(`/documents/${documentId}`, { replace });
@@ -3502,6 +3496,9 @@ const AppLayout = () => {
if (row.type === 'folder') {
selectFolder(row.id);
} else if (row.type === 'document') {
if (selectionCount === 0) {
openDetailPanel();
}
openDocumentPreview(row.id);
}
return;
@@ -3534,10 +3531,14 @@ const AppLayout = () => {
setFocusedRowKey(targetRow.key);
const hadSelection = selectionCount > 0;
handleRowSelection(targetRow.key, {
shiftKey,
preventDefault: () => {},
});
if (targetRow.type === 'document' && !hadSelection) {
openDetailPanel();
}
},
[
navigableRows,
@@ -3545,8 +3546,10 @@ const AppLayout = () => {
focusedRowKey,
selectedRowKeys,
handleRowSelection,
openDetailPanel,
selectFolder,
openDocumentPreview,
selectionCount,
],
);
@@ -5151,12 +5154,8 @@ const AppLayout = () => {
);
const handleDetailPanelClose = useCallback(() => {
detailPanelAutoOpenRef.current = false;
setDetailPanelOpen(false);
clearDocumentSelection();
setDetailPanelDocIds([]);
setDetailPanelDocs([]);
}, [clearDocumentSelection]);
}, []);
const detailPanelProps = useMemo(
() => ({
@@ -5315,7 +5314,7 @@ const AppLayout = () => {
openCorrespondentsModal,
openSettings,
detailPanelOpen,
setDetailPanelOpen,
openDetailPanel,
}),
[
token,
@@ -5375,7 +5374,7 @@ const AppLayout = () => {
openCorrespondentsModal,
openSettings,
detailPanelOpen,
setDetailPanelOpen,
openDetailPanel,
],
);
+46 -1
View File
@@ -21,6 +21,7 @@ import DocumentSummarySection, {
sortCorrespondents,
buildCorrespondentOptions,
} from '../documents/DocumentSummarySection';
import BreadcrumbTrail from '../ui/BreadcrumbTrail';
const MAX_PREVIEW_STACK_ITEMS = 15;
@@ -192,6 +193,39 @@ const DetailPanel = ({
return `${selectedCount} document${selectedCount === 1 ? '' : 's'}`;
}, [selectedCount, detailSummary]);
const headerBreadcrumbs = useMemo(() => {
if (selectedCount !== 1 || !singleDoc || typeof resolveFolderPath !== 'function') {
return null;
}
const handleNavigate = (folderId) => {
if (!folderId || typeof onFolderNavigate !== 'function') {
return;
}
onFolderNavigate(folderId);
};
const folderSegments = resolveFolderPath(singleDoc.folder_id);
const normalizedSegments = Array.isArray(folderSegments)
? folderSegments
.filter((segment) => segment && segment.id && segment.name)
.map((segment) => ({
id: segment.id,
label: segment.name,
onClick: segment.id ? () => handleNavigate(segment.id) : null,
}))
: [];
const documentLabel = detailSummary.title || 'Document';
return [
...normalizedSegments,
{
id: singleDoc.id || 'current-document',
label: documentLabel,
},
];
}, [selectedCount, singleDoc, resolveFolderPath, detailSummary, onFolderNavigate]);
const [zoomedPreview, setZoomedPreview] = useState(null);
const bulkDocumentIds = useMemo(
@@ -933,7 +967,18 @@ const DetailPanel = ({
<aside className="detail-panel panel">
<PanelHeader
leading={headerLeading}
title={headerTitle}
title={
headerBreadcrumbs ? (
<BreadcrumbTrail
entries={headerBreadcrumbs}
separator="/"
className="panel-header__breadcrumbs"
truncateFromStart
/>
) : (
headerTitle
)
}
titleTag="h3"
actions={headerActions.length ? headerActions : null}
/>
@@ -247,6 +247,7 @@ const DocumentSummarySection = ({
onUpdateIssued,
resolveFolderPath,
onFolderNavigate,
showFolderPath = true,
}) => {
const summary = useMemo(() => {
if (!document) {
@@ -283,7 +284,11 @@ const DocumentSummarySection = ({
}, [correspondents, document?.correspondents]);
const folderPath = useMemo(() => {
if (!document?.folder_id || typeof resolveFolderPath !== 'function') {
if (
!showFolderPath
|| !document?.folder_id
|| typeof resolveFolderPath !== 'function'
) {
return null;
}
const segments = resolveFolderPath(document.folder_id);
@@ -294,7 +299,7 @@ const DocumentSummarySection = ({
return null;
}
return filtered;
}, [document?.folder_id, resolveFolderPath]);
}, [document?.folder_id, resolveFolderPath, showFolderPath]);
const folderDisplayNode = useMemo(() => {
if (!folderPath || !folderPath.length) {
@@ -521,7 +526,7 @@ const DocumentSummarySection = ({
</>
)}
</div>
{folderDisplayNode ? (
{showFolderPath && folderDisplayNode ? (
<div className="doc-title-row__path" aria-label="Folder path">
{folderDisplayNode}
</div>
+17
View File
@@ -255,6 +255,21 @@ export const createPreviewSurface = ({
</>
)
: null;
let breadcrumbs = null;
if (document && typeof resolveFolderPath === 'function') {
const folderSegments = resolveFolderPath(document.folder_id);
const normalizedSegments = Array.isArray(folderSegments)
? folderSegments
.filter((segment) => segment && segment.id && segment.name)
.map((segment) => ({ id: segment.id, name: segment.name }))
: [];
const documentLabel = document.title || document.original_name || 'Document';
breadcrumbs = [
...normalizedSegments,
{ id: document.id || 'current-document', name: documentLabel },
];
}
const header = {
title,
subtitle: null,
@@ -268,6 +283,7 @@ export const createPreviewSurface = ({
notifyApiError,
onRegenerate,
}),
breadcrumbs,
};
return {
@@ -290,6 +306,7 @@ export const createPreviewSurface = ({
onUpdateIssued={onUpdateIssued}
resolveFolderPath={resolveFolderPath}
onFolderNavigate={onFolderNavigate}
showFolderPath={false}
/>
),
supportsDetail: false,
+40 -16
View File
@@ -26,8 +26,18 @@ const ELLIPSIS = { id: '__breadcrumbs_ellipsis__', label: '…', onClick: null,
const WIDTH_TOLERANCE = 1;
const WIDTH_BUFFER_RATIO = 0.99;
const BreadcrumbTrail = ({ entries = [], className = '', separator = '/' }) => {
const BreadcrumbTrail = ({
entries = [],
className = '',
separator = '/',
truncateFromStart = true,
}) => {
const normalized = useMemo(() => normalizeEntries(entries), [entries]);
const shouldTruncateFromStart = truncateFromStart !== false;
const measurementEntries = useMemo(
() => (shouldTruncateFromStart ? normalized : normalized.slice().reverse()),
[normalized, shouldTruncateFromStart],
);
const containerRef = useRef(null);
const measurementRef = useRef(null);
const ellipsisButtonRef = useRef(null);
@@ -50,7 +60,7 @@ const BreadcrumbTrail = ({ entries = [], className = '', separator = '/' }) => {
useEffect(() => {
setStartIndex(0);
closeEllipsisMenu();
}, [normalized, closeEllipsisMenu]);
}, [normalized, shouldTruncateFromStart, closeEllipsisMenu]);
useEffect(() => {
const container = containerRef.current;
@@ -58,10 +68,12 @@ const BreadcrumbTrail = ({ entries = [], className = '', separator = '/' }) => {
return undefined;
}
const target = container.parentElement || container;
const updateWidth = () => {
const nextWidth = target.getBoundingClientRect().width;
const host = containerRef.current;
if (!host) {
return;
}
const nextWidth = host.getBoundingClientRect().width;
if (!nextWidth) {
return;
}
@@ -71,12 +83,12 @@ const BreadcrumbTrail = ({ entries = [], className = '', separator = '/' }) => {
updateWidth();
const observer = new ResizeObserver(updateWidth);
observer.observe(target);
observer.observe(container.parentElement || container);
return () => observer.disconnect();
}, []);
useLayoutEffect(() => {
if (!normalized.length) {
if (!measurementEntries.length) {
return;
}
@@ -136,7 +148,7 @@ const BreadcrumbTrail = ({ entries = [], className = '', separator = '/' }) => {
ellipsisNode.style.display = originalEllipsisDisplay ?? 'none';
}
const available = availableWidth ?? container.clientWidth;
const available = availableWidth ?? container.getBoundingClientRect().width;
if (!available || !widths.length) {
return;
}
@@ -156,13 +168,24 @@ const BreadcrumbTrail = ({ entries = [], className = '', separator = '/' }) => {
if (nextStart !== startIndex) {
setStartIndex(nextStart);
}
}, [normalized, separator, availableWidth, startIndex]);
}, [measurementEntries, separator, availableWidth, startIndex]);
const sliceStart = Math.min(startIndex, Math.max(0, normalized.length - 1));
const slice = normalized.slice(sliceStart);
const visibleEntries = sliceStart > 0 ? [ELLIPSIS, ...slice] : slice;
const hiddenEntries = sliceStart > 0 ? normalized.slice(0, sliceStart) : [];
const trimmedCount = Math.min(startIndex, Math.max(0, normalized.length - 1));
const visibleEntries = shouldTruncateFromStart
? normalized.slice(trimmedCount)
: normalized.slice(0, Math.max(normalized.length - trimmedCount, 1));
const hiddenEntries = trimmedCount === 0
? []
: shouldTruncateFromStart
? normalized.slice(0, trimmedCount)
: normalized.slice(-trimmedCount);
const hasHiddenEntries = hiddenEntries.length > 0;
const ellipsisPlacement = shouldTruncateFromStart ? 'start' : 'end';
const displayEntries = hasHiddenEntries
? ellipsisPlacement === 'start'
? [ELLIPSIS, ...visibleEntries]
: [...visibleEntries, ELLIPSIS]
: visibleEntries;
useEffect(() => {
if (!hasHiddenEntries) {
@@ -191,9 +214,9 @@ const BreadcrumbTrail = ({ entries = [], className = '', separator = '/' }) => {
return (
<>
<span ref={containerRef} className={wrapperClassName}>
{visibleEntries.map((entry, index) => {
{displayEntries.map((entry, index) => {
const isEllipsis = entry.id === ELLIPSIS.id;
const isLast = index === visibleEntries.length - 1;
const isLast = index === displayEntries.length - 1;
if (isEllipsis) {
return (
@@ -273,7 +296,7 @@ const BreadcrumbTrail = ({ entries = [], className = '', separator = '/' }) => {
>
{ELLIPSIS.label}
</span>
{normalized.map((entry, index) => (
{measurementEntries.map((entry, index) => (
<React.Fragment key={`measure-${entry.id || index}`}>
{index > 0 ? (
<span
@@ -342,6 +365,7 @@ BreadcrumbTrail.propTypes = {
entries: PropTypes.arrayOf(breadcrumbEntryShape),
className: PropTypes.string,
separator: PropTypes.string,
truncateFromStart: PropTypes.bool,
};
export default BreadcrumbTrail;