rename
This commit is contained in:
@@ -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,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user