guts
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
DownloadIcon,
|
||||
DetailPanelCollapseIcon,
|
||||
WindowMaximizeIcon,
|
||||
IconZoomInArea,
|
||||
} from '../ui/icons';
|
||||
import PanelHeader from '../ui/PanelHeader';
|
||||
import { resolveDocumentAssetUrl } from '../asset_manager';
|
||||
@@ -12,6 +13,8 @@ import { sortCorrespondents, buildCorrespondentOptions } from '../documents/Docu
|
||||
import BreadcrumbTrail from '../ui/BreadcrumbTrail';
|
||||
import { extractDocumentMetadataPayload } from '../documents/documentMetadata';
|
||||
import DocumentViewerLayout from '../preview/DocumentViewerLayout';
|
||||
import PreviewZoomOverlay from './PreviewZoomOverlay';
|
||||
import { useAssetNavigator } from '../hooks/useAssetNavigator';
|
||||
|
||||
const DetailPanel = ({
|
||||
document = null,
|
||||
@@ -35,6 +38,19 @@ const DetailPanel = ({
|
||||
onClose = () => {},
|
||||
}) => {
|
||||
const singleDoc = document || null;
|
||||
const [zoomOverlayOpen, setZoomOverlayOpen] = useState(false);
|
||||
const previewNavigator = useAssetNavigator({
|
||||
document: singleDoc,
|
||||
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 { downloadHref: singleDownloadHref } = useMemo(
|
||||
() =>
|
||||
@@ -199,15 +215,19 @@ const DetailPanel = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="document-viewer document-viewer--stacked">
|
||||
<DocumentViewerLayout
|
||||
document={singleDoc}
|
||||
previewEntry={previewEntry}
|
||||
summaryProps={singleSummaryProps}
|
||||
metadataPayload={singleMetadataPayload}
|
||||
contentTabConfig={singleContentConfig}
|
||||
previewLoadingMessage="Loading preview…"
|
||||
/>
|
||||
<section className="document-viewer-panel document-viewer-panel--stacked">
|
||||
<div className="document-viewer-panel__body">
|
||||
<section className="document-viewer document-viewer--stacked">
|
||||
<DocumentViewerLayout
|
||||
document={singleDoc}
|
||||
previewEntry={previewEntry}
|
||||
summaryProps={singleSummaryProps}
|
||||
metadataPayload={singleMetadataPayload}
|
||||
contentTabConfig={singleContentConfig}
|
||||
previewLoadingMessage="Loading preview…"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
@@ -264,27 +284,83 @@ const DetailPanel = ({
|
||||
);
|
||||
}
|
||||
|
||||
const zoomDisplay = useMemo(() => {
|
||||
if (navigatorUrl && singleDoc) {
|
||||
return {
|
||||
url: navigatorUrl,
|
||||
alt: singleDoc.title,
|
||||
canGoPrev: navigatorCanGoPrev,
|
||||
canGoNext: navigatorCanGoNext,
|
||||
goPrev: navigatorCanGoPrev ? navigatorGoPrev : undefined,
|
||||
goNext: navigatorCanGoNext ? navigatorGoNext : undefined,
|
||||
};
|
||||
}
|
||||
if (singleDoc && previewEntry?.url) {
|
||||
return {
|
||||
url: previewEntry.url,
|
||||
alt: singleDoc.title,
|
||||
canGoPrev: false,
|
||||
canGoNext: false,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}, [
|
||||
navigatorUrl,
|
||||
navigatorCanGoPrev,
|
||||
navigatorCanGoNext,
|
||||
navigatorGoPrev,
|
||||
navigatorGoNext,
|
||||
singleDoc,
|
||||
previewEntry?.url,
|
||||
]);
|
||||
|
||||
if (singleDoc && zoomDisplay) {
|
||||
headerActions.push(
|
||||
<button
|
||||
key="zoom"
|
||||
type="button"
|
||||
className="icon-button"
|
||||
onClick={() => setZoomOverlayOpen(true)}
|
||||
aria-label="Open zoom preview"
|
||||
title="Open zoom preview"
|
||||
>
|
||||
<IconZoomInArea />
|
||||
</button>,
|
||||
);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setZoomOverlayOpen(false);
|
||||
}, [previewEntry?.url, singleDoc?.id]);
|
||||
|
||||
return (
|
||||
<aside className="detail-panel panel">
|
||||
<PanelHeader
|
||||
leading={headerLeading}
|
||||
title={
|
||||
headerBreadcrumbs ? (
|
||||
<BreadcrumbTrail
|
||||
entries={headerBreadcrumbs}
|
||||
separator="/"
|
||||
className="panel-header__breadcrumbs"
|
||||
truncateFromStart
|
||||
/>
|
||||
) : (
|
||||
headerTitle
|
||||
)
|
||||
}
|
||||
titleTag="h3"
|
||||
actions={headerActions.length ? headerActions : null}
|
||||
<>
|
||||
<aside className="detail-panel panel">
|
||||
<PanelHeader
|
||||
leading={headerLeading}
|
||||
title={
|
||||
headerBreadcrumbs ? (
|
||||
<BreadcrumbTrail
|
||||
entries={headerBreadcrumbs}
|
||||
separator="/"
|
||||
className="panel-header__breadcrumbs"
|
||||
truncateFromStart
|
||||
/>
|
||||
) : (
|
||||
headerTitle
|
||||
)
|
||||
}
|
||||
titleTag="h3"
|
||||
actions={headerActions.length ? headerActions : null}
|
||||
/>
|
||||
<div className="panel-body detail-panel__content">{renderContent()}</div>
|
||||
</aside>
|
||||
<PreviewZoomOverlay
|
||||
open={Boolean(zoomOverlayOpen && zoomDisplay)}
|
||||
display={zoomDisplay}
|
||||
onClose={() => setZoomOverlayOpen(false)}
|
||||
/>
|
||||
<div className="panel-body">{renderContent()}</div>
|
||||
</aside>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
const useDetailWorkspace = ({
|
||||
documents,
|
||||
searchResults,
|
||||
focusedDocumentId,
|
||||
selectionOrder,
|
||||
selectedDocumentIds,
|
||||
documentLookup,
|
||||
@@ -37,14 +36,6 @@ const useDetailWorkspace = ({
|
||||
tags,
|
||||
tagLookupById,
|
||||
}) => {
|
||||
const selectedDocument = useMemo(() => {
|
||||
if (!focusedDocumentId) {
|
||||
return null;
|
||||
}
|
||||
const pool = searchResults ?? documents;
|
||||
return pool.find((doc) => doc.id === focusedDocumentId) || null;
|
||||
}, [focusedDocumentId, searchResults, documents]);
|
||||
|
||||
const orderedSelectedDocuments = useMemo(() => {
|
||||
const ordered = [];
|
||||
const seen = new Set();
|
||||
@@ -185,12 +176,12 @@ const useDetailWorkspace = ({
|
||||
[folderNodes],
|
||||
);
|
||||
|
||||
const selectedPreviewEntry = useMemo(() => {
|
||||
if (!selectedDocument) {
|
||||
const detailPanelPreviewEntry = useMemo(() => {
|
||||
if (!detailPanelDocument) {
|
||||
return null;
|
||||
}
|
||||
return previewEntries.get(selectedDocument.id) || null;
|
||||
}, [selectedDocument, previewEntries]);
|
||||
return previewEntries.get(detailPanelDocument.id) || null;
|
||||
}, [detailPanelDocument, previewEntries]);
|
||||
|
||||
const previewWorkspaceEntry = useMemo(() => {
|
||||
if (!previewDocumentId) {
|
||||
@@ -239,7 +230,7 @@ const useDetailWorkspace = ({
|
||||
tagLookupById,
|
||||
onTagAdd: handleDocumentTagAdd,
|
||||
onTagRemove: handleTagRemove,
|
||||
previewEntry: selectedPreviewEntry,
|
||||
previewEntry: detailPanelPreviewEntry,
|
||||
onOpenPreview: openDocumentPreview,
|
||||
activePreviewId,
|
||||
onUpdateTitle: handleDocumentTitleUpdate,
|
||||
@@ -273,7 +264,7 @@ const useDetailWorkspace = ({
|
||||
resolveApiPath,
|
||||
resolveFolderPath,
|
||||
selectFolder,
|
||||
selectedPreviewEntry,
|
||||
detailPanelPreviewEntry,
|
||||
tags,
|
||||
tagLookupById,
|
||||
],
|
||||
|
||||
@@ -6,7 +6,9 @@ import React, {
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { DownloadIcon, CloseIcon } from '../ui/icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { DownloadIcon, CloseIcon, IconZoomInArea } from '../ui/icons';
|
||||
import PreviewZoomOverlay from '../detail/PreviewZoomOverlay';
|
||||
import {
|
||||
buildCorrespondentOptions,
|
||||
sortCorrespondents,
|
||||
@@ -14,10 +16,13 @@ import {
|
||||
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';
|
||||
|
||||
const PORTRAIT_WIDTH_TO_HEIGHT = 1 / Math.sqrt(2); // ≈0.707 (A-series aspect ratio)
|
||||
const DOCUMENT_VIEWER_PORTRAIT_HEIGHT_RATIO = 0.6;
|
||||
const DOCUMENT_VIEWER_PORTRAIT_HEIGHT_RATIO = 0.4;
|
||||
const PORTRAIT_RATIO_STYLE_ID = 'document-viewer-portrait-ratio-style';
|
||||
|
||||
const ensurePortraitRatioStyle = () => {
|
||||
@@ -56,6 +61,51 @@ const computeStackedLayoutBreakpoint = () => {
|
||||
return desiredWidth;
|
||||
};
|
||||
|
||||
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,
|
||||
@@ -72,7 +122,14 @@ const DocumentViewerPanel = ({
|
||||
hasOcr = false,
|
||||
ensureAssetUrl,
|
||||
getDocumentAsset,
|
||||
ensurePreviewData,
|
||||
resolveApiPath,
|
||||
notifyApiError,
|
||||
sidebarToggle = null,
|
||||
onClosePanel,
|
||||
resolveFolderPath,
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
const sortedCorrespondents = useMemo(
|
||||
() => sortCorrespondents(document?.correspondents || []),
|
||||
[document],
|
||||
@@ -171,6 +228,34 @@ const DocumentViewerPanel = ({
|
||||
|
||||
const viewerRef = useRef(null);
|
||||
const [isStackedLayout, setIsStackedLayout] = useState(false);
|
||||
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) {
|
||||
@@ -237,71 +322,197 @@ const DocumentViewerPanel = ({
|
||||
? '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 closeButton = onClosePanel
|
||||
? (
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button"
|
||||
onClick={() => onClosePanel?.()}
|
||||
aria-label="Close preview"
|
||||
title="Close preview"
|
||||
>
|
||||
<CloseIcon />
|
||||
</button>
|
||||
)
|
||||
: null;
|
||||
|
||||
const headerLeadingButtons = [sidebarToggle, closeButton].filter(Boolean);
|
||||
const headerLeadingContent = headerLeadingButtons.length
|
||||
? (
|
||||
<>
|
||||
{headerLeadingButtons}
|
||||
</>
|
||||
)
|
||||
: null;
|
||||
|
||||
if (!document) {
|
||||
return (
|
||||
<section className="document-viewer document-viewer--loading" ref={viewerRef}>
|
||||
<div className="document-viewer__details-pane">
|
||||
<div className="document-viewer__details">
|
||||
<div className="document-viewer__message">
|
||||
Loading document…
|
||||
<section className="document-viewer-panel">
|
||||
<PanelHeader title="Document preview" />
|
||||
<div className="document-viewer-panel__body">
|
||||
<section className="document-viewer document-viewer--loading" ref={viewerRef}>
|
||||
<div className="document-viewer__details-pane">
|
||||
<div className="document-viewer__details">
|
||||
<div className="document-viewer__message">
|
||||
Loading document…
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="document-viewer__viewport">
|
||||
<div className="document-viewer__message">Preparing preview…</div>
|
||||
<div className="document-viewer__viewport">
|
||||
<div className="document-viewer__message">Preparing preview…</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
const headerTitle = breadcrumbTrailEntries.length ? (
|
||||
<BreadcrumbTrail
|
||||
entries={breadcrumbTrailEntries}
|
||||
separator="/"
|
||||
className="panel-header__breadcrumbs"
|
||||
/>
|
||||
) : (
|
||||
document?.title || 'Document preview'
|
||||
);
|
||||
|
||||
return (
|
||||
<section className={viewerClassName} ref={viewerRef}>
|
||||
<DocumentViewerLayout
|
||||
document={document}
|
||||
previewEntry={previewEntry}
|
||||
summaryProps={summaryProps}
|
||||
metadataPayload={metadataPayload}
|
||||
contentTabConfig={contentTabConfig}
|
||||
previewLoadingMessage="Loading preview…"
|
||||
<>
|
||||
<section className="document-viewer-panel">
|
||||
<PanelHeader
|
||||
leading={headerLeadingContent}
|
||||
title={headerTitle}
|
||||
titleTag="h3"
|
||||
actions={headerActions}
|
||||
/>
|
||||
<div className="document-viewer-panel__body">
|
||||
<section className={viewerClassName} ref={viewerRef}>
|
||||
<DocumentViewerLayout
|
||||
document={document}
|
||||
previewEntry={previewEntry}
|
||||
summaryProps={summaryProps}
|
||||
metadataPayload={metadataPayload}
|
||||
contentTabConfig={contentTabConfig}
|
||||
previewLoadingMessage="Loading preview…"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
<PreviewZoomOverlay
|
||||
open={Boolean(zoomOverlayOpen && zoomDisplay)}
|
||||
display={zoomDisplay}
|
||||
onClose={handleZoomClose}
|
||||
/>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DocumentViewerPanel;
|
||||
|
||||
export const createDocumentViewerHeaderActions = ({
|
||||
document,
|
||||
actionState,
|
||||
previewEntry,
|
||||
}) => {
|
||||
if (!document) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const downloadHref = actionState?.downloadHref || previewEntry?.url;
|
||||
if (!downloadHref) {
|
||||
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}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const createDocumentViewerSurface = ({
|
||||
document,
|
||||
previewEntry,
|
||||
@@ -327,70 +538,18 @@ export const createDocumentViewerSurface = ({
|
||||
return null;
|
||||
}
|
||||
|
||||
const title = document.title || 'Document preview';
|
||||
const closeButton = onClose
|
||||
? (
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button"
|
||||
onClick={() => onClose?.()}
|
||||
aria-label="Close preview"
|
||||
title="Close preview"
|
||||
>
|
||||
<CloseIcon />
|
||||
</button>
|
||||
)
|
||||
const hasOcr = typeof getDocumentAsset === 'function'
|
||||
? Boolean(getDocumentAsset(document, 'ocr-text'))
|
||||
: false;
|
||||
|
||||
const sidebarToggle = typeof renderSidebarToggle === 'function'
|
||||
? renderSidebarToggle()
|
||||
: null;
|
||||
const sidebarToggle = renderSidebarToggle ? renderSidebarToggle() : null;
|
||||
const leading = closeButton || sidebarToggle
|
||||
? (
|
||||
<>
|
||||
{sidebarToggle}
|
||||
{closeButton}
|
||||
</>
|
||||
)
|
||||
: null;
|
||||
let breadcrumbs = null;
|
||||
if (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 }))
|
||||
: [];
|
||||
|
||||
breadcrumbs = [
|
||||
...normalizedSegments,
|
||||
{ id: document.id, name: document.title },
|
||||
];
|
||||
}
|
||||
|
||||
const actionState = createDocumentActionState({
|
||||
document,
|
||||
resolveApiPath,
|
||||
ensurePreviewData,
|
||||
ensureAssetUrl,
|
||||
getDocumentAsset,
|
||||
notifyApiError,
|
||||
ocrErrorMessage: 'Unable to open OCR text.',
|
||||
});
|
||||
|
||||
const header = {
|
||||
title,
|
||||
subtitle: null,
|
||||
leading,
|
||||
actions: createDocumentViewerHeaderActions({
|
||||
document,
|
||||
actionState,
|
||||
previewEntry,
|
||||
}),
|
||||
breadcrumbs,
|
||||
};
|
||||
|
||||
return {
|
||||
key: 'preview',
|
||||
variant: 'preview',
|
||||
header,
|
||||
header: null,
|
||||
content: (
|
||||
<DocumentViewerPanel
|
||||
document={document}
|
||||
@@ -405,9 +564,15 @@ export const createDocumentViewerSurface = ({
|
||||
onCorrespondentRemove={onCorrespondentRemove}
|
||||
onUpdateTitle={onUpdateTitle}
|
||||
onUpdateIssued={onUpdateIssued}
|
||||
hasOcr={Boolean(actionState?.hasOcr)}
|
||||
hasOcr={hasOcr}
|
||||
ensureAssetUrl={ensureAssetUrl}
|
||||
getDocumentAsset={getDocumentAsset}
|
||||
ensurePreviewData={ensurePreviewData}
|
||||
resolveApiPath={resolveApiPath}
|
||||
notifyApiError={notifyApiError}
|
||||
sidebarToggle={sidebarToggle}
|
||||
onClosePanel={onClose}
|
||||
resolveFolderPath={resolveFolderPath}
|
||||
/>
|
||||
),
|
||||
supportsDetail: false,
|
||||
|
||||
@@ -60,11 +60,24 @@
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.detail-panel__content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.detail-panel__content .document-viewer-panel {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.detail-panel__content .document-viewer-panel__body {
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.detail-section__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,3 +1,24 @@
|
||||
.document-viewer-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
.document-viewer-panel__body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
padding: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.document-viewer-panel__body > .document-viewer,
|
||||
.document-viewer-panel__body > .document-viewer--loading {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.document-viewer {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
@@ -10,14 +31,8 @@
|
||||
}
|
||||
|
||||
.document-viewer--stacked {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
grid-template-rows:
|
||||
minmax(auto, 1fr)
|
||||
auto;
|
||||
grid-template-areas:
|
||||
'viewport'
|
||||
'details';
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
overflow: auto;
|
||||
@@ -25,16 +40,19 @@
|
||||
|
||||
.document-viewer--stacked .document-viewer__viewport {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex: 2 2 auto;
|
||||
max-height: calc(var(--document-viewer-portrait-height-ratio) * 100vh);
|
||||
order: -1;
|
||||
}
|
||||
|
||||
.document-viewer--stacked .document-viewer__details-pane {
|
||||
overflow: visible;
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
order: 0;
|
||||
}
|
||||
|
||||
.document-drag-preview {
|
||||
@@ -322,6 +340,7 @@
|
||||
overflow: hidden;
|
||||
align-items: stretch;
|
||||
justify-content: center;
|
||||
max-height: 100%;
|
||||
grid-area: viewport;
|
||||
}
|
||||
|
||||
@@ -334,9 +353,6 @@
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.document-viewer__object--image {
|
||||
width: auto;
|
||||
height: auto;
|
||||
@@ -345,6 +361,15 @@
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.document-viewer--stacked .document-viewer__object:not(.document-viewer__object--image) {
|
||||
height: calc(var(--document-viewer-portrait-height-ratio) * 100vh);
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.document-viewer--stacked .document-viewer__object--image {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.document-viewer__unsupported {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
IconChevronRight as TablerChevronRight,
|
||||
IconDownload as TablerDownload,
|
||||
IconZoomInArea as TablerZoomInArea,
|
||||
IconPencil,
|
||||
IconTagFilled,
|
||||
IconUserFilled,
|
||||
@@ -118,6 +119,15 @@ export const DownloadIcon = ({ className, size = '1em', stroke = 1.6, ...rest })
|
||||
/>
|
||||
);
|
||||
|
||||
export const IconZoomInArea = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
<TablerZoomInArea
|
||||
className={composeClassName('icon', className)}
|
||||
size={size}
|
||||
stroke={stroke}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
|
||||
export const ViewListIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
|
||||
<IconLayoutList
|
||||
className={composeClassName('icon', className)}
|
||||
|
||||
Reference in New Issue
Block a user