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