frontend cleanup
This commit is contained in:
+123
-117
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user