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
+166 -581
View File
@@ -7,23 +7,15 @@ import {
WindowMaximizeIcon,
} from '../ui/icons';
import PanelHeader from '../ui/PanelHeader';
import { formatFileSize } from '../utils/format';
import { resolveDocumentAssetUrl, createAssetView } from '../asset_manager';
import { useAssetNavigator } from '../hooks/useAssetNavigator';
import { describeDocumentSummary } from '../documents/documentSummary';
import { createDocumentActionState } from '../documents/documentActions';
import PreviewZoomOverlay from './PreviewZoomOverlay';
import DocumentInfoPanel from '../documents/DocumentInfoPanel';
import {
TagSection,
CorrespondentSection,
sortCorrespondents,
buildCorrespondentOptions,
} from '../documents/DocumentSummarySection';
import { sortCorrespondents, buildCorrespondentOptions } from '../documents/DocumentSummarySection';
import BreadcrumbTrail from '../ui/BreadcrumbTrail';
const MAX_PREVIEW_STACK_ITEMS = 15;
const derivePreviewOrientation = (metadata) => {
const width = Number(metadata?.width);
const height = Number(metadata?.height);
@@ -33,122 +25,107 @@ const derivePreviewOrientation = (metadata) => {
return 'landscape';
};
const computeStackAngle = (docId, index) => {
if (index === 0) return 0;
let hash = 0;
const source = docId || `stack-${index}`;
for (let i = 0; i < source.length; i += 1) {
hash = (hash * 31 + source.charCodeAt(i)) % 997;
}
const magnitude = Math.max(3, (hash % 13) + 3);
const sign = index % 2 === 0 ? 1 : -1;
return magnitude * sign;
};
const PreviewStack = ({
items = [],
maxItems = MAX_PREVIEW_STACK_ITEMS,
const PreviewImage = ({
item,
emptyMessage = 'Preview unavailable',
emptyContent = null,
onItemActivate,
onActivate,
onOpenPreview,
onZoomPreview,
showNav = false,
canGoPrev = false,
canGoNext = false,
onGoPrev = null,
onGoNext = null,
}) => {
const limited = useMemo(() => items.slice(0, maxItems), [items, maxItems]);
const hasMultiple = limited.length > 1;
const preparedItems = useMemo(
() =>
limited.map((entry, index) => ({
entry,
angle: index === 0 ? 0 : computeStackAngle(entry.id, index),
})),
[limited],
);
if (!limited.length) {
if (!item) {
return (
<div className="preview-stack preview-stack--empty">
<div className="preview-image preview-image--empty">
{emptyContent || <span className="meta">{emptyMessage}</span>}
</div>
);
}
const handleActivate = (event) => {
event.stopPropagation();
if (onZoomPreview) {
onZoomPreview(item);
} else if (onOpenPreview) {
onOpenPreview(item.id);
} else if (onActivate) {
onActivate(item.id);
}
};
const interceptNavPointer = (event) => {
event.preventDefault();
event.stopPropagation();
};
return (
<div className="preview-stack preview-stack--stacked">
{preparedItems.map(({ entry, angle }, index) => {
const transform = hasMultiple
? `translate(-50%, -50%) rotate(${angle}deg)`
: 'translate(-50%, -50%)';
const isFront = index === 0;
return (
<div
key={entry.id || index}
className={`preview-stack__item orientation-${entry.orientation || 'landscape'}`}
style={{
zIndex: preparedItems.length - index,
transform,
<div className="preview-image">
<img
src={item.url}
alt={item.alt}
className={`preview-image__content orientation-${item.orientation || 'landscape'}`}
onClick={handleActivate}
onKeyDown={(event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
event.stopPropagation();
handleActivate(event);
}
}}
/>
{showNav ? (
<div className="preview-pane__nav preview-pane__nav--overlay">
<button
type="button"
className="preview-pane__nav-button preview-pane__nav-button--prev"
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
onGoPrev?.();
}}
aria-hidden={
hasMultiple && !onItemActivate && !onOpenPreview && !onZoomPreview
? 'true'
: undefined
}
onPointerDown={interceptNavPointer}
onPointerUp={interceptNavPointer}
onMouseDown={interceptNavPointer}
onMouseUp={interceptNavPointer}
disabled={!canGoPrev}
aria-label="Previous preview"
>
<img
src={entry.url}
alt={entry.alt}
className="preview-stack__image"
onClick={(event) => {
event.stopPropagation();
if (isFront) {
if (onZoomPreview) {
onZoomPreview(entry);
} else if (onOpenPreview) {
onOpenPreview(entry.id);
} else if (onItemActivate) {
onItemActivate(entry.id);
}
} else if (onItemActivate) {
onItemActivate(entry.id);
}
}}
onKeyDown={(event) => {
if (!onItemActivate && !onOpenPreview && !onZoomPreview) return;
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
event.stopPropagation();
if (isFront) {
if (onZoomPreview) {
onZoomPreview(entry);
} else if (onOpenPreview) {
onOpenPreview(entry.id);
} else {
onItemActivate?.(entry.id);
}
} else {
onItemActivate?.(entry.id);
}
}
}}
/>
</div>
);
})}
<ArrowLeftIcon />
</button>
<button
type="button"
className="preview-pane__nav-button preview-pane__nav-button--next"
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
onGoNext?.();
}}
onPointerDown={interceptNavPointer}
onPointerUp={interceptNavPointer}
onMouseDown={interceptNavPointer}
onMouseUp={interceptNavPointer}
disabled={!canGoNext}
aria-label="Next preview"
>
<ArrowRightIcon />
</button>
</div>
) : null}
</div>
);
};
const DetailPanel = ({
selectedDocuments = [],
document = null,
tags = [],
tagLookupById = new Map(),
onTagAdd,
onTagRemove,
onOpenPreview,
onBulkTagAdd,
onBulkTagRemove,
onBulkCorrespondentAdd,
onBulkCorrespondentRemove,
onPromoteSelection,
onUpdateTitle = async () => false,
onUpdateIssued = async () => false,
@@ -163,13 +140,9 @@ const DetailPanel = ({
resolveFolderPath = null,
onClose = () => {},
}) => {
const selectedCount = selectedDocuments.length;
const singleDoc = selectedCount === 1 ? selectedDocuments[0] : null;
const singleDoc = document || null;
const singleDocId = singleDoc?.id || null;
const selectionKey = useMemo(
() => selectedDocuments.map((doc) => doc?.id ?? '').join('|'),
[selectedDocuments],
);
const selectionKey = singleDocId || 'none';
const { downloadHref: singleDownloadHref } = useMemo(
() =>
@@ -185,18 +158,10 @@ const DetailPanel = ({
const detailSummary = useMemo(() => describeDocumentSummary(singleDoc), [singleDoc]);
const headerTitle = useMemo(() => {
if (selectedCount === 0) {
return 'Document details';
}
if (selectedCount === 1) {
return detailSummary.title;
}
return `${selectedCount} document${selectedCount === 1 ? '' : 's'}`;
}, [selectedCount, detailSummary]);
const headerTitle = singleDoc ? detailSummary.title : 'Document details';
const headerBreadcrumbs = useMemo(() => {
if (selectedCount !== 1 || !singleDoc || typeof resolveFolderPath !== 'function') {
if (!singleDoc || typeof resolveFolderPath !== 'function') {
return null;
}
@@ -225,15 +190,10 @@ const DetailPanel = ({
label: detailSummary.title,
},
];
}, [selectedCount, singleDoc, resolveFolderPath, detailSummary, onFolderNavigate]);
}, [singleDoc, resolveFolderPath, detailSummary, onFolderNavigate]);
const [zoomedPreview, setZoomedPreview] = useState(null);
const bulkDocumentIds = useMemo(
() => selectedDocuments.map((doc) => doc?.id).filter(Boolean),
[selectedDocuments],
);
useEffect(() => {
setZoomedPreview(null);
}, [selectionKey]);
@@ -284,77 +244,22 @@ const DetailPanel = ({
[ensureAssetUrl, getDocumentAsset],
);
const stackDocuments = useMemo(() => {
if (!selectedDocuments.length) return [];
const seen = new Set();
const ordered = [];
for (let index = selectedDocuments.length - 1; index >= 0; index -= 1) {
const doc = selectedDocuments[index];
if (!doc?.id || seen.has(doc.id)) continue;
seen.add(doc.id);
ordered.push(doc);
if (ordered.length >= MAX_PREVIEW_STACK_ITEMS) {
break;
}
}
return ordered;
}, [selectedDocuments]);
const stackTopDocument = stackDocuments[0] || null;
const stackTopDocId = stackTopDocument?.id || null;
const stackPreviewNavigator = useAssetNavigator({
document: stackTopDocument,
assetType: 'preview',
ensureAssetUrl,
getAsset: getDocumentAsset,
prefetch: 3,
});
const singlePreviewItems = useMemo(() => {
if (!singleDoc) return [];
const singlePreviewItem = useMemo(() => {
if (!singleDoc) return null;
const url = singlePreviewNavigator.currentUrl;
if (!url) {
return [];
}
const orientation = derivePreviewOrientation(singlePreviewNavigator.currentMetadata);
return [
{
if (url) {
return {
id: singleDoc.id,
url,
orientation,
orientation: derivePreviewOrientation(singlePreviewNavigator.currentMetadata),
alt: singleDoc.title,
},
];
}, [singleDoc, singlePreviewNavigator.currentUrl, singlePreviewNavigator.currentMetadata]);
const stackPreviews = useMemo(() => {
if (!stackDocuments.length) {
return [];
};
}
return stackDocuments
.map((doc) => {
if (!doc) return null;
if (stackTopDocument && doc.id === stackTopDocument.id) {
const url = stackPreviewNavigator.currentUrl;
if (!url) {
return null;
}
const orientation = derivePreviewOrientation(stackPreviewNavigator.currentMetadata);
return {
id: doc.id,
url,
orientation,
alt: doc.title,
};
}
return makePreviewItem(doc, 1);
})
.filter(Boolean);
return makePreviewItem(singleDoc, 1);
}, [
stackDocuments,
stackTopDocument,
stackPreviewNavigator.currentUrl,
stackPreviewNavigator.currentMetadata,
singleDoc,
singlePreviewNavigator.currentUrl,
singlePreviewNavigator.currentMetadata,
makePreviewItem,
]);
@@ -362,42 +267,6 @@ const DetailPanel = ({
const singleEffectiveCardinality = singleCardinality || (singlePreviewNavigator.currentUrl ? 1 : 0);
const singleHasPreview = Boolean(singlePreviewNavigator.currentUrl);
const topDocId = stackTopDocument?.id || null;
const topCardinality = stackPreviewNavigator.cardinality;
const topEffectiveCardinality = topCardinality || (stackPreviewNavigator.currentUrl ? 1 : 0);
const topHasPreview = Boolean(stackPreviewNavigator.currentUrl);
const bulkTagUnion = useMemo(() => {
if (!selectedDocuments.length) return [];
const tagMap = new Map();
selectedDocuments.forEach((doc) => {
(doc.tags || []).forEach((tag) => {
if (!tag?.label) return;
const label = tag.label.trim();
if (!label) return;
if (!tagMap.has(label)) {
const fallback = tagLookupById.get(tag.id);
tagMap.set(label, {
id: tag.id,
label,
color: tag.color ?? fallback?.color ?? null,
});
}
});
});
return [...tagMap.values()].sort((a, b) => a.label.localeCompare(b.label));
}, [selectedDocuments, tagLookupById]);
const stackTotalSizeBytes = useMemo(() => {
if (!stackPreviews.length) return 0;
const byId = new Map(selectedDocuments.map((doc) => [doc.id, doc]));
return stackPreviews.reduce((sum, item) => {
const source = byId.get(item.id);
const bytes = source?.current_version?.size_bytes;
return sum + (typeof bytes === 'number' ? bytes : 0);
}, 0);
}, [stackPreviews, selectedDocuments]);
const correspondentOptions = useMemo(
() => buildCorrespondentOptions(Array.isArray(correspondents) ? correspondents : []),
[correspondents],
@@ -496,77 +365,9 @@ const DetailPanel = ({
[singleHasOcr, loadSingleOcrContent],
);
const bulkCorrespondents = useMemo(() => {
if (selectedDocuments.length <= 1) {
const doc = selectedDocuments[0];
return doc ? sortCorrespondents(doc.correspondents || []) : [];
}
const map = new Map();
selectedDocuments.forEach((doc) => {
if (!doc?.id) return;
(doc.correspondents || []).forEach((entry) => {
if (!entry?.id || typeof entry.name !== 'string') return;
if (!map.has(entry.id)) {
map.set(entry.id, {
id: entry.id,
name: entry.name,
documentIds: new Set(),
});
}
map.get(entry.id).documentIds.add(doc.id);
});
});
return [...map.values()]
.map((entry) => ({
id: entry.id,
name: entry.name,
documentIds: [...entry.documentIds],
count: entry.documentIds.size,
}))
.sort((a, b) => a.name.localeCompare(b.name));
}, [selectedDocuments]);
const handleBulkCorrespondentRemove = useCallback(
(entry) => {
if (!entry?.id) return;
if (onBulkCorrespondentRemove) {
return onBulkCorrespondentRemove({
assignments: [
{
correspondent_id: entry.id,
},
],
documentIds: entry.documentIds,
});
}
if (!onCorrespondentRemove) return;
const targets = entry.documentIds && entry.documentIds.length
? entry.documentIds
: selectedDocuments
.filter((doc) => (doc.correspondents || []).some((item) => item.id === entry.id))
.map((doc) => doc.id);
return Promise.all(
targets.map((documentId) =>
onCorrespondentRemove({
documentId,
correspondentId: entry.id,
}),
),
).catch(() => {});
},
[onBulkCorrespondentRemove, onCorrespondentRemove, selectedDocuments],
);
const openZoomPreview = useCallback((config) => {
if (!config) return;
setZoomedPreview({
mode: config.mode,
docId: config.docId ?? null,
});
const openZoomPreview = useCallback((docId) => {
if (!docId) return;
setZoomedPreview({ docId });
}, []);
const closeZoomPreview = useCallback(() => {
@@ -578,64 +379,32 @@ const DetailPanel = ({
if (!singleHasPreview) return;
const targetId = entry?.id ?? singleDocId;
if (!targetId) return;
openZoomPreview({ mode: 'single', docId: targetId });
openZoomPreview(targetId);
},
[openZoomPreview, singleHasPreview, singleDocId],
);
const handleStackZoom = useCallback(
(entry) => {
if (!stackTopDocId || entry?.id !== stackTopDocId) return;
if (!topHasPreview) return;
openZoomPreview({ mode: 'stack', docId: stackTopDocId });
},
[openZoomPreview, stackTopDocId, topHasPreview],
);
const zoomDisplay = useMemo(() => {
if (!zoomedPreview) {
if (
!zoomedPreview
|| !singleDoc
|| !singleDocId
|| zoomedPreview.docId !== singleDocId
|| !singleHasPreview
) {
return null;
}
if (
zoomedPreview.mode === 'single' &&
singleDocId &&
singleDoc &&
singleHasPreview &&
zoomedPreview.docId === singleDocId
) {
return {
url: singlePreviewNavigator.currentUrl,
alt: singleDoc.title,
canGoPrev:
singleEffectiveCardinality > 1 && Boolean(singlePreviewNavigator.canGoPrev),
canGoNext:
singleEffectiveCardinality > 1 && Boolean(singlePreviewNavigator.canGoNext),
goPrev: singlePreviewNavigator.goPrev,
goNext: singlePreviewNavigator.goNext,
};
}
if (
zoomedPreview.mode === 'stack' &&
stackTopDocId &&
stackTopDocument &&
zoomedPreview.docId === stackTopDocId &&
topHasPreview
) {
return {
url: stackPreviewNavigator.currentUrl,
alt: stackTopDocument.title,
canGoPrev:
topEffectiveCardinality > 1 && Boolean(stackPreviewNavigator.canGoPrev),
canGoNext:
topEffectiveCardinality > 1 && Boolean(stackPreviewNavigator.canGoNext),
goPrev: stackPreviewNavigator.goPrev,
goNext: stackPreviewNavigator.goNext,
};
}
return null;
return {
url: singlePreviewNavigator.currentUrl,
alt: singleDoc.title,
canGoPrev:
singleEffectiveCardinality > 1 && Boolean(singlePreviewNavigator.canGoPrev),
canGoNext:
singleEffectiveCardinality > 1 && Boolean(singlePreviewNavigator.canGoNext),
goPrev: singlePreviewNavigator.goPrev,
goNext: singlePreviewNavigator.goNext,
};
}, [
zoomedPreview,
singleDoc,
@@ -647,15 +416,6 @@ const DetailPanel = ({
singlePreviewNavigator.goPrev,
singlePreviewNavigator.goNext,
singleEffectiveCardinality,
stackTopDocument,
stackTopDocId,
topHasPreview,
stackPreviewNavigator.currentUrl,
stackPreviewNavigator.canGoPrev,
stackPreviewNavigator.canGoNext,
stackPreviewNavigator.goPrev,
stackPreviewNavigator.goNext,
topEffectiveCardinality,
]);
useEffect(() => {
@@ -673,82 +433,41 @@ const DetailPanel = ({
cardinality: singleNavigatorCardinality,
} = singlePreviewNavigator;
const {
documentId: stackNavigatorDocId,
asset: stackNavigatorAsset,
ordinal: stackNavigatorOrdinal,
canGoPrev: stackNavigatorCanGoPrev,
canGoNext: stackNavigatorCanGoNext,
cardinality: stackNavigatorCardinality,
} = stackPreviewNavigator;
useEffect(() => {
if (typeof ensureAssetUrl !== 'function') {
return;
}
if (!singleNavigatorDocId || !singleNavigatorAsset || !Number.isFinite(singleNavigatorOrdinal)) {
return;
}
const warmNavigator = (navigator) => {
const {
documentId,
asset,
ordinal,
canGoPrev,
canGoNext,
cardinality,
} = navigator;
if (!documentId || !asset || !Number.isFinite(ordinal)) {
return;
const requests = [];
if (singleNavigatorCanGoPrev) {
const prevOrdinal = Math.max(1, singleNavigatorOrdinal - 1);
if (!singleNavigatorCardinality || prevOrdinal <= singleNavigatorCardinality) {
requests.push(
ensureAssetUrl(singleNavigatorDocId, singleNavigatorAsset, {
start: prevOrdinal,
limit: 1,
objectOrdinal: prevOrdinal,
}),
);
}
const requests = [];
if (canGoPrev) {
const prevOrdinal = Math.max(1, ordinal - 1);
if (!cardinality || prevOrdinal <= cardinality) {
requests.push(
ensureAssetUrl(documentId, asset, {
start: prevOrdinal,
limit: 1,
objectOrdinal: prevOrdinal,
}),
);
}
}
if (canGoNext) {
const nextOrdinal = ordinal + 1;
if (!cardinality || nextOrdinal <= cardinality) {
requests.push(
ensureAssetUrl(documentId, asset, {
start: nextOrdinal,
limit: 1,
objectOrdinal: nextOrdinal,
}),
);
}
}
if (singleNavigatorCanGoNext) {
const nextOrdinal = singleNavigatorOrdinal + 1;
if (!singleNavigatorCardinality || nextOrdinal <= singleNavigatorCardinality) {
requests.push(
ensureAssetUrl(singleNavigatorDocId, singleNavigatorAsset, {
start: nextOrdinal,
limit: 1,
objectOrdinal: nextOrdinal,
}),
);
}
}
requests.forEach((promise) => promise?.catch?.(() => {}));
};
const singleWarmState = {
documentId: singleNavigatorDocId,
asset: singleNavigatorAsset,
ordinal: singleNavigatorOrdinal,
canGoPrev: singleNavigatorCanGoPrev,
canGoNext: singleNavigatorCanGoNext,
cardinality: singleNavigatorCardinality,
};
const stackWarmState = {
documentId: stackNavigatorDocId,
asset: stackNavigatorAsset,
ordinal: stackNavigatorOrdinal,
canGoPrev: stackNavigatorCanGoPrev,
canGoNext: stackNavigatorCanGoNext,
cardinality: stackNavigatorCardinality,
};
warmNavigator(singleWarmState);
warmNavigator(stackWarmState);
requests.forEach((promise) => promise?.catch?.(() => {}));
}, [
ensureAssetUrl,
singleNavigatorDocId,
@@ -757,25 +476,18 @@ const DetailPanel = ({
singleNavigatorCanGoPrev,
singleNavigatorCanGoNext,
singleNavigatorCardinality,
stackNavigatorDocId,
stackNavigatorAsset,
stackNavigatorOrdinal,
stackNavigatorCanGoPrev,
stackNavigatorCanGoNext,
stackNavigatorCardinality,
]);
const renderSingle = () => {
const renderContent = () => {
if (!singleDoc) {
return <p className="meta">Select a document to view metadata, tags and actions.</p>;
}
const effectiveCardinality =
singlePreviewNavigator.cardinality || (singlePreviewNavigator.currentUrl ? 1 : 0);
const canGoPrev = singlePreviewNavigator.canGoPrev;
const canGoNext = singlePreviewNavigator.canGoNext;
const effectiveCardinality = singleEffectiveCardinality;
const navCanGoPrev = Boolean(singlePreviewNavigator.canGoPrev);
const navCanGoNext = Boolean(singlePreviewNavigator.canGoNext);
const hasPreviewImage = Boolean(singlePreviewNavigator.currentUrl);
const previewMissingAsset = !singleNavigatorAsset;
const previewMissingAsset = !singlePreviewNavigator.currentUrl;
const displayContentType = singleDoc.content_type || 'this file type';
const displayFilename =
singleDoc.filename || singleDoc.original_name || singleDoc.title || 'download';
@@ -799,61 +511,24 @@ const DetailPanel = ({
</div>
) : null;
const emptyMessage = previewMissingAsset ? 'Preview unavailable' : 'Preview loading…';
const interceptNavPointer = (event) => {
event.preventDefault();
event.stopPropagation();
};
const showNav = hasPreviewImage && (effectiveCardinality > 1 || navCanGoPrev || navCanGoNext);
return (
<>
<div className="preview-pane preview-pane--stack">
<PreviewStack
items={singlePreviewItems}
maxItems={1}
<div className="preview-pane">
<div className="preview-pane__media">
<PreviewImage
item={singlePreviewItem}
emptyMessage={emptyMessage}
emptyContent={previewFallback}
onItemActivate={handlePreviewActivate}
onActivate={handlePreviewActivate}
onOpenPreview={onOpenPreview}
onZoomPreview={handleSingleZoom}
showNav={showNav}
canGoPrev={navCanGoPrev}
canGoNext={navCanGoNext}
onGoPrev={navCanGoPrev ? singlePreviewNavigator.goPrev : undefined}
onGoNext={navCanGoNext ? singlePreviewNavigator.goNext : undefined}
/>
{hasPreviewImage && (effectiveCardinality > 1 || canGoPrev || canGoNext) ? (
<div className="preview-pane__nav preview-pane__nav--overlay">
<button
type="button"
className="preview-pane__nav-button preview-pane__nav-button--prev"
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
singlePreviewNavigator.goPrev();
}}
onPointerDown={interceptNavPointer}
onPointerUp={interceptNavPointer}
onMouseDown={interceptNavPointer}
onMouseUp={interceptNavPointer}
disabled={!canGoPrev}
aria-label="Previous preview"
>
<ArrowLeftIcon />
</button>
<button
type="button"
className="preview-pane__nav-button preview-pane__nav-button--next"
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
singlePreviewNavigator.goNext();
}}
onPointerDown={interceptNavPointer}
onPointerUp={interceptNavPointer}
onMouseDown={interceptNavPointer}
onMouseUp={interceptNavPointer}
disabled={!canGoNext}
aria-label="Next preview"
>
<ArrowRightIcon />
</button>
</div>
) : null}
</div>
<div className="document-viewer__details">
<DocumentInfoPanel
@@ -866,99 +541,7 @@ const DetailPanel = ({
hideTabNavWhenSingle={false}
/>
</div>
</>
);
};
const renderBulk = () => {
const countLabel = `${selectedCount} document${selectedCount === 1 ? '' : 's'}`;
const sizeLabel = stackTotalSizeBytes ? formatFileSize(stackTotalSizeBytes) : '—';
const headerLabel = `${countLabel}${sizeLabel ? ` (${sizeLabel})` : ''}`;
const topDocIdLocal = topDocId;
const topCardinalityLocal = topEffectiveCardinality;
const topHasPreview = Boolean(stackPreviewNavigator.currentUrl);
const topCanGoPrev = stackPreviewNavigator.canGoPrev;
const topCanGoNext = stackPreviewNavigator.canGoNext;
const interceptTopNavPointer = (event) => {
event.preventDefault();
event.stopPropagation();
};
const documentIds = bulkDocumentIds;
return (
<>
<div className="preview-pane preview-pane--stack">
<PreviewStack
items={stackPreviews}
emptyMessage="No previews available."
onItemActivate={handlePreviewActivate}
onOpenPreview={onOpenPreview}
onZoomPreview={handleStackZoom}
/>
{topDocIdLocal && topHasPreview && (topCardinalityLocal > 1 || topCanGoPrev || topCanGoNext) ? (
<div className="preview-pane__nav preview-pane__nav--overlay">
<button
type="button"
className="preview-pane__nav-button preview-pane__nav-button--prev"
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
stackPreviewNavigator.goPrev();
}}
onPointerDown={interceptTopNavPointer}
onPointerUp={interceptTopNavPointer}
onMouseDown={interceptTopNavPointer}
onMouseUp={interceptTopNavPointer}
disabled={!topCanGoPrev}
aria-label="Previous preview"
>
<ArrowLeftIcon />
</button>
<button
type="button"
className="preview-pane__nav-button preview-pane__nav-button--next"
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
stackPreviewNavigator.goNext();
}}
onPointerDown={interceptTopNavPointer}
onPointerUp={interceptTopNavPointer}
onMouseDown={interceptTopNavPointer}
onMouseUp={interceptTopNavPointer}
disabled={!topCanGoNext}
aria-label="Next preview"
>
<ArrowRightIcon />
</button>
</div>
) : null}
</div>
<h3 style={{ margin: 0 }}>{headerLabel}</h3>
<TagSection
tags={bulkTagUnion}
emptyMessage="No tags assigned."
onRemove={(tag) => onBulkTagRemove?.({ label: tag.label, documentIds })}
onAdd={({ value }) =>
onBulkTagAdd?.({ label: value, input: null, documentIds })
}
addPlaceholder="Add tag to selection"
addButtonLabel="Add tag"
datalistOptions={tags}
className="bulk-tags"
/>
<CorrespondentSection
entries={bulkCorrespondents}
onRemove={handleBulkCorrespondentRemove}
onAdd={({ name }) =>
onBulkCorrespondentAdd?.({ name, input: null, documentIds })
}
addPlaceholder="Add correspondent to selection"
datalistOptions={correspondentOptions}
showCount
className="bulk-correspondents"
/>
</>
</div>
);
};
@@ -1035,14 +618,16 @@ const DetailPanel = ({
actions={headerActions.length ? headerActions : null}
/>
<div className="panel-body">
{selectedCount <= 1 ? renderSingle() : renderBulk()}
{renderContent()}
</div>
</aside>
<PreviewZoomOverlay
open={Boolean(zoomDisplay?.url)}
display={zoomDisplay}
onClose={closeZoomPreview}
/>
{singleDoc && (
<PreviewZoomOverlay
open={Boolean(zoomDisplay)}
display={zoomDisplay}
onClose={closeZoomPreview}
/>
)}
</>
);
};