frontend cleanup

This commit is contained in:
2025-10-30 18:17:01 +01:00
parent bb34a47fa8
commit 85f3d90329
7 changed files with 510 additions and 292 deletions
+123 -117
View File
@@ -13,8 +13,9 @@ import { getTagColorStyle } from '../utils/colors';
import { formatFileSize } from '../utils/format';
import { resolveDocumentAssetUrl, createAssetView } from '../asset_manager';
import { useAssetNavigator } from '../hooks/useAssetNavigator';
import { describeDocumentSummary } from '../documents/documentSummary';
import { createDocumentActionState } from '../documents/documentActions';
import PreviewZoomOverlay from './PreviewZoomOverlay';
import { openOcrTextInNewTab } from '../utils/ocr';
const MAX_PREVIEW_STACK_ITEMS = 15;
@@ -314,14 +315,29 @@ const DetailPanel = ({
[selectedDocuments],
);
const singleDownloadHref = useMemo(() => {
if (!singleDoc) return null;
const downloadPath = singleDoc.current_version?.download_path;
if (!downloadPath || !resolveApiPath) {
return null;
const { downloadHref: singleDownloadHref, hasOcr: singleHasOcr, openOcr } = useMemo(
() =>
createDocumentActionState({
document: singleDoc,
resolveApiPath,
ensurePreviewData,
ensureAssetUrl,
getDocumentAsset,
}),
[singleDoc, resolveApiPath, ensurePreviewData, ensureAssetUrl, getDocumentAsset],
);
const detailSummary = useMemo(() => describeDocumentSummary(singleDoc), [singleDoc]);
const headerTitle = useMemo(() => {
if (selectedCount === 0) {
return 'Document details';
}
return resolveApiPath(downloadPath);
}, [singleDoc, resolveApiPath]);
if (selectedCount === 1) {
return detailSummary.title;
}
return `${selectedCount} document${selectedCount === 1 ? '' : 's'}`;
}, [selectedCount, detailSummary]);
const [titleEditDocId, setTitleEditDocId] = useState(null);
const [titleDraft, setTitleDraft] = useState('');
@@ -403,27 +419,6 @@ const DetailPanel = ({
[onPromoteSelection],
);
const hasOcrAsset = useMemo(
() => Boolean(singleDoc && getDocumentAsset(singleDoc, 'ocr-text')),
[singleDoc, getDocumentAsset],
);
const openOcr = useCallback(async () => {
if (!singleDoc) {
return;
}
try {
await openOcrTextInNewTab({
document: singleDoc,
ensurePreviewData,
getDocumentAsset,
ensureAssetUrl,
});
} catch (error) {
/* noop */
}
}, [singleDoc, ensurePreviewData, getDocumentAsset, ensureAssetUrl]);
const singlePreviewNavigator = useAssetNavigator({
document: singleDoc,
assetType: 'preview',
@@ -614,6 +609,90 @@ const DetailPanel = ({
return segments;
}, [singleDoc?.folder_id, resolveFolderPath]);
const folderLabel = detailSummary.folderLabel;
const folderDisplayNode = useMemo(() => {
if (!singleDoc) {
return folderLabel || '—';
}
if (!singleFolderPath?.length) {
return folderLabel || '—';
}
return (
<span className="detail-folder-path">
{singleFolderPath.map((segment, index) => {
const label = segment?.name || '…';
const targetId = segment?.id || null;
const key = `${targetId || label}-${index}`;
const isClickable = Boolean(targetId) && typeof onFolderNavigate === 'function';
const href = !isClickable
? null
: targetId === 'root'
? '/documents'
: `/documents/folder/${targetId}`;
return (
<React.Fragment key={key}>
{index > 0 ? <span className="detail-folder-path__separator">/</span> : null}
{isClickable ? (
<a
href={href}
className="detail-folder-path__link"
onClick={(event) => {
if (
event.button !== 0 ||
event.metaKey ||
event.ctrlKey ||
event.shiftKey ||
event.altKey
) {
return;
}
event.preventDefault();
event.stopPropagation();
onFolderNavigate(targetId);
}}
>
{label}
</a>
) : (
<span className="detail-folder-path__segment">{label}</span>
)}
</React.Fragment>
);
})}
</span>
);
}, [singleDoc, singleFolderPath, folderLabel, onFolderNavigate]);
const detailInfoRows = useMemo(() => {
if (!singleDoc) {
return [];
}
const allowedKeys = new Set(['uploaded', 'size', 'type', 'issued', 'pages', 'created', 'updated', 'folder']);
const rows = detailSummary.summaryRows
.filter((row) => {
if (!allowedKeys.has(row.key)) {
return false;
}
if (row.key === 'pages') {
return Number.isFinite(detailSummary.pageCount);
}
if (row.key === 'folder') {
return Boolean(singleFolderPath?.length);
}
return true;
})
.map((row) => (row.key === 'folder' ? { ...row, value: folderDisplayNode } : row));
rows.push({
key: 'original-name',
label: 'Original filename',
value: singleDoc.original_name || '—',
});
return rows;
}, [singleDoc, detailSummary, folderDisplayNode, singleFolderPath]);
const bulkCorrespondents = useMemo(() => {
if (selectedDocuments.length <= 1) {
const doc = selectedDocuments[0];
@@ -890,20 +969,7 @@ const DetailPanel = ({
const displayName = singleDoc.title || singleDoc.original_name;
const isEditingTitle = titleEditDocId === singleDoc.id;
const sizeBytes = Number(singleDoc.current_version?.size_bytes) || 0;
const sizeLabel = sizeBytes > 0 ? formatFileSize(sizeBytes) : '—';
const issuedAt = singleDoc.issued_at
? new Date(singleDoc.issued_at).toLocaleString()
: '—';
const tagsForDoc = Array.isArray(singleDoc.tags) ? singleDoc.tags : [];
const pageCountRaw = singleDoc.current_version?.metadata?.page_count;
const pageCountValue =
typeof pageCountRaw === 'number'
? pageCountRaw
: pageCountRaw != null && pageCountRaw !== ''
? Number.parseInt(pageCountRaw, 10)
: null;
const hasPageCount = Number.isFinite(pageCountValue) && pageCountValue >= 0;
const metadata =
singleDoc.metadata && Object.keys(singleDoc.metadata).length > 0 ? singleDoc.metadata : null;
const effectiveCardinality = singleEffectiveCardinality;
@@ -1014,76 +1080,17 @@ const DetailPanel = ({
</div>
{titleError ? <div className="status-inline error">{titleError}</div> : null}
<div className="meta">
<div>
<strong>Uploaded:</strong>{' '}
{singleDoc.uploaded_at ? new Date(singleDoc.uploaded_at).toLocaleString() : '—'}
</div>
<div>
<strong>Size:</strong>{' '}
{sizeLabel}
</div>
<div>
<strong>Type:</strong> {singleDoc.content_type || 'Unknown'}
</div>
<div>
<strong>Issued:</strong> {issuedAt}
</div>
{hasPageCount ? (
<div>
<strong>Pages:</strong> {pageCountValue}
</div>
) : null}
{singleFolderPath?.length ? (
<div>
<strong>Folder:</strong>{' '}
<span className="detail-folder-path">
{singleFolderPath.map((segment, index) => {
const label = segment?.name || '…';
const targetId = segment?.id || null;
const key = `${targetId || label}-${index}`;
const isClickable = Boolean(targetId) && typeof onFolderNavigate === 'function';
const href = !isClickable
? null
: targetId === 'root'
? '/documents'
: `/documents/folder/${targetId}`;
return (
<React.Fragment key={key}>
{index > 0 ? <span className="detail-folder-path__separator">/</span> : null}
{isClickable ? (
<a
href={href}
className="detail-folder-path__link"
onClick={(event) => {
if (
event.button !== 0 ||
event.metaKey ||
event.ctrlKey ||
event.shiftKey ||
event.altKey
) {
return;
}
event.preventDefault();
event.stopPropagation();
onFolderNavigate(targetId);
}}
>
{label}
</a>
) : (
<span className="detail-folder-path__segment">{label}</span>
)}
</React.Fragment>
);
})}
</span>
</div>
) : null}
<div>
<strong>Original filename:</strong>{' '}
{singleDoc.original_name}
</div>
{detailInfoRows.map((row) => {
const rawValue = row.value;
const displayValue =
rawValue === null || rawValue === undefined || rawValue === '' ? '—' : rawValue;
return (
<div key={row.key}>
<strong>{row.label}:</strong>{' '}
{displayValue}
</div>
);
})}
</div>
<TagSection
title="Tags"
@@ -1227,13 +1234,12 @@ const DetailPanel = ({
};
const isBulkSelection = selectedCount > 1;
const showOcrAction = Boolean(singleDoc && hasOcrAsset);
const showOcrAction = Boolean(singleDoc && singleHasOcr);
return (
<>
<aside className="detail-panel panel">
<div className="panel-header">
<div className="panel-actions">
<button
type="button"
className="icon-button"
@@ -1258,7 +1264,8 @@ const DetailPanel = ({
<WindowMaximizeIcon />
</button>
) : null}
<div className="spacer" />
<h3 className="panel-header__title">{headerTitle}</h3>
<div className="panel-actions__spacer" />
{isBulkSelection && onBulkReanalyze ? (
<button
type="button"
@@ -1292,7 +1299,7 @@ const DetailPanel = ({
className="icon-button"
onClick={(event) => {
event.stopPropagation();
openOcr();
openOcr().catch(() => {});
}}
aria-label="View OCR text"
title="View OCR text"
@@ -1315,7 +1322,6 @@ const DetailPanel = ({
</button>
) : null}
</div>
</div>
<div className="panel-body">
{selectedCount <= 1 ? renderSingle() : renderBulk()}
</div>