desktop redo
This commit is contained in:
@@ -1,20 +1,14 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { DownloadIcon, CloseIcon } from '../ui/icons';
|
||||
import DocumentSummarySection, {
|
||||
import {
|
||||
buildCorrespondentOptions,
|
||||
sortCorrespondents,
|
||||
} from '../documents/DocumentSummarySection';
|
||||
import DocumentInfoPanel from '../documents/DocumentInfoPanel';
|
||||
import { extractDocumentMetadataPayload } from '../documents/documentMetadata';
|
||||
import { createDocumentActionState } from '../documents/documentActions';
|
||||
import { resolveDocumentAssetUrl } from '../asset_manager';
|
||||
|
||||
const formatDateTime = (value) => {
|
||||
if (!value) {
|
||||
return '—';
|
||||
}
|
||||
const date = new Date(value);
|
||||
return Number.isNaN(date.getTime()) ? '—' : date.toLocaleString();
|
||||
};
|
||||
|
||||
const DocumentViewerPanel = ({
|
||||
document,
|
||||
documentId,
|
||||
@@ -42,32 +36,6 @@ const DocumentViewerPanel = ({
|
||||
[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.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 previewContent = useMemo(() => {
|
||||
if (!document || !previewEntry?.url) {
|
||||
return null;
|
||||
@@ -128,31 +96,41 @@ const DocumentViewerPanel = ({
|
||||
);
|
||||
}, [previewEntry, document]);
|
||||
|
||||
const metadataPayload = useMemo(() => {
|
||||
if (!document || !document.metadata || Object.keys(document.metadata).length === 0) {
|
||||
return null;
|
||||
}
|
||||
return document.metadata;
|
||||
}, [document]);
|
||||
const metadataPayload = useMemo(
|
||||
() => extractDocumentMetadataPayload(document),
|
||||
[document],
|
||||
);
|
||||
|
||||
const [activeTab, setActiveTab] = useState('details');
|
||||
useEffect(() => {
|
||||
setActiveTab('details');
|
||||
}, [document?.id, hasOcr, metadataPayload]);
|
||||
const summaryProps = useMemo(
|
||||
() => ({
|
||||
tagLookupById,
|
||||
tagOptions,
|
||||
onTagAdd,
|
||||
onTagRemove,
|
||||
correspondents: sortedCorrespondents,
|
||||
correspondentOptions,
|
||||
onCorrespondentAdd,
|
||||
onCorrespondentRemove,
|
||||
onUpdateTitle,
|
||||
onUpdateIssued,
|
||||
}),
|
||||
[
|
||||
tagLookupById,
|
||||
tagOptions,
|
||||
onTagAdd,
|
||||
onTagRemove,
|
||||
sortedCorrespondents,
|
||||
correspondentOptions,
|
||||
onCorrespondentAdd,
|
||||
onCorrespondentRemove,
|
||||
onUpdateTitle,
|
||||
onUpdateIssued,
|
||||
],
|
||||
);
|
||||
|
||||
const [ocrContent, setOcrContent] = useState(null);
|
||||
const [ocrLoading, setOcrLoading] = useState(false);
|
||||
const [ocrError, setOcrError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
const loadOcrContent = useCallback(async ({ signal } = {}) => {
|
||||
if (!document || !hasOcr || typeof getDocumentAsset !== 'function') {
|
||||
setOcrContent(null);
|
||||
setOcrLoading(false);
|
||||
setOcrError(null);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
return '';
|
||||
}
|
||||
|
||||
const updateUrl = () =>
|
||||
@@ -162,68 +140,47 @@ const DocumentViewerPanel = ({
|
||||
});
|
||||
|
||||
const asset = getDocumentAsset(document, 'ocr-text');
|
||||
let url = updateUrl();
|
||||
|
||||
const ensureAndUpdate = async () => {
|
||||
setOcrLoading(true);
|
||||
setOcrError(null);
|
||||
|
||||
let url = updateUrl();
|
||||
if (!url && document.id && asset?.id && typeof ensureAssetUrl === 'function') {
|
||||
try {
|
||||
await ensureAssetUrl(document.id, asset, { start: 1, limit: 1 });
|
||||
url = updateUrl();
|
||||
} catch (error) {
|
||||
if (!cancelled) {
|
||||
setOcrError('Unable to load OCR content.');
|
||||
}
|
||||
}
|
||||
if (!url && document.id && asset?.id && typeof ensureAssetUrl === 'function') {
|
||||
await ensureAssetUrl(document.id, asset, { start: 1, limit: 1 });
|
||||
if (signal?.aborted) {
|
||||
throw new DOMException('Aborted', 'AbortError');
|
||||
}
|
||||
url = updateUrl();
|
||||
}
|
||||
|
||||
let textContent = null;
|
||||
if (!cancelled && url) {
|
||||
const controller = new AbortController();
|
||||
if (!url) {
|
||||
return '';
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
mode: 'cors',
|
||||
credentials: 'omit',
|
||||
signal: controller.signal,
|
||||
});
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
mode: 'cors',
|
||||
credentials: 'omit',
|
||||
signal,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Unexpected status: ${response.status}`);
|
||||
}
|
||||
if (!response.ok) {
|
||||
throw new Error(`Unexpected status: ${response.status}`);
|
||||
}
|
||||
|
||||
textContent = await response.text();
|
||||
} catch (error) {
|
||||
if (!cancelled) {
|
||||
console.error('[OCR] Failed to fetch text', error);
|
||||
setOcrError('Unable to load OCR content.');
|
||||
}
|
||||
}
|
||||
return response.text();
|
||||
}, [document, hasOcr, getDocumentAsset, ensureAssetUrl]);
|
||||
|
||||
if (!cancelled) {
|
||||
setOcrContent(textContent);
|
||||
}
|
||||
|
||||
controller.abort();
|
||||
}
|
||||
|
||||
if (!cancelled) {
|
||||
if (!textContent) {
|
||||
setOcrContent(null);
|
||||
}
|
||||
setOcrLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
ensureAndUpdate();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [document, hasOcr, ensureAssetUrl, getDocumentAsset]);
|
||||
const contentTabConfig = useMemo(
|
||||
() => ({
|
||||
enabled: hasOcr,
|
||||
id: 'content',
|
||||
label: 'Content',
|
||||
loadContent: loadOcrContent,
|
||||
loadingMessage: 'Loading OCR content…',
|
||||
emptyMessage: 'No OCR content available.',
|
||||
unavailableMessage: 'No OCR content available.',
|
||||
errorMessage: 'Failed to load OCR content.',
|
||||
}),
|
||||
[hasOcr, loadOcrContent],
|
||||
);
|
||||
|
||||
if (!document) {
|
||||
return (
|
||||
@@ -243,96 +200,16 @@ const DocumentViewerPanel = ({
|
||||
return (
|
||||
<section className="document-viewer">
|
||||
<div className="document-viewer__details">
|
||||
<DocumentSummarySection
|
||||
<DocumentInfoPanel
|
||||
document={document}
|
||||
tagLookupById={tagLookupById}
|
||||
tagOptions={tagOptions}
|
||||
onTagAdd={onTagAdd}
|
||||
onTagRemove={onTagRemove}
|
||||
correspondents={sortedCorrespondents}
|
||||
correspondentOptions={correspondentOptions}
|
||||
onCorrespondentAdd={onCorrespondentAdd}
|
||||
onCorrespondentRemove={onCorrespondentRemove}
|
||||
onUpdateTitle={onUpdateTitle}
|
||||
onUpdateIssued={onUpdateIssued}
|
||||
summaryProps={summaryProps}
|
||||
metadataPayload={metadataPayload}
|
||||
contentConfig={contentTabConfig}
|
||||
defaultTabId="details"
|
||||
classNamePrefix="document-viewer"
|
||||
hideTabNavWhenSingle={false}
|
||||
resetKey={document?.id}
|
||||
/>
|
||||
<div className="document-viewer__tabs-wrapper">
|
||||
<div className="document-viewer__tabs" role="tablist" aria-label="Document details">
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={activeTab === 'details'}
|
||||
className={`document-viewer__tab${activeTab === 'details' ? ' is-active' : ''}`}
|
||||
onClick={() => setActiveTab('details')}
|
||||
>
|
||||
Details
|
||||
</button>
|
||||
{hasOcr ? (
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={activeTab === 'content'}
|
||||
className={`document-viewer__tab${activeTab === 'content' ? ' is-active' : ''}`}
|
||||
onClick={() => setActiveTab('content')}
|
||||
>
|
||||
Content
|
||||
</button>
|
||||
) : null}
|
||||
{metadataPayload ? (
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={activeTab === 'metadata'}
|
||||
className={`document-viewer__tab${activeTab === 'metadata' ? ' is-active' : ''}`}
|
||||
onClick={() => setActiveTab('metadata')}
|
||||
>
|
||||
Metadata
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="document-viewer__tabpanes">
|
||||
{activeTab === 'details' ? (
|
||||
<div role="tabpanel" className="document-viewer__tabpanel">
|
||||
<section className="document-viewer__section">
|
||||
<dl className="document-viewer__section-list">
|
||||
{metadataItems.map(({ label, value }) => (
|
||||
<div className="document-viewer__section-item" key={label}>
|
||||
<dt>{label}</dt>
|
||||
<dd>{value || '—'}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</section>
|
||||
</div>
|
||||
) : null}
|
||||
{activeTab === 'content' && hasOcr ? (
|
||||
<div role="tabpanel" className="document-viewer__tabpanel">
|
||||
{ocrLoading ? (
|
||||
<div className="document-viewer__message">Loading OCR content…</div>
|
||||
) : ocrError ? (
|
||||
<div className="document-viewer__message document-viewer__message--error">
|
||||
{ocrError}
|
||||
</div>
|
||||
) : ocrContent ? (
|
||||
<pre className="document-viewer__object document-viewer__object--ocr-text">
|
||||
{ocrContent}
|
||||
</pre>
|
||||
) : (
|
||||
<div className="document-viewer__message">No OCR content available.</div>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
{activeTab === 'metadata' && metadataPayload ? (
|
||||
<div role="tabpanel" className="document-viewer__tabpanel">
|
||||
<section className="document-viewer__section document-viewer__section--metadata-json">
|
||||
<pre className="document-viewer__metadata-json">
|
||||
{JSON.stringify(metadataPayload, null, 2)}
|
||||
</pre>
|
||||
</section>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="document-viewer__viewport">
|
||||
{!previewEntry?.url ? (
|
||||
|
||||
Reference in New Issue
Block a user