desktop redo

This commit is contained in:
2025-11-05 12:29:30 +01:00
parent 928753add8
commit 934fb6b689
7 changed files with 795 additions and 360 deletions
+78 -201
View File
@@ -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 ? (