diff --git a/frontend/src/app/DocumentsRoute.tsx b/frontend/src/app/DocumentsRoute.tsx index 70ba486..fc8ae29 100644 --- a/frontend/src/app/DocumentsRoute.tsx +++ b/frontend/src/app/DocumentsRoute.tsx @@ -4,6 +4,7 @@ import { useNavigate } from 'react-router-dom'; import { DocumentsFilterProvider, } from '../documents/context/DocumentsFilterContext'; +import { PreviewProvider } from '../preview/PreviewContext'; import { useWorkspaceSurface } from './useWorkspaceSurface'; import { DocumentsHeaderBreadcrumb } from '../documents/panel/DocumentsPanelHeader'; import { SidebarProvider, useSidebarContext } from '../sidebar/SidebarContext'; @@ -75,7 +76,9 @@ const DocumentsRouteContent: React.FC = () => { const content = renderSurface(); return ( - {content} + + {content} + ); }; diff --git a/frontend/src/desktop/DesktopWorkspace.tsx b/frontend/src/desktop/DesktopWorkspace.tsx index 4aad89b..6665a8a 100644 --- a/frontend/src/desktop/DesktopWorkspace.tsx +++ b/frontend/src/desktop/DesktopWorkspace.tsx @@ -18,6 +18,7 @@ import { createDocumentEntryKey } from '../app/entryKey'; import { PointerTrackingProvider, usePointerTracking } from './PointerTrackingContext'; import type { Identifier } from '../types/identifiers'; import type { DocumentsListEntry, Document } from '../types/documents'; +import { usePreviewContext } from '../preview/PreviewContext'; type TagLike = { id?: Identifier | null; label?: string; color?: string | null } | null; type OverlaySource = { url: string; alt?: string | null; mimeType?: string | null; }; @@ -50,7 +51,6 @@ export interface DesktopWorkspaceProps { onDocumentTagDrop?: (docId: Identifier, tag: any) => void; tenantId?: Identifier | null; viewId?: string | null; - onPreview?: (doc: DeskDocument) => void; } // Wrapper to handle hooks per card @@ -84,8 +84,8 @@ const DesktopWorkspaceContent: React.FC = ({ onDocumentTagDrop, tenantId, viewId, - onPreview, }) => { + const { openPreview } = usePreviewContext(); const { addPointer, removePointer } = usePointerTracking(); const containerRef = useRef(null); const [isLayoutReady, setIsLayoutReady] = useState(false); @@ -207,16 +207,16 @@ const DesktopWorkspaceContent: React.FC = ({ // Preview the last selected document const lastId = selectedDocumentIds[selectedDocumentIds.length - 1]; const doc = items.find(i => String(i.id) === lastId); - if (doc && onPreview) { + if (doc) { e.preventDefault(); - onPreview(doc); + openPreview(doc); } } }; window.addEventListener('keydown', handleWindowKeyDown); return () => window.removeEventListener('keydown', handleWindowKeyDown); - }, [selectedDocumentIds, items, onPreview]); + }, [selectedDocumentIds, items, openPreview]); return ( <> diff --git a/frontend/src/documents/DocumentsView.tsx b/frontend/src/documents/DocumentsView.tsx index f1e2732..bd86c19 100644 --- a/frontend/src/documents/DocumentsView.tsx +++ b/frontend/src/documents/DocumentsView.tsx @@ -2,6 +2,7 @@ import React, { useEffect, useCallback } from 'react'; import { useDocumentViewLogic, DocumentViewLogic } from './hooks/useDocumentViewLogic'; import { useDocumentsNavigation } from './hooks/useDocumentsNavigation'; import { useWorkspaceSelectionContext } from '../app/WorkspaceSelectionContext'; +import { usePreviewContext } from '../preview/PreviewContext'; import DocumentsListRow from './components/DocumentsListRow'; import DocumentsGridCard from './components/DocumentsGridCard'; import DocumentsListContainer from './components/DocumentsListContainer'; @@ -21,7 +22,8 @@ const AbstractDocumentsView = void; chil containerProps, ...props }: AbstractDocumentsViewProps) => { - const { entries, onDocumentRename, onFolderRename, onFolderSelect, onPreview, scrollRef, viewId } = props; + const { entries, onDocumentRename, onFolderRename, onFolderSelect, scrollRef, viewId } = props; + const { openPreview } = usePreviewContext(); const viewLogic = useDocumentViewLogic({ onDocumentRename, onFolderRename, @@ -29,7 +31,7 @@ const AbstractDocumentsView = void; chil const { handleKeyDown, handleFocus } = useDocumentsNavigation({ entries, onFolderSelect, - onPreview, + onPreview: openPreview, }); const { clearSelection } = viewLogic; @@ -80,6 +82,7 @@ const AbstractDocumentsView = void; chil entry={entry} viewLogic={viewLogic} {...props} + onPreview={openPreview} /> ))} diff --git a/frontend/src/documents/panel/DocumentsPanel.tsx b/frontend/src/documents/panel/DocumentsPanel.tsx index b94967d..4156e45 100644 --- a/frontend/src/documents/panel/DocumentsPanel.tsx +++ b/frontend/src/documents/panel/DocumentsPanel.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef } from 'react'; import { DocumentsList, DocumentsGrid } from '../DocumentsView'; import type { DragEvent, ReactNode, RefObject } from 'react'; import type { @@ -10,7 +10,7 @@ import type { } from '../../types/documents'; import DesktopWorkspace from '../../desktop/DesktopWorkspace'; import { isTagTransferEvent } from '../tagTransfer'; -import PreviewZoomOverlay from '../../detail/PreviewZoomOverlay'; +import { usePreviewContext } from '../../preview/PreviewContext'; import { isPointerModifierEvent, isPrimaryPointerEvent } from '../useEntryPointer'; import { WorkspaceSelectionProvider, @@ -109,7 +109,6 @@ const DocumentsPanelInner: React.FC = ({ isSearchLoading = false, viewMode = 'list', onViewModeChange, - ensureDownloadUrl, onRefresh = () => { }, sortField, sortDirection, @@ -317,73 +316,7 @@ const DocumentsPanelInner: React.FC = ({ const isDeskView = viewMode === 'desk'; const gridIconSize = DEFAULT_GRID_ICON_SIZE; - const [previewDoc, setPreviewDoc] = useState(null); - - useEffect(() => { - if (previewDoc && !rows.find(d => d.id === previewDoc.id)) { - setPreviewDoc(null); - } - }, [previewDoc, rows]); - - const [previewUrl, setPreviewUrl] = useState(null); - - const overlayDocument = useMemo(() => { - if (!previewDoc || !previewUrl) { - return previewDoc; - } - return { - ...previewDoc, - documentLink: { - url: previewUrl, - alt: previewDoc.title, - mimeType: previewDoc.mime_type, - }, - }; - }, [previewDoc, previewUrl]); - - 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 closePreviewOverlay = useCallback(() => { - setPreviewDoc(null); - setPreviewUrl(null); - }, []); - - const handleDocumentPreviewZoom = useCallback( - (doc) => { - if (!doc || !doc.id) { - return; - } - if (!ensureDownloadUrl) { - return; - } - setPreviewDoc(doc); - }, - [ensureDownloadUrl], - ); + const { openPreview } = usePreviewContext(); const isTagDragEvent = useCallback((event) => isTagTransferEvent(event), []); @@ -431,17 +364,15 @@ const DocumentsPanelInner: React.FC = ({ if (!doc) { return; } - if (event) { - event.preventDefault(); - event.stopPropagation(); - } - if (event?.altKey) { - handleDocumentPreviewZoom(doc); + if (event && (event.altKey || (event.button === 1))) { + openPreview(doc); return; } - onDocumentActivate?.(doc.id); + if (onDocumentActivate) { + onDocumentActivate(doc); + } }, - [handleDocumentPreviewZoom, onDocumentActivate], + [onDocumentActivate, openPreview], ); const handleFolderClick = useCallback( @@ -521,7 +452,6 @@ const DocumentsPanelInner: React.FC = ({ onCorrespondentClick: toggleCorrespondentFilter, tenantId: currentTenantId, viewId, - onPreview: handleDocumentPreviewZoom, }; const renderBody = () => { @@ -578,11 +508,6 @@ const DocumentsPanelInner: React.FC = ({ > {renderBody()} - ); }; diff --git a/frontend/src/preview/DocumentViewerPanel.tsx b/frontend/src/preview/DocumentViewerPanel.tsx index 2610f0f..139dff7 100644 --- a/frontend/src/preview/DocumentViewerPanel.tsx +++ b/frontend/src/preview/DocumentViewerPanel.tsx @@ -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 = ({ [hasOcr, loadOcrContent], ); - const [zoomOverlayOpen, setZoomOverlayOpen] = useState(false); + const { openPreview } = usePreviewContext(); + + const handleZoomOpen = useCallback(() => { + if (!document) { + return; + } + openPreview(document); + }, [document, openPreview]); + + const panelRef = useRef(null); + const isStackedLayout = useViewerLayoutMode(panelRef, document?.id); const resolvedDocumentLink = useMemo(() => { if (!document) { @@ -253,24 +261,6 @@ const DocumentViewerPanel: React.FC = ({ }; }, [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(null); - const isStackedLayout = useViewerLayoutMode(panelRef, document?.id); - const { panelStyle: managedDetailPanelStyle, handleProps: managedResizeHandleProps, @@ -333,23 +323,11 @@ const DocumentViewerPanel: React.FC = ({ })); }, [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 = ({ document?.title || 'Document preview' ); - const overlayDocument = useMemo(() => ( - document && zoomDisplay?.url - ? { ...document, documentLink: zoomDisplay } - : document - ), [document, zoomDisplay]); - - const overlay = ( - - ); - 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 = ({ />
{viewerSection}
- {overlay} ); } @@ -517,7 +480,6 @@ const DocumentViewerPanel: React.FC = ({ /> {viewerSection} - {overlay} ); }; diff --git a/frontend/src/preview/PreviewContext.tsx b/frontend/src/preview/PreviewContext.tsx new file mode 100644 index 0000000..ba662e2 --- /dev/null +++ b/frontend/src/preview/PreviewContext.tsx @@ -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(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; +} + +export const PreviewProvider: React.FC = ({ children, ensureDownloadUrl }) => { + const [previewDoc, setPreviewDoc] = useState(null); + const [previewUrl, setPreviewUrl] = useState(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 ( + + {children} + + + ); +};