hydration
This commit is contained in:
@@ -1,123 +1,17 @@
|
|||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
DownloadIcon,
|
DownloadIcon,
|
||||||
ArrowLeftIcon,
|
|
||||||
ArrowRightIcon,
|
|
||||||
DetailPanelCollapseIcon,
|
DetailPanelCollapseIcon,
|
||||||
WindowMaximizeIcon,
|
WindowMaximizeIcon,
|
||||||
} from '../ui/icons';
|
} from '../ui/icons';
|
||||||
import PanelHeader from '../ui/PanelHeader';
|
import PanelHeader from '../ui/PanelHeader';
|
||||||
import { resolveDocumentAssetUrl, createAssetView } from '../asset_manager';
|
import { resolveDocumentAssetUrl } from '../asset_manager';
|
||||||
import { useAssetNavigator } from '../hooks/useAssetNavigator';
|
|
||||||
import { describeDocumentSummary } from '../documents/documentSummary';
|
import { describeDocumentSummary } from '../documents/documentSummary';
|
||||||
import { createDocumentActionState } from '../documents/documentActions';
|
import { createDocumentActionState } from '../documents/documentActions';
|
||||||
import PreviewZoomOverlay from './PreviewZoomOverlay';
|
|
||||||
import DocumentInfoPanel from '../documents/DocumentInfoPanel';
|
|
||||||
import { sortCorrespondents, buildCorrespondentOptions } from '../documents/DocumentSummarySection';
|
import { sortCorrespondents, buildCorrespondentOptions } from '../documents/DocumentSummarySection';
|
||||||
import BreadcrumbTrail from '../ui/BreadcrumbTrail';
|
import BreadcrumbTrail from '../ui/BreadcrumbTrail';
|
||||||
|
import { extractDocumentMetadataPayload } from '../documents/documentMetadata';
|
||||||
const derivePreviewOrientation = (metadata) => {
|
import DocumentViewerLayout from '../preview/DocumentViewerLayout';
|
||||||
const width = Number(metadata?.width);
|
|
||||||
const height = Number(metadata?.height);
|
|
||||||
if (Number.isFinite(width) && Number.isFinite(height) && width > 0 && height > 0) {
|
|
||||||
return width >= height ? 'landscape' : 'portrait';
|
|
||||||
}
|
|
||||||
return 'landscape';
|
|
||||||
};
|
|
||||||
|
|
||||||
const PreviewImage = ({
|
|
||||||
item,
|
|
||||||
emptyMessage = 'Preview unavailable',
|
|
||||||
emptyContent = null,
|
|
||||||
onActivate,
|
|
||||||
onOpenPreview,
|
|
||||||
onZoomPreview,
|
|
||||||
showNav = false,
|
|
||||||
canGoPrev = false,
|
|
||||||
canGoNext = false,
|
|
||||||
onGoPrev = null,
|
|
||||||
onGoNext = null,
|
|
||||||
}) => {
|
|
||||||
if (!item) {
|
|
||||||
return (
|
|
||||||
<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-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?.();
|
|
||||||
}}
|
|
||||||
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();
|
|
||||||
onGoNext?.();
|
|
||||||
}}
|
|
||||||
onPointerDown={interceptNavPointer}
|
|
||||||
onPointerUp={interceptNavPointer}
|
|
||||||
onMouseDown={interceptNavPointer}
|
|
||||||
onMouseUp={interceptNavPointer}
|
|
||||||
disabled={!canGoNext}
|
|
||||||
aria-label="Next preview"
|
|
||||||
>
|
|
||||||
<ArrowRightIcon />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const DetailPanel = ({
|
const DetailPanel = ({
|
||||||
document = null,
|
document = null,
|
||||||
@@ -126,7 +20,6 @@ const DetailPanel = ({
|
|||||||
onTagAdd,
|
onTagAdd,
|
||||||
onTagRemove,
|
onTagRemove,
|
||||||
onOpenPreview,
|
onOpenPreview,
|
||||||
onPromoteSelection,
|
|
||||||
onUpdateTitle = async () => false,
|
onUpdateTitle = async () => false,
|
||||||
onUpdateIssued = async () => false,
|
onUpdateIssued = async () => false,
|
||||||
ensureAssetUrl = null,
|
ensureAssetUrl = null,
|
||||||
@@ -138,11 +31,10 @@ const DetailPanel = ({
|
|||||||
resolveApiPath,
|
resolveApiPath,
|
||||||
onFolderNavigate = null,
|
onFolderNavigate = null,
|
||||||
resolveFolderPath = null,
|
resolveFolderPath = null,
|
||||||
|
previewEntry = null,
|
||||||
onClose = () => {},
|
onClose = () => {},
|
||||||
}) => {
|
}) => {
|
||||||
const singleDoc = document || null;
|
const singleDoc = document || null;
|
||||||
const singleDocId = singleDoc?.id || null;
|
|
||||||
const selectionKey = singleDocId || 'none';
|
|
||||||
|
|
||||||
const { downloadHref: singleDownloadHref } = useMemo(
|
const { downloadHref: singleDownloadHref } = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -192,81 +84,6 @@ const DetailPanel = ({
|
|||||||
];
|
];
|
||||||
}, [singleDoc, resolveFolderPath, detailSummary, onFolderNavigate]);
|
}, [singleDoc, resolveFolderPath, detailSummary, onFolderNavigate]);
|
||||||
|
|
||||||
const [zoomedPreview, setZoomedPreview] = useState(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setZoomedPreview(null);
|
|
||||||
}, [selectionKey]);
|
|
||||||
|
|
||||||
const handlePreviewActivate = useCallback(
|
|
||||||
(docId) => {
|
|
||||||
if (!docId) return;
|
|
||||||
onPromoteSelection?.(docId);
|
|
||||||
},
|
|
||||||
[onPromoteSelection],
|
|
||||||
);
|
|
||||||
|
|
||||||
const singlePreviewNavigator = useAssetNavigator({
|
|
||||||
document: singleDoc,
|
|
||||||
assetType: 'preview',
|
|
||||||
ensureAssetUrl,
|
|
||||||
getAsset: getDocumentAsset,
|
|
||||||
prefetch: 3,
|
|
||||||
});
|
|
||||||
|
|
||||||
const makePreviewItem = useCallback(
|
|
||||||
(doc, ordinal = 1) => {
|
|
||||||
if (!doc) return null;
|
|
||||||
const asset = getDocumentAsset(doc, 'preview');
|
|
||||||
const assetView = createAssetView(asset);
|
|
||||||
const object = assetView.getObject(ordinal);
|
|
||||||
let url = object?.url || null;
|
|
||||||
if (!url) {
|
|
||||||
url = resolveDocumentAssetUrl(doc, 'preview', {
|
|
||||||
ensureAssetUrl,
|
|
||||||
getAsset: getDocumentAsset,
|
|
||||||
ensureOptions: { start: ordinal, limit: 1 },
|
|
||||||
objectOrdinal: ordinal,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (!url) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const metadata = object?.metadata || assetView.getPrimaryMetadata() || {};
|
|
||||||
const orientation = derivePreviewOrientation(metadata);
|
|
||||||
return {
|
|
||||||
id: doc.id,
|
|
||||||
url,
|
|
||||||
orientation,
|
|
||||||
alt: doc.title,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
[ensureAssetUrl, getDocumentAsset],
|
|
||||||
);
|
|
||||||
|
|
||||||
const singlePreviewItem = useMemo(() => {
|
|
||||||
if (!singleDoc) return null;
|
|
||||||
const url = singlePreviewNavigator.currentUrl;
|
|
||||||
if (url) {
|
|
||||||
return {
|
|
||||||
id: singleDoc.id,
|
|
||||||
url,
|
|
||||||
orientation: derivePreviewOrientation(singlePreviewNavigator.currentMetadata),
|
|
||||||
alt: singleDoc.title,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return makePreviewItem(singleDoc, 1);
|
|
||||||
}, [
|
|
||||||
singleDoc,
|
|
||||||
singlePreviewNavigator.currentUrl,
|
|
||||||
singlePreviewNavigator.currentMetadata,
|
|
||||||
makePreviewItem,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const singleCardinality = singlePreviewNavigator.cardinality;
|
|
||||||
const singleEffectiveCardinality = singleCardinality || (singlePreviewNavigator.currentUrl ? 1 : 0);
|
|
||||||
const singleHasPreview = Boolean(singlePreviewNavigator.currentUrl);
|
|
||||||
|
|
||||||
const correspondentOptions = useMemo(
|
const correspondentOptions = useMemo(
|
||||||
() => buildCorrespondentOptions(Array.isArray(correspondents) ? correspondents : []),
|
() => buildCorrespondentOptions(Array.isArray(correspondents) ? correspondents : []),
|
||||||
[correspondents],
|
[correspondents],
|
||||||
@@ -277,6 +94,12 @@ const DetailPanel = ({
|
|||||||
return sortCorrespondents(singleDoc.correspondents || []);
|
return sortCorrespondents(singleDoc.correspondents || []);
|
||||||
}, [singleDoc]);
|
}, [singleDoc]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof ensurePreviewData === 'function' && singleDoc?.id) {
|
||||||
|
ensurePreviewData(singleDoc.id);
|
||||||
|
}
|
||||||
|
}, [ensurePreviewData, singleDoc?.id]);
|
||||||
|
|
||||||
const singleSummaryProps = useMemo(
|
const singleSummaryProps = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
tagLookupById,
|
tagLookupById,
|
||||||
@@ -304,6 +127,11 @@ const DetailPanel = ({
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const singleMetadataPayload = useMemo(
|
||||||
|
() => extractDocumentMetadataPayload(singleDoc),
|
||||||
|
[singleDoc],
|
||||||
|
);
|
||||||
|
|
||||||
const singleHasOcr = useMemo(() => {
|
const singleHasOcr = useMemo(() => {
|
||||||
if (!singleDoc || typeof getDocumentAsset !== 'function') {
|
if (!singleDoc || typeof getDocumentAsset !== 'function') {
|
||||||
return false;
|
return false;
|
||||||
@@ -365,183 +193,22 @@ const DetailPanel = ({
|
|||||||
[singleHasOcr, loadSingleOcrContent],
|
[singleHasOcr, loadSingleOcrContent],
|
||||||
);
|
);
|
||||||
|
|
||||||
const openZoomPreview = useCallback((docId) => {
|
|
||||||
if (!docId) return;
|
|
||||||
setZoomedPreview({ docId });
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const closeZoomPreview = useCallback(() => {
|
|
||||||
setZoomedPreview(null);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleSingleZoom = useCallback(
|
|
||||||
(entry) => {
|
|
||||||
if (!singleHasPreview) return;
|
|
||||||
const targetId = entry?.id ?? singleDocId;
|
|
||||||
if (!targetId) return;
|
|
||||||
openZoomPreview(targetId);
|
|
||||||
},
|
|
||||||
[openZoomPreview, singleHasPreview, singleDocId],
|
|
||||||
);
|
|
||||||
|
|
||||||
const zoomDisplay = useMemo(() => {
|
|
||||||
if (
|
|
||||||
!zoomedPreview
|
|
||||||
|| !singleDoc
|
|
||||||
|| !singleDocId
|
|
||||||
|| zoomedPreview.docId !== singleDocId
|
|
||||||
|| !singleHasPreview
|
|
||||||
) {
|
|
||||||
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,
|
|
||||||
singleDocId,
|
|
||||||
singleHasPreview,
|
|
||||||
singlePreviewNavigator.currentUrl,
|
|
||||||
singlePreviewNavigator.canGoPrev,
|
|
||||||
singlePreviewNavigator.canGoNext,
|
|
||||||
singlePreviewNavigator.goPrev,
|
|
||||||
singlePreviewNavigator.goNext,
|
|
||||||
singleEffectiveCardinality,
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (zoomedPreview && !zoomDisplay) {
|
|
||||||
setZoomedPreview(null);
|
|
||||||
}
|
|
||||||
}, [zoomedPreview, zoomDisplay]);
|
|
||||||
|
|
||||||
const {
|
|
||||||
documentId: singleNavigatorDocId,
|
|
||||||
asset: singleNavigatorAsset,
|
|
||||||
ordinal: singleNavigatorOrdinal,
|
|
||||||
canGoPrev: singleNavigatorCanGoPrev,
|
|
||||||
canGoNext: singleNavigatorCanGoNext,
|
|
||||||
cardinality: singleNavigatorCardinality,
|
|
||||||
} = singlePreviewNavigator;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (typeof ensureAssetUrl !== 'function') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!singleNavigatorDocId || !singleNavigatorAsset || !Number.isFinite(singleNavigatorOrdinal)) {
|
|
||||||
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,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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?.(() => {}));
|
|
||||||
}, [
|
|
||||||
ensureAssetUrl,
|
|
||||||
singleNavigatorDocId,
|
|
||||||
singleNavigatorAsset,
|
|
||||||
singleNavigatorOrdinal,
|
|
||||||
singleNavigatorCanGoPrev,
|
|
||||||
singleNavigatorCanGoNext,
|
|
||||||
singleNavigatorCardinality,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const renderContent = () => {
|
const renderContent = () => {
|
||||||
if (!singleDoc) {
|
if (!singleDoc) {
|
||||||
return <p className="meta">Select a document to view metadata, tags and actions.</p>;
|
return <p className="meta">Select a document to view metadata, tags and actions.</p>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const effectiveCardinality = singleEffectiveCardinality;
|
|
||||||
const navCanGoPrev = Boolean(singlePreviewNavigator.canGoPrev);
|
|
||||||
const navCanGoNext = Boolean(singlePreviewNavigator.canGoNext);
|
|
||||||
const hasPreviewImage = Boolean(singlePreviewNavigator.currentUrl);
|
|
||||||
const previewMissingAsset = !singlePreviewNavigator.currentUrl;
|
|
||||||
const displayContentType = singleDoc.content_type || 'this file type';
|
|
||||||
const displayFilename =
|
|
||||||
singleDoc.filename || singleDoc.original_name || singleDoc.title || 'download';
|
|
||||||
const previewFallback = previewMissingAsset ? (
|
|
||||||
<div className="preview-pane__unsupported">
|
|
||||||
<div className="preview-pane__unsupported-message">
|
|
||||||
Preview not available for {displayContentType} files.
|
|
||||||
</div>
|
|
||||||
<div className="preview-pane__unsupported-filename">{displayFilename}</div>
|
|
||||||
{singleDownloadHref ? (
|
|
||||||
<a
|
|
||||||
className="button-link preview-pane__unsupported-download"
|
|
||||||
href={singleDownloadHref}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<DownloadIcon />
|
|
||||||
<span>Download</span>
|
|
||||||
</a>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
) : null;
|
|
||||||
const emptyMessage = previewMissingAsset ? 'Preview unavailable' : 'Preview loading…';
|
|
||||||
const showNav = hasPreviewImage && (effectiveCardinality > 1 || navCanGoPrev || navCanGoNext);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="preview-pane">
|
<section className="document-viewer document-viewer--stacked">
|
||||||
<div className="preview-pane__media">
|
<DocumentViewerLayout
|
||||||
<PreviewImage
|
|
||||||
item={singlePreviewItem}
|
|
||||||
emptyMessage={emptyMessage}
|
|
||||||
emptyContent={previewFallback}
|
|
||||||
onActivate={handlePreviewActivate}
|
|
||||||
onOpenPreview={onOpenPreview}
|
|
||||||
onZoomPreview={handleSingleZoom}
|
|
||||||
showNav={showNav}
|
|
||||||
canGoPrev={navCanGoPrev}
|
|
||||||
canGoNext={navCanGoNext}
|
|
||||||
onGoPrev={navCanGoPrev ? singlePreviewNavigator.goPrev : undefined}
|
|
||||||
onGoNext={navCanGoNext ? singlePreviewNavigator.goNext : undefined}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="document-viewer__details">
|
|
||||||
<DocumentInfoPanel
|
|
||||||
document={singleDoc}
|
document={singleDoc}
|
||||||
|
previewEntry={previewEntry}
|
||||||
summaryProps={singleSummaryProps}
|
summaryProps={singleSummaryProps}
|
||||||
contentConfig={singleContentConfig}
|
metadataPayload={singleMetadataPayload}
|
||||||
classNamePrefix="document-viewer"
|
contentTabConfig={singleContentConfig}
|
||||||
defaultTabId="details"
|
previewLoadingMessage="Loading preview…"
|
||||||
resetKey={singleDocId}
|
|
||||||
hideTabNavWhenSingle={false}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</section>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -598,7 +265,6 @@ const DetailPanel = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<aside className="detail-panel panel">
|
<aside className="detail-panel panel">
|
||||||
<PanelHeader
|
<PanelHeader
|
||||||
leading={headerLeading}
|
leading={headerLeading}
|
||||||
@@ -617,18 +283,8 @@ const DetailPanel = ({
|
|||||||
titleTag="h3"
|
titleTag="h3"
|
||||||
actions={headerActions.length ? headerActions : null}
|
actions={headerActions.length ? headerActions : null}
|
||||||
/>
|
/>
|
||||||
<div className="panel-body">
|
<div className="panel-body">{renderContent()}</div>
|
||||||
{renderContent()}
|
|
||||||
</div>
|
|
||||||
</aside>
|
</aside>
|
||||||
{singleDoc && (
|
|
||||||
<PreviewZoomOverlay
|
|
||||||
open={Boolean(zoomDisplay)}
|
|
||||||
display={zoomDisplay}
|
|
||||||
onClose={closeZoomPreview}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ const useDetailWorkspace = ({
|
|||||||
previewDocumentId,
|
previewDocumentId,
|
||||||
activePreviewId,
|
activePreviewId,
|
||||||
openDocumentPreview,
|
openDocumentPreview,
|
||||||
promoteSelectionOrder,
|
|
||||||
handleDocumentTitleUpdate,
|
handleDocumentTitleUpdate,
|
||||||
handleDocumentIssuedUpdate,
|
handleDocumentIssuedUpdate,
|
||||||
handleDocumentTagAdd,
|
handleDocumentTagAdd,
|
||||||
@@ -242,7 +241,6 @@ const useDetailWorkspace = ({
|
|||||||
onTagRemove: handleTagRemove,
|
onTagRemove: handleTagRemove,
|
||||||
previewEntry: selectedPreviewEntry,
|
previewEntry: selectedPreviewEntry,
|
||||||
onOpenPreview: openDocumentPreview,
|
onOpenPreview: openDocumentPreview,
|
||||||
onPromoteSelection: promoteSelectionOrder,
|
|
||||||
activePreviewId,
|
activePreviewId,
|
||||||
onUpdateTitle: handleDocumentTitleUpdate,
|
onUpdateTitle: handleDocumentTitleUpdate,
|
||||||
onUpdateIssued: handleDocumentIssuedUpdate,
|
onUpdateIssued: handleDocumentIssuedUpdate,
|
||||||
@@ -272,7 +270,6 @@ const useDetailWorkspace = ({
|
|||||||
handleDocumentTitleUpdate,
|
handleDocumentTitleUpdate,
|
||||||
handleTagRemove,
|
handleTagRemove,
|
||||||
openDocumentPreview,
|
openDocumentPreview,
|
||||||
promoteSelectionOrder,
|
|
||||||
resolveApiPath,
|
resolveApiPath,
|
||||||
resolveFolderPath,
|
resolveFolderPath,
|
||||||
selectFolder,
|
selectFolder,
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import DocumentInfoPanel from '../documents/DocumentInfoPanel';
|
||||||
|
import { DownloadIcon } from '../ui/icons';
|
||||||
|
|
||||||
|
const DocumentViewerLayout = ({
|
||||||
|
document,
|
||||||
|
previewEntry,
|
||||||
|
summaryProps,
|
||||||
|
metadataPayload,
|
||||||
|
contentTabConfig,
|
||||||
|
resetKey,
|
||||||
|
classNamePrefix = 'document-viewer',
|
||||||
|
defaultTabId = 'details',
|
||||||
|
infoPanelProps = {},
|
||||||
|
previewLoadingMessage = 'Preparing preview…',
|
||||||
|
}) => {
|
||||||
|
const previewContent = useMemo(() => {
|
||||||
|
if (!document || !previewEntry?.url) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedContentType = (previewEntry.contentType
|
||||||
|
|| document.content_type
|
||||||
|
|| '')
|
||||||
|
.toLowerCase();
|
||||||
|
const isImage = normalizedContentType.startsWith('image/');
|
||||||
|
const isPdf = normalizedContentType === 'application/pdf'
|
||||||
|
|| normalizedContentType === 'application/x-pdf';
|
||||||
|
|
||||||
|
if (isImage) {
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
src={previewEntry.url}
|
||||||
|
alt={`Preview of ${document.title}`}
|
||||||
|
className="document-viewer__object document-viewer__object--image"
|
||||||
|
draggable={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPdf) {
|
||||||
|
return (
|
||||||
|
<iframe
|
||||||
|
src={previewEntry.url}
|
||||||
|
title={`Preview of ${document.title}`}
|
||||||
|
className="document-viewer__object"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayContentType = document.content_type || previewEntry.contentType || 'this file type';
|
||||||
|
const displayFilename = previewEntry.filename
|
||||||
|
|| document.filename
|
||||||
|
|| document.original_name
|
||||||
|
|| 'download';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="document-viewer__unsupported">
|
||||||
|
<div className="document-viewer__unsupported-message">
|
||||||
|
Preview is not available for {displayContentType} files.
|
||||||
|
</div>
|
||||||
|
<div className="document-viewer__unsupported-filename">{displayFilename}</div>
|
||||||
|
<a
|
||||||
|
className="button-link document-viewer__unsupported-download"
|
||||||
|
href={previewEntry.url}
|
||||||
|
download={displayFilename}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<DownloadIcon />
|
||||||
|
<span>Download</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}, [previewEntry, document]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="document-viewer__details-pane">
|
||||||
|
<div className="document-viewer__details">
|
||||||
|
<DocumentInfoPanel
|
||||||
|
document={document}
|
||||||
|
summaryProps={summaryProps}
|
||||||
|
metadataPayload={metadataPayload}
|
||||||
|
contentConfig={contentTabConfig}
|
||||||
|
defaultTabId={defaultTabId}
|
||||||
|
classNamePrefix={classNamePrefix}
|
||||||
|
hideTabNavWhenSingle={false}
|
||||||
|
resetKey={resetKey || document?.id}
|
||||||
|
{...infoPanelProps}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="document-viewer__viewport">
|
||||||
|
{!previewEntry?.url ? (
|
||||||
|
<div className="document-viewer__message">{previewLoadingMessage}</div>
|
||||||
|
) : (
|
||||||
|
previewContent
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DocumentViewerLayout;
|
||||||
@@ -11,10 +11,10 @@ import {
|
|||||||
buildCorrespondentOptions,
|
buildCorrespondentOptions,
|
||||||
sortCorrespondents,
|
sortCorrespondents,
|
||||||
} from '../documents/DocumentSummarySection';
|
} from '../documents/DocumentSummarySection';
|
||||||
import DocumentInfoPanel from '../documents/DocumentInfoPanel';
|
|
||||||
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 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.6;
|
||||||
@@ -83,66 +83,6 @@ const DocumentViewerPanel = ({
|
|||||||
[correspondents],
|
[correspondents],
|
||||||
);
|
);
|
||||||
|
|
||||||
const previewContent = useMemo(() => {
|
|
||||||
if (!document || !previewEntry?.url) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalizedContentType = (previewEntry.contentType
|
|
||||||
|| document.content_type
|
|
||||||
|| '')
|
|
||||||
.toLowerCase();
|
|
||||||
const isImage = normalizedContentType.startsWith('image/');
|
|
||||||
const isPdf = normalizedContentType === 'application/pdf'
|
|
||||||
|| normalizedContentType === 'application/x-pdf';
|
|
||||||
|
|
||||||
if (isImage) {
|
|
||||||
return (
|
|
||||||
<img
|
|
||||||
src={previewEntry.url}
|
|
||||||
alt={`Preview of ${document.title}`}
|
|
||||||
className="document-viewer__object document-viewer__object--image"
|
|
||||||
draggable={false}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isPdf) {
|
|
||||||
return (
|
|
||||||
<iframe
|
|
||||||
src={previewEntry.url}
|
|
||||||
title={`Preview of ${document.title}`}
|
|
||||||
className="document-viewer__object"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const displayContentType = document.content_type || previewEntry.contentType || 'this file type';
|
|
||||||
const displayFilename = previewEntry.filename
|
|
||||||
|| document.filename
|
|
||||||
|| document.original_name
|
|
||||||
|| 'download';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="document-viewer__unsupported">
|
|
||||||
<div className="document-viewer__unsupported-message">
|
|
||||||
Preview is not available for {displayContentType} files.
|
|
||||||
</div>
|
|
||||||
<div className="document-viewer__unsupported-filename">{displayFilename}</div>
|
|
||||||
<a
|
|
||||||
className="button-link document-viewer__unsupported-download"
|
|
||||||
href={previewEntry.url}
|
|
||||||
download={displayFilename}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<DownloadIcon />
|
|
||||||
<span>Download</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}, [previewEntry, document]);
|
|
||||||
|
|
||||||
const metadataPayload = useMemo(
|
const metadataPayload = useMemo(
|
||||||
() => extractDocumentMetadataPayload(document),
|
() => extractDocumentMetadataPayload(document),
|
||||||
[document],
|
[document],
|
||||||
@@ -316,27 +256,14 @@ const DocumentViewerPanel = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={viewerClassName} ref={viewerRef}>
|
<section className={viewerClassName} ref={viewerRef}>
|
||||||
<div className="document-viewer__details-pane">
|
<DocumentViewerLayout
|
||||||
<div className="document-viewer__details">
|
|
||||||
<DocumentInfoPanel
|
|
||||||
document={document}
|
document={document}
|
||||||
|
previewEntry={previewEntry}
|
||||||
summaryProps={summaryProps}
|
summaryProps={summaryProps}
|
||||||
metadataPayload={metadataPayload}
|
metadataPayload={metadataPayload}
|
||||||
contentConfig={contentTabConfig}
|
contentTabConfig={contentTabConfig}
|
||||||
defaultTabId="details"
|
previewLoadingMessage="Loading preview…"
|
||||||
classNamePrefix="document-viewer"
|
|
||||||
hideTabNavWhenSingle={false}
|
|
||||||
resetKey={document?.id}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="document-viewer__viewport">
|
|
||||||
{!previewEntry?.url ? (
|
|
||||||
<div className="document-viewer__message">Loading preview…</div>
|
|
||||||
) : (
|
|
||||||
previewContent
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user