diff --git a/frontend/src/detail/DetailPanel.jsx b/frontend/src/detail/DetailPanel.jsx
index ac86ccf..4b74640 100644
--- a/frontend/src/detail/DetailPanel.jsx
+++ b/frontend/src/detail/DetailPanel.jsx
@@ -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 (
+
+ {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 (
+
+ {index > 0 ? / : null}
+ {isClickable ? (
+ {
+ if (
+ event.button !== 0 ||
+ event.metaKey ||
+ event.ctrlKey ||
+ event.shiftKey ||
+ event.altKey
+ ) {
+ return;
+ }
+ event.preventDefault();
+ event.stopPropagation();
+ onFolderNavigate(targetId);
+ }}
+ >
+ {label}
+
+ ) : (
+ {label}
+ )}
+
+ );
+ })}
+
+ );
+ }, [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 = ({
{titleError ?
{titleError}
: null}
-
- Uploaded:{' '}
- {singleDoc.uploaded_at ? new Date(singleDoc.uploaded_at).toLocaleString() : '—'}
-
-
- Size:{' '}
- {sizeLabel}
-
-
- Type: {singleDoc.content_type || 'Unknown'}
-
-
- Issued: {issuedAt}
-
- {hasPageCount ? (
-
- Pages: {pageCountValue}
-
- ) : null}
- {singleFolderPath?.length ? (
-
- ) : null}
-
- Original filename:{' '}
- {singleDoc.original_name}
-
+ {detailInfoRows.map((row) => {
+ const rawValue = row.value;
+ const displayValue =
+ rawValue === null || rawValue === undefined || rawValue === '' ? '—' : rawValue;
+ return (
+
+ {row.label}:{' '}
+ {displayValue}
+
+ );
+ })}
1;
- const showOcrAction = Boolean(singleDoc && hasOcrAsset);
+ const showOcrAction = Boolean(singleDoc && singleHasOcr);
return (
<>