Files
papercrate/frontend/src/preview/PreviewWorkspace.jsx
T
2025-11-01 04:16:31 +01:00

284 lines
7.0 KiB
React

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 PreviewWorkspace = ({
document,
previewEntry,
tagLookupById,
tagOptions,
onTagAdd,
onTagRemove,
correspondents,
onCorrespondentAdd,
onCorrespondentRemove,
onUpdateTitle,
onUpdateIssued,
resolveFolderPath,
onFolderNavigate,
}) => {
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 metadataPayload = useMemo(() => {
if (!document || !document.metadata || Object.keys(document.metadata).length === 0) {
return null;
}
return document.metadata;
}, [document]);
if (!document) {
return null;
}
return (
<section className="preview-workspace">
<div className="preview-workspace__details">
<DocumentSummarySection
document={document}
tagLookupById={tagLookupById}
tagOptions={tagOptions}
onTagAdd={onTagAdd}
onTagRemove={onTagRemove}
correspondents={sortedCorrespondents}
correspondentOptions={correspondentOptions}
onCorrespondentAdd={onCorrespondentAdd}
onCorrespondentRemove={onCorrespondentRemove}
onUpdateTitle={onUpdateTitle}
onUpdateIssued={onUpdateIssued}
resolveFolderPath={resolveFolderPath}
onFolderNavigate={onFolderNavigate}
/>
<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="preview-workspace__viewer">
{!previewEntry?.url ? (
<div className="preview-workspace__message">Loading preview</div>
) : (
<iframe
src={previewEntry.url}
title={`Preview of ${document.title}`}
className="preview-workspace__object"
/>
)}
</div>
</section>
);
};
export default PreviewWorkspace;
export const createPreviewWorkspaceHeaderActions = ({
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 createPreviewSurface = ({
document,
previewEntry,
ensureAssetUrl,
ensurePreviewData,
getDocumentAsset,
resolveApiPath,
notifyApiError,
onRegenerate,
onClose,
renderSidebarToggle,
tagLookupById,
tagOptions,
onTagAdd,
onTagRemove,
correspondents,
onCorrespondentAdd,
onCorrespondentRemove,
onUpdateTitle,
onUpdateIssued,
resolveFolderPath,
onFolderNavigate,
}) => {
if (!document) {
return null;
}
const title = document.title;
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;
const header = {
title,
subtitle: null,
leading,
actions: createPreviewWorkspaceHeaderActions({
document,
ensureAssetUrl,
ensurePreviewData,
getDocumentAsset,
resolveApiPath,
notifyApiError,
onRegenerate,
}),
};
return {
key: 'preview',
variant: 'preview',
header,
content: (
<PreviewWorkspace
document={document}
previewEntry={previewEntry}
tagLookupById={tagLookupById}
tagOptions={tagOptions}
onTagAdd={onTagAdd}
onTagRemove={onTagRemove}
correspondents={correspondents}
onCorrespondentAdd={onCorrespondentAdd}
onCorrespondentRemove={onCorrespondentRemove}
onUpdateTitle={onUpdateTitle}
onUpdateIssued={onUpdateIssued}
resolveFolderPath={resolveFolderPath}
onFolderNavigate={onFolderNavigate}
/>
),
supportsDetail: false,
};
};