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