detailspanel, selection handling, single document details panel
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
+166
-581
@@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -185,9 +185,11 @@ const DocumentViewerPanel = ({
|
||||
if (!document) {
|
||||
return (
|
||||
<section className="document-viewer document-viewer--loading">
|
||||
<div className="document-viewer__details">
|
||||
<div className="document-viewer__message">
|
||||
Loading document{documentId ? ` ${documentId}` : ''}…
|
||||
<div className="document-viewer__details-pane">
|
||||
<div className="document-viewer__details">
|
||||
<div className="document-viewer__message">
|
||||
Loading document{documentId ? ` ${documentId}` : ''}…
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="document-viewer__viewport">
|
||||
@@ -199,17 +201,19 @@ const DocumentViewerPanel = ({
|
||||
|
||||
return (
|
||||
<section className="document-viewer">
|
||||
<div className="document-viewer__details">
|
||||
<DocumentInfoPanel
|
||||
document={document}
|
||||
summaryProps={summaryProps}
|
||||
metadataPayload={metadataPayload}
|
||||
contentConfig={contentTabConfig}
|
||||
defaultTabId="details"
|
||||
classNamePrefix="document-viewer"
|
||||
hideTabNavWhenSingle={false}
|
||||
resetKey={document?.id}
|
||||
/>
|
||||
<div className="document-viewer__details-pane">
|
||||
<div className="document-viewer__details">
|
||||
<DocumentInfoPanel
|
||||
document={document}
|
||||
summaryProps={summaryProps}
|
||||
metadataPayload={metadataPayload}
|
||||
contentConfig={contentTabConfig}
|
||||
defaultTabId="details"
|
||||
classNamePrefix="document-viewer"
|
||||
hideTabNavWhenSingle={false}
|
||||
resetKey={document?.id}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="document-viewer__viewport">
|
||||
{!previewEntry?.url ? (
|
||||
|
||||
+61
-33
@@ -864,7 +864,7 @@ button.danger:hover:not([disabled]) {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 30em) minmax(0, 1fr);
|
||||
gap: 1.5rem;
|
||||
gap: 1rem;
|
||||
min-height: 0;
|
||||
padding: 1rem 1rem;
|
||||
}
|
||||
@@ -973,7 +973,14 @@ button.danger:hover:not([disabled]) {
|
||||
gap: 0.5rem;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.document-viewer__details-pane {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
flex: 1;
|
||||
}
|
||||
.document-viewer__tabs-wrapper {
|
||||
display: flex;
|
||||
@@ -987,7 +994,6 @@ button.danger:hover:not([disabled]) {
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
@@ -1123,7 +1129,6 @@ button.danger:hover:not([disabled]) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
overflow: auto;
|
||||
padding: 1rem 0;
|
||||
font-size: 1rem;
|
||||
white-space: pre-wrap;
|
||||
@@ -2882,7 +2887,7 @@ button.danger:hover:not([disabled]) {
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
padding: 1.25rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.detail-section__header {
|
||||
@@ -3134,6 +3139,18 @@ button.danger:hover:not([disabled]) {
|
||||
|
||||
.preview-pane {
|
||||
margin-top: 0.4rem;
|
||||
background: transparent;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.25rem;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.preview-pane__media {
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
min-height: 220px;
|
||||
@@ -3144,6 +3161,44 @@ button.danger:hover:not([disabled]) {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.preview-image {
|
||||
width: 100%;
|
||||
max-width: 360px;
|
||||
max-height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
pointer-events: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.preview-image__content {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
outline-color 120ms ease,
|
||||
box-shadow 120ms ease,
|
||||
filter 120ms ease,
|
||||
background-color 120ms ease;
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.preview-image__content:hover,
|
||||
.preview-image__content:focus-visible {
|
||||
outline-color: var(--accent-focus);
|
||||
box-shadow:
|
||||
inset 0 0 0 999px var(--accent-elevated),
|
||||
0 6px 18px var(--accent-elevated-strong);
|
||||
}
|
||||
|
||||
.thumbnail-preview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -3381,33 +3436,6 @@ button.danger:hover:not([disabled]) {
|
||||
height: 80%;
|
||||
}
|
||||
|
||||
.preview-stack__image {
|
||||
display: block;
|
||||
width: auto;
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
outline-color 120ms ease,
|
||||
box-shadow 120ms ease,
|
||||
filter 120ms ease,
|
||||
background-color 120ms ease;
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: -2px;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.preview-stack__image:hover,
|
||||
.preview-stack__image:focus-visible {
|
||||
outline-color: var(--accent-focus);
|
||||
box-shadow:
|
||||
inset 0 0 0 999px var(--accent-elevated),
|
||||
0 6px 18px var(--accent-elevated-strong);
|
||||
}
|
||||
|
||||
.preview-pane__unsupported {
|
||||
width: 100%;
|
||||
max-width: 320px;
|
||||
@@ -3501,7 +3529,7 @@ button.danger:hover:not([disabled]) {
|
||||
transform: scale(calc(1 / var(--preview-nav-scale, 1)));
|
||||
}
|
||||
|
||||
.preview-pane--stack:hover .preview-pane__nav--overlay,
|
||||
.preview-pane__media:hover .preview-pane__nav--overlay,
|
||||
.desk-item__card:hover .preview-pane__nav--overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user