feat: Introduce PreviewContext and document a new spatial workspace architecture.

This commit is contained in:
2025-11-27 01:14:18 +01:00
parent 9ba581f64e
commit 82243b4170
6 changed files with 131 additions and 144 deletions
+14 -52
View File
@@ -1,9 +1,7 @@
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import type { ReactNode } from 'react';
import { useNavigate } from 'react-router-dom';
@@ -14,11 +12,11 @@ import {
IconX,
WindowMaximizeIcon,
} from '../ui/icons';
import PreviewZoomOverlay from '../detail/PreviewZoomOverlay';
import {
buildCorrespondentOptions,
sortCorrespondents,
} from '../documents/DocumentSummarySection';
import { usePreviewContext } from '../preview/PreviewContext';
import type { DocumentSummarySectionProps } from '../documents/DocumentSummarySection';
import { extractDocumentMetadataPayload } from '../documents/documentSummary';
import { createDocumentActionState } from '../documents/documentActions';
@@ -233,7 +231,17 @@ const DocumentViewerPanel: React.FC<DocumentViewerPanelProps> = ({
[hasOcr, loadOcrContent],
);
const [zoomOverlayOpen, setZoomOverlayOpen] = useState(false);
const { openPreview } = usePreviewContext();
const handleZoomOpen = useCallback(() => {
if (!document) {
return;
}
openPreview(document);
}, [document, openPreview]);
const panelRef = useRef<HTMLDivElement | HTMLFormElement | HTMLElement | null>(null);
const isStackedLayout = useViewerLayoutMode(panelRef, document?.id);
const resolvedDocumentLink = useMemo(() => {
if (!document) {
@@ -253,24 +261,6 @@ const DocumentViewerPanel: React.FC<DocumentViewerPanelProps> = ({
};
}, [document]);
const handleZoomOpen = useCallback(() => {
if (!resolvedDocumentLink?.url) {
return;
}
setZoomOverlayOpen(true);
}, [resolvedDocumentLink?.url]);
const handleZoomClose = useCallback(() => {
setZoomOverlayOpen(false);
}, []);
useEffect(() => {
setZoomOverlayOpen(false);
}, [resolvedDocumentLink?.url, document?.id]);
const panelRef = useRef<HTMLDivElement | HTMLFormElement | HTMLElement | null>(null);
const isStackedLayout = useViewerLayoutMode(panelRef, document?.id);
const {
panelStyle: managedDetailPanelStyle,
handleProps: managedResizeHandleProps,
@@ -333,23 +323,11 @@ const DocumentViewerPanel: React.FC<DocumentViewerPanelProps> = ({
}));
}, [breadcrumbs, navigateToFolder]);
const zoomDisplay = useMemo(() => {
if (!resolvedDocumentLink?.url || !document) {
return null;
}
const normalizedMimeType = document.mime_type;
return {
url: resolvedDocumentLink.url,
alt: document.title,
mimeType: normalizedMimeType,
};
}, [document, resolvedDocumentLink?.url]);
const headerActions = createDocumentViewerHeaderActions({
document,
actionState,
onZoom: zoomDisplay ? handleZoomOpen : null,
canZoom: Boolean(zoomDisplay),
onZoom: handleZoomOpen,
canZoom: Boolean(document),
});
const collapseButton = isSidebarVariant && onCollapsePanel
@@ -469,20 +447,6 @@ const DocumentViewerPanel: React.FC<DocumentViewerPanelProps> = ({
document?.title || 'Document preview'
);
const overlayDocument = useMemo(() => (
document && zoomDisplay?.url
? { ...document, documentLink: zoomDisplay }
: document
), [document, zoomDisplay]);
const overlay = (
<PreviewZoomOverlay
open={Boolean(zoomOverlayOpen && zoomDisplay)}
document={overlayDocument}
onClose={handleZoomClose}
/>
);
if (isSidebarVariant) {
const sidebarClass = `detail-panel panel${sidebarMode === 'inline' ? ' detail-panel--inline' : ''}${isPanelResizing ? ' detail-panel--resizing' : ''}`;
return (
@@ -501,7 +465,6 @@ const DocumentViewerPanel: React.FC<DocumentViewerPanelProps> = ({
/>
<div className="panel-body detail-panel__content">{viewerSection}</div>
</aside>
{overlay}
</>
);
}
@@ -517,7 +480,6 @@ const DocumentViewerPanel: React.FC<DocumentViewerPanelProps> = ({
/>
{viewerSection}
</section>
{overlay}
</>
);
};
+94
View File
@@ -0,0 +1,94 @@
import React, { createContext, useContext, useState, useCallback, useMemo, useEffect } from 'react';
import PreviewZoomOverlay from '../detail/PreviewZoomOverlay';
import type { Document } from '../types/documents';
import type { Identifier } from '../types/identifiers';
interface PreviewContextType {
openPreview: (doc: Document) => void;
closePreview: () => void;
}
const PreviewContext = createContext<PreviewContextType | null>(null);
export const usePreviewContext = () => {
const context = useContext(PreviewContext);
if (!context) {
throw new Error('usePreviewContext must be used within a PreviewProvider');
}
return context;
};
interface PreviewProviderProps {
children: React.ReactNode;
ensureDownloadUrl?: (docId: Identifier) => Promise<any>;
}
export const PreviewProvider: React.FC<PreviewProviderProps> = ({ children, ensureDownloadUrl }) => {
const [previewDoc, setPreviewDoc] = useState<Document | null>(null);
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
const openPreview = useCallback((doc: Document) => {
setPreviewDoc(doc);
}, []);
const closePreview = useCallback(() => {
setPreviewDoc(null);
setPreviewUrl(null);
}, []);
useEffect(() => {
if (!previewDoc) {
setPreviewUrl(null);
return;
}
let cancelled = false;
if (ensureDownloadUrl) {
ensureDownloadUrl(previewDoc.id)
.then((entry) => {
if (!cancelled && entry?.url) {
setPreviewUrl(entry.url);
}
})
.catch(() => {
if (!cancelled) {
setPreviewUrl(null);
}
});
}
return () => {
cancelled = true;
};
}, [previewDoc, ensureDownloadUrl]);
const overlayDocument = useMemo(() => {
if (!previewDoc || !previewUrl) {
return previewDoc;
}
return {
...previewDoc,
documentLink: {
url: previewUrl,
alt: previewDoc.title,
mimeType: previewDoc.mime_type,
},
};
}, [previewDoc, previewUrl]);
const value = useMemo(() => ({
openPreview,
closePreview,
}), [openPreview, closePreview]);
return (
<PreviewContext.Provider value={value}>
{children}
<PreviewZoomOverlay
open={Boolean(previewUrl)}
onClose={closePreview}
document={overlayDocument}
/>
</PreviewContext.Provider>
);
};