From 2dd786ce147b10aab0ddafa407c0a370f0acfdec Mon Sep 17 00:00:00 2001 From: Nils Schneider Date: Tue, 11 Nov 2025 10:50:57 +0100 Subject: [PATCH] hydration --- frontend/src/detail/DetailPanel.jsx | 436 ++---------------- frontend/src/detail/useDetailWorkspace.js | 3 - frontend/src/preview/DocumentViewerLayout.jsx | 105 +++++ frontend/src/preview/DocumentViewerPanel.jsx | 91 +--- 4 files changed, 160 insertions(+), 475 deletions(-) create mode 100644 frontend/src/preview/DocumentViewerLayout.jsx diff --git a/frontend/src/detail/DetailPanel.jsx b/frontend/src/detail/DetailPanel.jsx index 5d4acd4..ea3752a 100644 --- a/frontend/src/detail/DetailPanel.jsx +++ b/frontend/src/detail/DetailPanel.jsx @@ -1,123 +1,17 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo } from 'react'; import { DownloadIcon, - ArrowLeftIcon, - ArrowRightIcon, DetailPanelCollapseIcon, WindowMaximizeIcon, } from '../ui/icons'; import PanelHeader from '../ui/PanelHeader'; -import { resolveDocumentAssetUrl, createAssetView } from '../asset_manager'; -import { useAssetNavigator } from '../hooks/useAssetNavigator'; +import { resolveDocumentAssetUrl } from '../asset_manager'; import { describeDocumentSummary } from '../documents/documentSummary'; import { createDocumentActionState } from '../documents/documentActions'; -import PreviewZoomOverlay from './PreviewZoomOverlay'; -import DocumentInfoPanel from '../documents/DocumentInfoPanel'; import { sortCorrespondents, buildCorrespondentOptions } from '../documents/DocumentSummarySection'; import BreadcrumbTrail from '../ui/BreadcrumbTrail'; - -const derivePreviewOrientation = (metadata) => { - const width = Number(metadata?.width); - const height = Number(metadata?.height); - if (Number.isFinite(width) && Number.isFinite(height) && width > 0 && height > 0) { - return width >= height ? 'landscape' : 'portrait'; - } - return 'landscape'; -}; - -const PreviewImage = ({ - item, - emptyMessage = 'Preview unavailable', - emptyContent = null, - onActivate, - onOpenPreview, - onZoomPreview, - showNav = false, - canGoPrev = false, - canGoNext = false, - onGoPrev = null, - onGoNext = null, -}) => { - if (!item) { - return ( -
- {emptyContent || {emptyMessage}} -
- ); - } - - const handleActivate = (event) => { - event.stopPropagation(); - if (onZoomPreview) { - onZoomPreview(item); - } else if (onOpenPreview) { - onOpenPreview(item.id); - } else if (onActivate) { - onActivate(item.id); - } - }; - - const interceptNavPointer = (event) => { - event.preventDefault(); - event.stopPropagation(); - }; - - return ( -
- {item.alt} { - if (event.key === 'Enter' || event.key === ' ') { - event.preventDefault(); - event.stopPropagation(); - handleActivate(event); - } - }} - /> - {showNav ? ( -
- - -
- ) : null} -
- ); -}; +import { extractDocumentMetadataPayload } from '../documents/documentMetadata'; +import DocumentViewerLayout from '../preview/DocumentViewerLayout'; const DetailPanel = ({ document = null, @@ -126,7 +20,6 @@ const DetailPanel = ({ onTagAdd, onTagRemove, onOpenPreview, - onPromoteSelection, onUpdateTitle = async () => false, onUpdateIssued = async () => false, ensureAssetUrl = null, @@ -138,11 +31,10 @@ const DetailPanel = ({ resolveApiPath, onFolderNavigate = null, resolveFolderPath = null, + previewEntry = null, onClose = () => {}, }) => { const singleDoc = document || null; - const singleDocId = singleDoc?.id || null; - const selectionKey = singleDocId || 'none'; const { downloadHref: singleDownloadHref } = useMemo( () => @@ -192,81 +84,6 @@ const DetailPanel = ({ ]; }, [singleDoc, resolveFolderPath, detailSummary, onFolderNavigate]); - const [zoomedPreview, setZoomedPreview] = useState(null); - - useEffect(() => { - setZoomedPreview(null); - }, [selectionKey]); - - const handlePreviewActivate = useCallback( - (docId) => { - if (!docId) return; - onPromoteSelection?.(docId); - }, - [onPromoteSelection], - ); - - const singlePreviewNavigator = useAssetNavigator({ - document: singleDoc, - assetType: 'preview', - ensureAssetUrl, - getAsset: getDocumentAsset, - prefetch: 3, - }); - - const makePreviewItem = useCallback( - (doc, ordinal = 1) => { - if (!doc) return null; - const asset = getDocumentAsset(doc, 'preview'); - const assetView = createAssetView(asset); - const object = assetView.getObject(ordinal); - let url = object?.url || null; - if (!url) { - url = resolveDocumentAssetUrl(doc, 'preview', { - ensureAssetUrl, - getAsset: getDocumentAsset, - ensureOptions: { start: ordinal, limit: 1 }, - objectOrdinal: ordinal, - }); - } - if (!url) { - return null; - } - const metadata = object?.metadata || assetView.getPrimaryMetadata() || {}; - const orientation = derivePreviewOrientation(metadata); - return { - id: doc.id, - url, - orientation, - alt: doc.title, - }; - }, - [ensureAssetUrl, getDocumentAsset], - ); - - const singlePreviewItem = useMemo(() => { - if (!singleDoc) return null; - const url = singlePreviewNavigator.currentUrl; - if (url) { - return { - id: singleDoc.id, - url, - orientation: derivePreviewOrientation(singlePreviewNavigator.currentMetadata), - alt: singleDoc.title, - }; - } - return makePreviewItem(singleDoc, 1); - }, [ - singleDoc, - singlePreviewNavigator.currentUrl, - singlePreviewNavigator.currentMetadata, - makePreviewItem, - ]); - - const singleCardinality = singlePreviewNavigator.cardinality; - const singleEffectiveCardinality = singleCardinality || (singlePreviewNavigator.currentUrl ? 1 : 0); - const singleHasPreview = Boolean(singlePreviewNavigator.currentUrl); - const correspondentOptions = useMemo( () => buildCorrespondentOptions(Array.isArray(correspondents) ? correspondents : []), [correspondents], @@ -277,6 +94,12 @@ const DetailPanel = ({ return sortCorrespondents(singleDoc.correspondents || []); }, [singleDoc]); + useEffect(() => { + if (typeof ensurePreviewData === 'function' && singleDoc?.id) { + ensurePreviewData(singleDoc.id); + } + }, [ensurePreviewData, singleDoc?.id]); + const singleSummaryProps = useMemo( () => ({ tagLookupById, @@ -304,6 +127,11 @@ const DetailPanel = ({ ], ); + const singleMetadataPayload = useMemo( + () => extractDocumentMetadataPayload(singleDoc), + [singleDoc], + ); + const singleHasOcr = useMemo(() => { if (!singleDoc || typeof getDocumentAsset !== 'function') { return false; @@ -365,183 +193,22 @@ const DetailPanel = ({ [singleHasOcr, loadSingleOcrContent], ); - const openZoomPreview = useCallback((docId) => { - if (!docId) return; - setZoomedPreview({ docId }); - }, []); - - const closeZoomPreview = useCallback(() => { - setZoomedPreview(null); - }, []); - - const handleSingleZoom = useCallback( - (entry) => { - if (!singleHasPreview) return; - const targetId = entry?.id ?? singleDocId; - if (!targetId) return; - openZoomPreview(targetId); - }, - [openZoomPreview, singleHasPreview, singleDocId], - ); - - const zoomDisplay = useMemo(() => { - if ( - !zoomedPreview - || !singleDoc - || !singleDocId - || zoomedPreview.docId !== singleDocId - || !singleHasPreview - ) { - return null; - } - - return { - url: singlePreviewNavigator.currentUrl, - alt: singleDoc.title, - canGoPrev: - singleEffectiveCardinality > 1 && Boolean(singlePreviewNavigator.canGoPrev), - canGoNext: - singleEffectiveCardinality > 1 && Boolean(singlePreviewNavigator.canGoNext), - goPrev: singlePreviewNavigator.goPrev, - goNext: singlePreviewNavigator.goNext, - }; - }, [ - zoomedPreview, - singleDoc, - singleDocId, - singleHasPreview, - singlePreviewNavigator.currentUrl, - singlePreviewNavigator.canGoPrev, - singlePreviewNavigator.canGoNext, - singlePreviewNavigator.goPrev, - singlePreviewNavigator.goNext, - singleEffectiveCardinality, - ]); - - useEffect(() => { - if (zoomedPreview && !zoomDisplay) { - setZoomedPreview(null); - } - }, [zoomedPreview, zoomDisplay]); - - const { - documentId: singleNavigatorDocId, - asset: singleNavigatorAsset, - ordinal: singleNavigatorOrdinal, - canGoPrev: singleNavigatorCanGoPrev, - canGoNext: singleNavigatorCanGoNext, - cardinality: singleNavigatorCardinality, - } = singlePreviewNavigator; - - useEffect(() => { - if (typeof ensureAssetUrl !== 'function') { - return; - } - if (!singleNavigatorDocId || !singleNavigatorAsset || !Number.isFinite(singleNavigatorOrdinal)) { - return; - } - - const requests = []; - if (singleNavigatorCanGoPrev) { - const prevOrdinal = Math.max(1, singleNavigatorOrdinal - 1); - if (!singleNavigatorCardinality || prevOrdinal <= singleNavigatorCardinality) { - requests.push( - ensureAssetUrl(singleNavigatorDocId, singleNavigatorAsset, { - start: prevOrdinal, - limit: 1, - objectOrdinal: prevOrdinal, - }), - ); - } - } - if (singleNavigatorCanGoNext) { - const nextOrdinal = singleNavigatorOrdinal + 1; - if (!singleNavigatorCardinality || nextOrdinal <= singleNavigatorCardinality) { - requests.push( - ensureAssetUrl(singleNavigatorDocId, singleNavigatorAsset, { - start: nextOrdinal, - limit: 1, - objectOrdinal: nextOrdinal, - }), - ); - } - } - - requests.forEach((promise) => promise?.catch?.(() => {})); - }, [ - ensureAssetUrl, - singleNavigatorDocId, - singleNavigatorAsset, - singleNavigatorOrdinal, - singleNavigatorCanGoPrev, - singleNavigatorCanGoNext, - singleNavigatorCardinality, - ]); - const renderContent = () => { if (!singleDoc) { return

Select a document to view metadata, tags and actions.

; } - const effectiveCardinality = singleEffectiveCardinality; - const navCanGoPrev = Boolean(singlePreviewNavigator.canGoPrev); - const navCanGoNext = Boolean(singlePreviewNavigator.canGoNext); - const hasPreviewImage = Boolean(singlePreviewNavigator.currentUrl); - const previewMissingAsset = !singlePreviewNavigator.currentUrl; - const displayContentType = singleDoc.content_type || 'this file type'; - const displayFilename = - singleDoc.filename || singleDoc.original_name || singleDoc.title || 'download'; - const previewFallback = previewMissingAsset ? ( -
-
- Preview not available for {displayContentType} files. -
-
{displayFilename}
- {singleDownloadHref ? ( - - - Download - - ) : null} -
- ) : null; - const emptyMessage = previewMissingAsset ? 'Preview unavailable' : 'Preview loading…'; - const showNav = hasPreviewImage && (effectiveCardinality > 1 || navCanGoPrev || navCanGoNext); - return ( -
-
- -
-
- -
-
+
+ +
); }; @@ -598,37 +265,26 @@ const DetailPanel = ({ } return ( - <> - - {singleDoc && ( - - )} - + ); }; diff --git a/frontend/src/detail/useDetailWorkspace.js b/frontend/src/detail/useDetailWorkspace.js index 4c74455..27859c6 100644 --- a/frontend/src/detail/useDetailWorkspace.js +++ b/frontend/src/detail/useDetailWorkspace.js @@ -22,7 +22,6 @@ const useDetailWorkspace = ({ previewDocumentId, activePreviewId, openDocumentPreview, - promoteSelectionOrder, handleDocumentTitleUpdate, handleDocumentIssuedUpdate, handleDocumentTagAdd, @@ -242,7 +241,6 @@ const useDetailWorkspace = ({ onTagRemove: handleTagRemove, previewEntry: selectedPreviewEntry, onOpenPreview: openDocumentPreview, - onPromoteSelection: promoteSelectionOrder, activePreviewId, onUpdateTitle: handleDocumentTitleUpdate, onUpdateIssued: handleDocumentIssuedUpdate, @@ -272,7 +270,6 @@ const useDetailWorkspace = ({ handleDocumentTitleUpdate, handleTagRemove, openDocumentPreview, - promoteSelectionOrder, resolveApiPath, resolveFolderPath, selectFolder, diff --git a/frontend/src/preview/DocumentViewerLayout.jsx b/frontend/src/preview/DocumentViewerLayout.jsx new file mode 100644 index 0000000..964e5ea --- /dev/null +++ b/frontend/src/preview/DocumentViewerLayout.jsx @@ -0,0 +1,105 @@ +import React, { useMemo } from 'react'; +import DocumentInfoPanel from '../documents/DocumentInfoPanel'; +import { DownloadIcon } from '../ui/icons'; + +const DocumentViewerLayout = ({ + document, + previewEntry, + summaryProps, + metadataPayload, + contentTabConfig, + resetKey, + classNamePrefix = 'document-viewer', + defaultTabId = 'details', + infoPanelProps = {}, + previewLoadingMessage = 'Preparing preview…', +}) => { + const previewContent = useMemo(() => { + if (!document || !previewEntry?.url) { + return null; + } + + const normalizedContentType = (previewEntry.contentType + || document.content_type + || '') + .toLowerCase(); + const isImage = normalizedContentType.startsWith('image/'); + const isPdf = normalizedContentType === 'application/pdf' + || normalizedContentType === 'application/x-pdf'; + + if (isImage) { + return ( + {`Preview + ); + } + + if (isPdf) { + return ( +