811 lines
22 KiB
React
811 lines
22 KiB
React
import React, {
|
|
useCallback,
|
|
useEffect,
|
|
useMemo,
|
|
useRef,
|
|
useState,
|
|
} from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import {
|
|
DownloadIcon,
|
|
CloseIcon,
|
|
IconZoomInArea,
|
|
DetailPanelCollapseIcon,
|
|
WindowMaximizeIcon,
|
|
} from '../ui/icons';
|
|
import PreviewZoomOverlay from '../detail/PreviewZoomOverlay';
|
|
import {
|
|
buildCorrespondentOptions,
|
|
sortCorrespondents,
|
|
} from '../documents/DocumentSummarySection';
|
|
import { extractDocumentMetadataPayload } from '../documents/documentMetadata';
|
|
import { createDocumentActionState } from '../documents/documentActions';
|
|
import { resolveDocumentAssetUrl } from '../asset_manager';
|
|
import PanelHeader from '../ui/PanelHeader';
|
|
import BreadcrumbTrail from '../ui/BreadcrumbTrail';
|
|
import { useAssetNavigator } from '../hooks/useAssetNavigator';
|
|
import DocumentViewerLayout from './DocumentViewerLayout';
|
|
import useViewerLayoutMode from './useViewerLayoutMode';
|
|
import { useSidebarContext } from '../sidebar/SidebarContext';
|
|
|
|
const DETAIL_PANEL_WIDTH_STORAGE_KEY = 'detailPanelWidth';
|
|
const MIN_DETAIL_PANEL_WIDTH = 320;
|
|
const MAX_DETAIL_PANEL_WIDTH = 960;
|
|
|
|
const isBrowser = typeof window !== 'undefined';
|
|
const isDocumentAvailable = typeof document !== 'undefined';
|
|
|
|
const getDetailPanelBounds = () => {
|
|
if (!isBrowser) {
|
|
return {
|
|
min: MIN_DETAIL_PANEL_WIDTH,
|
|
max: MAX_DETAIL_PANEL_WIDTH,
|
|
};
|
|
}
|
|
const viewportWidth = Math.max(window.innerWidth, 1);
|
|
const minFractionWidth = viewportWidth / 5;
|
|
const maxFractionWidth = viewportWidth * 0.75;
|
|
const rawMin = Math.max(MIN_DETAIL_PANEL_WIDTH, minFractionWidth);
|
|
const rawMax = Math.min(MAX_DETAIL_PANEL_WIDTH, maxFractionWidth);
|
|
if (rawMin >= rawMax) {
|
|
const fallback = Math.min(Math.max(rawMin, viewportWidth * 0.5), MAX_DETAIL_PANEL_WIDTH);
|
|
return { min: fallback, max: fallback };
|
|
}
|
|
return {
|
|
min: rawMin,
|
|
max: rawMax,
|
|
};
|
|
};
|
|
|
|
const clampDetailPanelWidth = (value) => {
|
|
if (!Number.isFinite(value) || value <= 0) {
|
|
return null;
|
|
}
|
|
const { min, max } = getDetailPanelBounds();
|
|
return Math.min(Math.max(value, min), max);
|
|
};
|
|
|
|
const loadStoredDetailPanelWidth = () => {
|
|
if (!isBrowser) {
|
|
return null;
|
|
}
|
|
try {
|
|
const raw = window.localStorage?.getItem(DETAIL_PANEL_WIDTH_STORAGE_KEY);
|
|
if (!raw) {
|
|
return null;
|
|
}
|
|
const parsed = parseInt(raw, 10);
|
|
return Number.isFinite(parsed) ? parsed : null;
|
|
} catch (error) {
|
|
console.warn('Failed to read detail panel width', error);
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const applyDetailPanelWidth = (width) => {
|
|
if (!isDocumentAvailable || width == null) {
|
|
return;
|
|
}
|
|
document.documentElement.style.setProperty('--detail-panel-width', `${width}px`);
|
|
};
|
|
|
|
const getSidebarWidthFromRoot = () => {
|
|
if (!isBrowser || !isDocumentAvailable) {
|
|
return null;
|
|
}
|
|
const computed = window.getComputedStyle(document.documentElement);
|
|
const parsed = parseFloat(computed.getPropertyValue('--sidebar-width'));
|
|
return Number.isFinite(parsed) ? parsed : null;
|
|
};
|
|
|
|
const shouldSuppressSidebarForDetailWidth = (detailWidth) => {
|
|
if (!isBrowser || !Number.isFinite(detailWidth)) {
|
|
return false;
|
|
}
|
|
const sidebarWidth = getSidebarWidthFromRoot() || 0;
|
|
const minMainContentWidth = window.innerWidth / 3;
|
|
const occupiedWidth = detailWidth + sidebarWidth;
|
|
const availableWidth = window.innerWidth - occupiedWidth;
|
|
return availableWidth < minMainContentWidth;
|
|
};
|
|
|
|
export const createDocumentViewerHeaderActions = ({
|
|
document,
|
|
actionState,
|
|
previewEntry,
|
|
onZoom,
|
|
canZoom = false,
|
|
}) => {
|
|
if (!document) {
|
|
return null;
|
|
}
|
|
|
|
const downloadHref = actionState?.downloadHref || previewEntry?.url;
|
|
if (!downloadHref && !(canZoom && onZoom)) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<>
|
|
{downloadHref ? (
|
|
<a
|
|
className="icon-button"
|
|
href={downloadHref}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
aria-label="Download document"
|
|
title="Download document"
|
|
>
|
|
<DownloadIcon />
|
|
</a>
|
|
) : null}
|
|
{canZoom && onZoom ? (
|
|
<button
|
|
type="button"
|
|
className="icon-button"
|
|
onClick={onZoom}
|
|
aria-label="Open zoom preview"
|
|
title="Open zoom preview"
|
|
>
|
|
<IconZoomInArea />
|
|
</button>
|
|
) : null}
|
|
</>
|
|
);
|
|
};
|
|
|
|
const DocumentViewerPanel = ({
|
|
document,
|
|
previewEntry,
|
|
hydrateDocument,
|
|
tagLookupById,
|
|
tagOptions,
|
|
onTagAdd,
|
|
onTagRemove,
|
|
correspondents,
|
|
onCorrespondentAdd,
|
|
onCorrespondentRemove,
|
|
onUpdateTitle,
|
|
onUpdateIssued,
|
|
ensureAssetUrl,
|
|
getDocumentAsset,
|
|
ensurePreviewData,
|
|
resolveApiPath,
|
|
notifyApiError,
|
|
sidebarToggle = null,
|
|
onClosePanel,
|
|
resolveFolderPath,
|
|
variant = 'viewer',
|
|
onCollapsePanel,
|
|
onMaximizePanel,
|
|
}) => {
|
|
const navigate = useNavigate();
|
|
const isSidebarVariant = variant === 'sidebar';
|
|
const { setSidebarSuppressed } = useSidebarContext();
|
|
const sortedCorrespondents = useMemo(
|
|
() => sortCorrespondents(document?.correspondents || []),
|
|
[document],
|
|
);
|
|
|
|
const correspondentOptions = useMemo(
|
|
() => buildCorrespondentOptions(Array.isArray(correspondents) ? correspondents : []),
|
|
[correspondents],
|
|
);
|
|
|
|
const metadataPayload = useMemo(
|
|
() => extractDocumentMetadataPayload(document),
|
|
[document],
|
|
);
|
|
|
|
const hasOcr = useMemo(() => {
|
|
if (!document || typeof getDocumentAsset !== 'function') {
|
|
return false;
|
|
}
|
|
return Boolean(getDocumentAsset(document, 'ocr-text'));
|
|
}, [document, getDocumentAsset]);
|
|
|
|
const summaryProps = useMemo(
|
|
() => ({
|
|
tagLookupById,
|
|
tagOptions,
|
|
onTagAdd,
|
|
onTagRemove,
|
|
correspondents: sortedCorrespondents,
|
|
correspondentOptions,
|
|
onCorrespondentAdd,
|
|
onCorrespondentRemove,
|
|
onUpdateTitle,
|
|
onUpdateIssued,
|
|
}),
|
|
[
|
|
tagLookupById,
|
|
tagOptions,
|
|
onTagAdd,
|
|
onTagRemove,
|
|
sortedCorrespondents,
|
|
correspondentOptions,
|
|
onCorrespondentAdd,
|
|
onCorrespondentRemove,
|
|
onUpdateTitle,
|
|
onUpdateIssued,
|
|
],
|
|
);
|
|
|
|
const loadOcrContent = useCallback(async ({ signal } = {}) => {
|
|
if (!document || !hasOcr || typeof getDocumentAsset !== 'function') {
|
|
return '';
|
|
}
|
|
|
|
const updateUrl = () =>
|
|
resolveDocumentAssetUrl(document, 'ocr-text', {
|
|
ensureAssetUrl,
|
|
getAsset: getDocumentAsset,
|
|
});
|
|
|
|
const asset = getDocumentAsset(document, 'ocr-text');
|
|
let url = updateUrl();
|
|
|
|
if (!url && document.id && asset?.id && typeof ensureAssetUrl === 'function') {
|
|
await ensureAssetUrl(document.id, asset, { start: 1, limit: 1 });
|
|
if (signal?.aborted) {
|
|
throw new DOMException('Aborted', 'AbortError');
|
|
}
|
|
url = updateUrl();
|
|
}
|
|
|
|
if (!url) {
|
|
return '';
|
|
}
|
|
|
|
const response = await fetch(url, {
|
|
method: 'GET',
|
|
mode: 'cors',
|
|
credentials: 'omit',
|
|
signal,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Unexpected status: ${response.status}`);
|
|
}
|
|
|
|
return response.text();
|
|
}, [document, hasOcr, getDocumentAsset, ensureAssetUrl]);
|
|
|
|
const contentTabConfig = useMemo(
|
|
() => ({
|
|
enabled: hasOcr,
|
|
id: 'content',
|
|
label: 'Content',
|
|
loadContent: loadOcrContent,
|
|
loadingMessage: 'Loading OCR content…',
|
|
emptyMessage: 'No OCR content available.',
|
|
unavailableMessage: 'No OCR content available.',
|
|
errorMessage: 'Failed to load OCR content.',
|
|
}),
|
|
[hasOcr, loadOcrContent],
|
|
);
|
|
|
|
const [zoomOverlayOpen, setZoomOverlayOpen] = useState(false);
|
|
const previewNavigator = useAssetNavigator({
|
|
document,
|
|
assetType: 'preview',
|
|
ensureAssetUrl,
|
|
getAsset: getDocumentAsset,
|
|
prefetch: 3,
|
|
});
|
|
const navigatorUrl = previewNavigator?.currentUrl;
|
|
const navigatorCanGoPrev = Boolean(previewNavigator?.canGoPrev);
|
|
const navigatorCanGoNext = Boolean(previewNavigator?.canGoNext);
|
|
const navigatorGoPrev = previewNavigator?.goPrev;
|
|
const navigatorGoNext = previewNavigator?.goNext;
|
|
|
|
const handleZoomOpen = useCallback(() => {
|
|
if (!previewEntry?.url) {
|
|
return;
|
|
}
|
|
setZoomOverlayOpen(true);
|
|
}, [previewEntry?.url]);
|
|
|
|
const handleZoomClose = useCallback(() => {
|
|
setZoomOverlayOpen(false);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
setZoomOverlayOpen(false);
|
|
}, [previewEntry?.url, document?.id]);
|
|
|
|
useEffect(() => {
|
|
if (typeof hydrateDocument === 'function' && document?.id) {
|
|
hydrateDocument(document.id);
|
|
}
|
|
}, [hydrateDocument, document?.id]);
|
|
|
|
const panelRef = useRef(null);
|
|
const isStackedLayout = useViewerLayoutMode(panelRef, document?.id);
|
|
const pendingWidthRef = useRef(null);
|
|
const [isResizingPanel, setIsResizingPanel] = useState(false);
|
|
const [detailPanelWidth, setDetailPanelWidth] = useState(() => {
|
|
if (!isBrowser) {
|
|
return null;
|
|
}
|
|
const stored = loadStoredDetailPanelWidth();
|
|
return stored != null ? clampDetailPanelWidth(stored) : null;
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (detailPanelWidth != null) {
|
|
const clamped = clampDetailPanelWidth(detailPanelWidth);
|
|
if (clamped != null) {
|
|
applyDetailPanelWidth(clamped);
|
|
}
|
|
}
|
|
}, [detailPanelWidth]);
|
|
|
|
useEffect(() => {
|
|
if (!isSidebarVariant) {
|
|
setSidebarSuppressed(false);
|
|
return undefined;
|
|
}
|
|
const updateSuppression = () => {
|
|
if (!isBrowser) {
|
|
setSidebarSuppressed(false);
|
|
return;
|
|
}
|
|
const panelWidth = pendingWidthRef.current
|
|
?? detailPanelWidth
|
|
?? panelRef.current?.getBoundingClientRect().width;
|
|
setSidebarSuppressed(shouldSuppressSidebarForDetailWidth(panelWidth));
|
|
};
|
|
|
|
updateSuppression();
|
|
const handleWindowResize = () => updateSuppression();
|
|
window.addEventListener('resize', handleWindowResize);
|
|
return () => {
|
|
window.removeEventListener('resize', handleWindowResize);
|
|
};
|
|
}, [isSidebarVariant, detailPanelWidth, setSidebarSuppressed]);
|
|
|
|
useEffect(() => {
|
|
if (!isSidebarVariant) {
|
|
setSidebarSuppressed(false);
|
|
}
|
|
}, [isSidebarVariant, setSidebarSuppressed]);
|
|
|
|
useEffect(() => {
|
|
if (!isBrowser) {
|
|
return undefined;
|
|
}
|
|
const handleResize = () => {
|
|
setDetailPanelWidth((prev) => {
|
|
if (prev == null) {
|
|
return prev;
|
|
}
|
|
const clamped = clampDetailPanelWidth(prev);
|
|
if (clamped != null && clamped !== prev) {
|
|
applyDetailPanelWidth(clamped);
|
|
try {
|
|
window.localStorage?.setItem(DETAIL_PANEL_WIDTH_STORAGE_KEY, String(Math.round(clamped)));
|
|
} catch (error) {
|
|
console.warn('Failed to persist detail panel width', error);
|
|
}
|
|
return clamped;
|
|
}
|
|
return prev;
|
|
});
|
|
};
|
|
window.addEventListener('resize', handleResize);
|
|
return () => window.removeEventListener('resize', handleResize);
|
|
}, []);
|
|
|
|
const handleResizePointerDown = useCallback((event) => {
|
|
if (!isSidebarVariant || !panelRef.current || !isBrowser) {
|
|
return;
|
|
}
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
const pointerId = event.pointerId;
|
|
const target = event.currentTarget;
|
|
target.setPointerCapture?.(pointerId);
|
|
setIsResizingPanel(true);
|
|
|
|
const rect = panelRef.current.getBoundingClientRect();
|
|
const startWidth = rect.width;
|
|
const startX = event.clientX;
|
|
|
|
const updateWidth = (nextWidth) => {
|
|
const clamped = clampDetailPanelWidth(nextWidth);
|
|
if (clamped != null) {
|
|
pendingWidthRef.current = clamped;
|
|
applyDetailPanelWidth(clamped);
|
|
if (isSidebarVariant) {
|
|
setSidebarSuppressed(shouldSuppressSidebarForDetailWidth(clamped));
|
|
}
|
|
}
|
|
};
|
|
|
|
const handlePointerMove = (moveEvent) => {
|
|
if (moveEvent.pointerId !== pointerId) {
|
|
return;
|
|
}
|
|
const delta = startX - moveEvent.clientX;
|
|
updateWidth(startWidth + delta);
|
|
};
|
|
|
|
const handlePointerUp = (upEvent) => {
|
|
if (upEvent.pointerId !== pointerId) {
|
|
return;
|
|
}
|
|
target.releasePointerCapture?.(pointerId);
|
|
window.removeEventListener('pointermove', handlePointerMove);
|
|
window.removeEventListener('pointerup', handlePointerUp);
|
|
setIsResizingPanel(false);
|
|
if (pendingWidthRef.current != null) {
|
|
const finalizedWidth = pendingWidthRef.current;
|
|
pendingWidthRef.current = null;
|
|
setDetailPanelWidth(finalizedWidth);
|
|
try {
|
|
window.localStorage?.setItem(DETAIL_PANEL_WIDTH_STORAGE_KEY, String(Math.round(finalizedWidth)));
|
|
} catch (error) {
|
|
console.warn('Failed to persist detail panel width', error);
|
|
}
|
|
}
|
|
};
|
|
|
|
window.addEventListener('pointermove', handlePointerMove);
|
|
window.addEventListener('pointerup', handlePointerUp);
|
|
}, [isSidebarVariant, setSidebarSuppressed]);
|
|
|
|
const handleResizeKeyDown = useCallback((event) => {
|
|
if (!isSidebarVariant || !isBrowser) {
|
|
return;
|
|
}
|
|
if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') {
|
|
return;
|
|
}
|
|
const baseWidth = pendingWidthRef.current
|
|
?? detailPanelWidth
|
|
?? panelRef.current?.getBoundingClientRect().width;
|
|
if (!Number.isFinite(baseWidth)) {
|
|
return;
|
|
}
|
|
event.preventDefault();
|
|
const step = event.shiftKey ? 40 : 20;
|
|
const delta = event.key === 'ArrowLeft' ? step : -step;
|
|
const nextWidth = clampDetailPanelWidth(baseWidth + delta);
|
|
if (nextWidth == null) {
|
|
return;
|
|
}
|
|
setDetailPanelWidth(nextWidth);
|
|
try {
|
|
window.localStorage?.setItem(DETAIL_PANEL_WIDTH_STORAGE_KEY, String(Math.round(nextWidth)));
|
|
} catch (error) {
|
|
console.warn('Failed to persist detail panel width', error);
|
|
}
|
|
}, [detailPanelWidth, isSidebarVariant]);
|
|
|
|
const viewerClassName = isStackedLayout
|
|
? 'document-viewer document-viewer--stacked'
|
|
: 'document-viewer';
|
|
|
|
const actionState = useMemo(
|
|
() =>
|
|
document
|
|
? createDocumentActionState({
|
|
document,
|
|
resolveApiPath,
|
|
ensurePreviewData,
|
|
ensureAssetUrl,
|
|
getDocumentAsset,
|
|
notifyApiError,
|
|
ocrErrorMessage: 'Unable to open OCR text.',
|
|
})
|
|
: null,
|
|
[
|
|
document,
|
|
resolveApiPath,
|
|
ensurePreviewData,
|
|
ensureAssetUrl,
|
|
getDocumentAsset,
|
|
notifyApiError,
|
|
],
|
|
);
|
|
|
|
const breadcrumbs = useMemo(() => {
|
|
if (!document || typeof resolveFolderPath !== 'function') {
|
|
return [];
|
|
}
|
|
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 }))
|
|
: [];
|
|
|
|
return [
|
|
...normalizedSegments,
|
|
{ id: document.id, name: document.title },
|
|
];
|
|
}, [document, resolveFolderPath]);
|
|
|
|
const handleBreadcrumbNavigate = useCallback(
|
|
(crumb) => {
|
|
if (!crumb?.id) {
|
|
return;
|
|
}
|
|
const target = crumb.id === 'root'
|
|
? '/documents'
|
|
: `/documents/folder/${crumb.id}`;
|
|
navigate(target);
|
|
},
|
|
[navigate],
|
|
);
|
|
|
|
const breadcrumbTrailEntries = useMemo(() => {
|
|
if (!breadcrumbs.length) {
|
|
return [];
|
|
}
|
|
const lastIndex = breadcrumbs.length - 1;
|
|
return breadcrumbs.map((crumb, index) => ({
|
|
id: crumb.id,
|
|
label: crumb.name,
|
|
onClick: index < lastIndex ? () => handleBreadcrumbNavigate(crumb) : null,
|
|
}));
|
|
}, [breadcrumbs, handleBreadcrumbNavigate]);
|
|
|
|
const zoomDisplay = useMemo(() => {
|
|
if (navigatorUrl && document) {
|
|
return {
|
|
url: navigatorUrl,
|
|
alt: document.title,
|
|
canGoPrev: navigatorCanGoPrev,
|
|
canGoNext: navigatorCanGoNext,
|
|
goPrev: navigatorCanGoPrev ? navigatorGoPrev : undefined,
|
|
goNext: navigatorCanGoNext ? navigatorGoNext : undefined,
|
|
};
|
|
}
|
|
if (previewEntry?.url && document) {
|
|
return {
|
|
url: previewEntry.url,
|
|
alt: document.title,
|
|
canGoPrev: false,
|
|
canGoNext: false,
|
|
};
|
|
}
|
|
return null;
|
|
}, [
|
|
navigatorUrl,
|
|
navigatorCanGoPrev,
|
|
navigatorCanGoNext,
|
|
navigatorGoPrev,
|
|
navigatorGoNext,
|
|
previewEntry?.url,
|
|
document,
|
|
]);
|
|
|
|
const headerActions = createDocumentViewerHeaderActions({
|
|
document,
|
|
actionState,
|
|
previewEntry,
|
|
onZoom: zoomDisplay ? handleZoomOpen : null,
|
|
canZoom: Boolean(zoomDisplay),
|
|
});
|
|
|
|
const collapseButton = isSidebarVariant && typeof onCollapsePanel === 'function'
|
|
? (
|
|
<button
|
|
type="button"
|
|
className="icon-button"
|
|
onClick={() => onCollapsePanel?.()}
|
|
aria-label="Close detail panel"
|
|
title="Close detail panel"
|
|
>
|
|
<DetailPanelCollapseIcon />
|
|
</button>
|
|
)
|
|
: null;
|
|
|
|
const maximizeButton = isSidebarVariant && typeof onMaximizePanel === 'function'
|
|
? (
|
|
<button
|
|
type="button"
|
|
className="icon-button"
|
|
onClick={(event) => {
|
|
event.stopPropagation();
|
|
onMaximizePanel?.(document?.id);
|
|
}}
|
|
aria-label="Maximize"
|
|
title="Maximize"
|
|
>
|
|
<WindowMaximizeIcon className="icon--flip-y" />
|
|
</button>
|
|
)
|
|
: null;
|
|
|
|
const closeButton = !isSidebarVariant && onClosePanel
|
|
? (
|
|
<button
|
|
type="button"
|
|
className="icon-button"
|
|
onClick={() => onClosePanel?.()}
|
|
aria-label="Close preview"
|
|
title="Close preview"
|
|
>
|
|
<CloseIcon />
|
|
</button>
|
|
)
|
|
: null;
|
|
|
|
const headerLeadingButtons = isSidebarVariant
|
|
? [collapseButton, maximizeButton].filter(Boolean)
|
|
: [sidebarToggle, closeButton].filter(Boolean);
|
|
const headerLeadingContent = headerLeadingButtons.length
|
|
? (
|
|
<>
|
|
{headerLeadingButtons}
|
|
</>
|
|
)
|
|
: null;
|
|
|
|
const resizeHandle = isSidebarVariant ? (
|
|
<button
|
|
type="button"
|
|
className={`detail-panel__resize-handle${isResizingPanel ? ' is-active' : ''}`}
|
|
aria-label="Resize detail panel"
|
|
onPointerDown={handleResizePointerDown}
|
|
onKeyDown={handleResizeKeyDown}
|
|
>
|
|
<span aria-hidden="true" />
|
|
</button>
|
|
) : null;
|
|
|
|
const loadingSection = (
|
|
<div className="document-viewer-panel__body">
|
|
<section className="document-viewer document-viewer--loading">
|
|
<div className="document-viewer__details-pane">
|
|
<div className="document-viewer__details">
|
|
<div className="document-viewer__message">Loading document…</div>
|
|
</div>
|
|
</div>
|
|
<div className="document-viewer__viewport">
|
|
<div className="document-viewer__message">Preparing preview…</div>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
);
|
|
|
|
const viewerSection = document ? (
|
|
<div
|
|
className={isStackedLayout
|
|
? 'document-viewer-panel__body document-viewer-panel__body--stacked'
|
|
: 'document-viewer-panel__body'}
|
|
>
|
|
<section className={viewerClassName}>
|
|
<DocumentViewerLayout
|
|
document={document}
|
|
previewEntry={previewEntry}
|
|
summaryProps={summaryProps}
|
|
metadataPayload={metadataPayload}
|
|
contentTabConfig={contentTabConfig}
|
|
previewLoadingMessage="Loading preview…"
|
|
layoutMode={isStackedLayout ? 'stacked' : 'split'}
|
|
/>
|
|
</section>
|
|
</div>
|
|
) : loadingSection;
|
|
|
|
const headerTitle = breadcrumbTrailEntries.length ? (
|
|
<BreadcrumbTrail
|
|
entries={breadcrumbTrailEntries}
|
|
separator="/"
|
|
className="panel-header__breadcrumbs"
|
|
truncateFromStart={isSidebarVariant}
|
|
/>
|
|
) : (
|
|
document?.title || 'Document preview'
|
|
);
|
|
|
|
const overlay = (
|
|
<PreviewZoomOverlay
|
|
open={Boolean(zoomOverlayOpen && zoomDisplay)}
|
|
display={zoomDisplay}
|
|
onClose={handleZoomClose}
|
|
/>
|
|
);
|
|
|
|
if (isSidebarVariant) {
|
|
return (
|
|
<>
|
|
<aside className={`detail-panel panel${isResizingPanel ? ' detail-panel--resizing' : ''}`} ref={panelRef}>
|
|
{resizeHandle}
|
|
<PanelHeader
|
|
leading={headerLeadingContent}
|
|
title={headerTitle}
|
|
titleTag="h3"
|
|
actions={headerActions}
|
|
/>
|
|
<div className="panel-body detail-panel__content">{viewerSection}</div>
|
|
</aside>
|
|
{overlay}
|
|
</>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<section className="document-viewer-panel" ref={panelRef}>
|
|
<PanelHeader
|
|
leading={headerLeadingContent}
|
|
title={headerTitle}
|
|
titleTag="h3"
|
|
actions={headerActions}
|
|
/>
|
|
{viewerSection}
|
|
</section>
|
|
{overlay}
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default DocumentViewerPanel;
|
|
|
|
export const createDocumentViewerSurface = ({
|
|
document,
|
|
previewEntry,
|
|
ensureAssetUrl,
|
|
ensurePreviewData,
|
|
getDocumentAsset,
|
|
resolveApiPath,
|
|
notifyApiError,
|
|
onClose,
|
|
renderSidebarToggle,
|
|
tagLookupById,
|
|
tagOptions,
|
|
onTagAdd,
|
|
onTagRemove,
|
|
correspondents,
|
|
onCorrespondentAdd,
|
|
onCorrespondentRemove,
|
|
onUpdateTitle,
|
|
onUpdateIssued,
|
|
resolveFolderPath,
|
|
}) => {
|
|
if (!document) {
|
|
return null;
|
|
}
|
|
|
|
const sidebarToggle = typeof renderSidebarToggle === 'function'
|
|
? renderSidebarToggle()
|
|
: null;
|
|
|
|
return {
|
|
key: 'preview',
|
|
variant: 'preview',
|
|
header: null,
|
|
content: (
|
|
<DocumentViewerPanel
|
|
document={document}
|
|
previewEntry={previewEntry}
|
|
hydrateDocument={ensurePreviewData}
|
|
tagLookupById={tagLookupById}
|
|
tagOptions={tagOptions}
|
|
onTagAdd={onTagAdd}
|
|
onTagRemove={onTagRemove}
|
|
correspondents={correspondents}
|
|
onCorrespondentAdd={onCorrespondentAdd}
|
|
onCorrespondentRemove={onCorrespondentRemove}
|
|
onUpdateTitle={onUpdateTitle}
|
|
onUpdateIssued={onUpdateIssued}
|
|
ensureAssetUrl={ensureAssetUrl}
|
|
getDocumentAsset={getDocumentAsset}
|
|
ensurePreviewData={ensurePreviewData}
|
|
resolveApiPath={resolveApiPath}
|
|
notifyApiError={notifyApiError}
|
|
sidebarToggle={sidebarToggle}
|
|
onClosePanel={onClose}
|
|
resolveFolderPath={resolveFolderPath}
|
|
/>
|
|
),
|
|
supportsDetail: false,
|
|
};
|
|
};
|