This commit is contained in:
2025-11-02 01:18:24 +01:00
parent 32d627fd6d
commit 7d8595b63f
6 changed files with 65 additions and 118 deletions
@@ -0,0 +1,330 @@
import React, { useMemo } from 'react';
import { DownloadIcon, TextScanIcon, AnalyzeIcon, CloseIcon } from '../ui/icons';
import DocumentSummarySection, {
buildCorrespondentOptions,
sortCorrespondents,
} from '../documents/DocumentSummarySection';
import { createDocumentActionState } from '../documents/documentActions';
const formatDateTime = (value) => {
if (!value) {
return '—';
}
const date = new Date(value);
return Number.isNaN(date.getTime()) ? '—' : date.toLocaleString();
};
const DocumentViewerPanel = ({
document,
documentId,
previewEntry,
tagLookupById,
tagOptions,
onTagAdd,
onTagRemove,
correspondents,
onCorrespondentAdd,
onCorrespondentRemove,
onUpdateTitle,
onUpdateIssued,
}) => {
const sortedCorrespondents = useMemo(
() => sortCorrespondents(document?.correspondents || []),
[document],
);
const correspondentOptions = useMemo(
() => buildCorrespondentOptions(Array.isArray(correspondents) ? correspondents : []),
[correspondents],
);
const metadataItems = useMemo(() => {
if (!document) {
return [];
}
return [
{ label: 'Created at', value: formatDateTime(document.created_at) },
{ label: 'Updated at', value: formatDateTime(document.updated_at) },
{
label: 'Filename',
value: document.archive_path || document.filename || '—',
},
{
label: 'Original filename',
value: document.original_name || '—',
},
{
label: 'SHA-256 checksum',
value: document.current_version?.checksum || '—',
},
{
label: 'Content type',
value: document.content_type || '—',
},
];
}, [document]);
const contentType = (document?.content_type || '').toLowerCase();
const previewContent = useMemo(() => {
if (!previewEntry?.url) {
return null;
}
const title = document?.title || document?.original_name || 'Document';
if (contentType.startsWith('image/')) {
return (
<img
src={previewEntry.url}
alt={`Preview of ${title}`}
className="document-viewer__object document-viewer__object--image"
draggable={false}
/>
);
}
return (
<iframe
src={previewEntry.url}
title={`Preview of ${title}`}
className="document-viewer__object"
/>
);
}, [previewEntry?.url, contentType, document?.title, document?.original_name]);
const metadataPayload = useMemo(() => {
if (!document || !document.metadata || Object.keys(document.metadata).length === 0) {
return null;
}
return document.metadata;
}, [document]);
if (!document) {
return (
<section className="document-viewer document-viewer--loading">
<div className="document-viewer__details">
<div className="document-viewer__message">
Loading document{documentId ? ` ${documentId}` : ''}
</div>
</div>
<div className="document-viewer__viewport">
<div className="document-viewer__message">Preparing preview</div>
</div>
</section>
);
}
return (
<section className="document-viewer">
<div className="document-viewer__details">
<DocumentSummarySection
document={document}
tagLookupById={tagLookupById}
tagOptions={tagOptions}
onTagAdd={onTagAdd}
onTagRemove={onTagRemove}
correspondents={sortedCorrespondents}
correspondentOptions={correspondentOptions}
onCorrespondentAdd={onCorrespondentAdd}
onCorrespondentRemove={onCorrespondentRemove}
onUpdateTitle={onUpdateTitle}
onUpdateIssued={onUpdateIssued}
/>
<section className="preview-section">
<h3 className="preview-section__title">Metadata</h3>
<dl className="preview-section__list">
{metadataItems.map(({ label, value }) => (
<div className="preview-section__item" key={label}>
<dt>{label}</dt>
<dd>{value || '—'}</dd>
</div>
))}
</dl>
{metadataPayload ? (
<details className="preview-section__payload">
<summary>Show metadata payload</summary>
<pre>{JSON.stringify(metadataPayload, null, 2)}</pre>
</details>
) : null}
</section>
</div>
<div className="document-viewer__viewport">
{!previewEntry?.url ? (
<div className="document-viewer__message">Loading preview</div>
) : (
previewContent
)}
</div>
</section>
);
};
export default DocumentViewerPanel;
export const createDocumentViewerHeaderActions = ({
document,
ensureAssetUrl,
ensurePreviewData,
getDocumentAsset,
resolveApiPath,
notifyApiError,
onRegenerate,
}) => {
if (!document) {
return null;
}
const { downloadHref, hasOcr, openOcr } = createDocumentActionState({
document,
resolveApiPath,
ensurePreviewData,
ensureAssetUrl,
getDocumentAsset,
notifyApiError,
ocrErrorMessage: 'Unable to open OCR text.',
});
return (
<>
{downloadHref ? (
<a
className="icon-button"
href={downloadHref}
target="_blank"
rel="noopener noreferrer"
aria-label="Download document"
title="Download document"
>
<DownloadIcon />
</a>
) : null}
{hasOcr ? (
<button
type="button"
className="icon-button"
onClick={() => {
openOcr().catch(() => {});
}}
aria-label="View OCR text"
title="View OCR text"
>
<TextScanIcon />
</button>
) : null}
<button
type="button"
className="icon-button"
onClick={() => onRegenerate(document.id)}
aria-label="Re-run analysis"
title="Re-run analysis"
>
<AnalyzeIcon />
</button>
</>
);
};
export const createDocumentViewerSurface = ({
documentId,
document,
previewEntry,
ensureAssetUrl,
ensurePreviewData,
getDocumentAsset,
resolveApiPath,
notifyApiError,
onRegenerate,
onClose,
renderSidebarToggle,
tagLookupById,
tagOptions,
onTagAdd,
onTagRemove,
correspondents,
onCorrespondentAdd,
onCorrespondentRemove,
onUpdateTitle,
onUpdateIssued,
resolveFolderPath,
}) => {
if (!documentId && !document) {
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>
)
: null;
const sidebarToggle = renderSidebarToggle ? renderSidebarToggle() : null;
const leading = closeButton || sidebarToggle
? (
<>
{sidebarToggle}
{closeButton}
</>
)
: null;
let breadcrumbs = null;
if (document && 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 }))
: [];
const documentLabel = document.title || document.original_name || 'Document';
breadcrumbs = [
...normalizedSegments,
{ id: document.id || 'current-document', name: documentLabel },
];
}
const header = {
title,
subtitle: null,
leading,
actions: createDocumentViewerHeaderActions({
document,
ensureAssetUrl,
ensurePreviewData,
getDocumentAsset,
resolveApiPath,
notifyApiError,
onRegenerate,
}),
breadcrumbs,
};
return {
key: 'preview',
variant: 'preview',
header,
content: (
<DocumentViewerPanel
document={document}
documentId={documentId}
previewEntry={previewEntry}
tagLookupById={tagLookupById}
tagOptions={tagOptions}
onTagAdd={onTagAdd}
onTagRemove={onTagRemove}
correspondents={correspondents}
onCorrespondentAdd={onCorrespondentAdd}
onCorrespondentRemove={onCorrespondentRemove}
onUpdateTitle={onUpdateTitle}
onUpdateIssued={onUpdateIssued}
/>
),
supportsDetail: false,
};
};