feat: Introduce PreviewContext and document a new spatial workspace architecture.
This commit is contained in:
@@ -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 (
|
||||
<DocumentsFilterProvider value={documentsFilter}>
|
||||
{content}
|
||||
<PreviewProvider ensureDownloadUrl={surfaceConfig.documentsTableProps?.ensureDownloadUrl}>
|
||||
{content}
|
||||
</PreviewProvider>
|
||||
</DocumentsFilterProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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<DesktopWorkspaceProps> = ({
|
||||
onDocumentTagDrop,
|
||||
tenantId,
|
||||
viewId,
|
||||
onPreview,
|
||||
}) => {
|
||||
const { openPreview } = usePreviewContext();
|
||||
const { addPointer, removePointer } = usePointerTracking();
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [isLayoutReady, setIsLayoutReady] = useState(false);
|
||||
@@ -207,16 +207,16 @@ const DesktopWorkspaceContent: React.FC<DesktopWorkspaceProps> = ({
|
||||
// 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 (
|
||||
<>
|
||||
|
||||
@@ -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 = <CProps extends { clearSelection: () => void; chil
|
||||
containerProps,
|
||||
...props
|
||||
}: AbstractDocumentsViewProps<CProps>) => {
|
||||
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 = <CProps extends { clearSelection: () => void; chil
|
||||
const { handleKeyDown, handleFocus } = useDocumentsNavigation({
|
||||
entries,
|
||||
onFolderSelect,
|
||||
onPreview,
|
||||
onPreview: openPreview,
|
||||
});
|
||||
const { clearSelection } = viewLogic;
|
||||
|
||||
@@ -80,6 +82,7 @@ const AbstractDocumentsView = <CProps extends { clearSelection: () => void; chil
|
||||
entry={entry}
|
||||
viewLogic={viewLogic}
|
||||
{...props}
|
||||
onPreview={openPreview}
|
||||
/>
|
||||
))}
|
||||
</ContainerComponent>
|
||||
|
||||
@@ -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<DocumentsPanelInnerProps> = ({
|
||||
isSearchLoading = false,
|
||||
viewMode = 'list',
|
||||
onViewModeChange,
|
||||
ensureDownloadUrl,
|
||||
onRefresh = () => { },
|
||||
sortField,
|
||||
sortDirection,
|
||||
@@ -317,73 +316,7 @@ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = ({
|
||||
const isDeskView = viewMode === 'desk';
|
||||
const gridIconSize = DEFAULT_GRID_ICON_SIZE;
|
||||
|
||||
const [previewDoc, setPreviewDoc] = useState<Document | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (previewDoc && !rows.find(d => d.id === previewDoc.id)) {
|
||||
setPreviewDoc(null);
|
||||
}
|
||||
}, [previewDoc, rows]);
|
||||
|
||||
const [previewUrl, setPreviewUrl] = useState<string | null>(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<DocumentsPanelInnerProps> = ({
|
||||
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<DocumentsPanelInnerProps> = ({
|
||||
onCorrespondentClick: toggleCorrespondentFilter,
|
||||
tenantId: currentTenantId,
|
||||
viewId,
|
||||
onPreview: handleDocumentPreviewZoom,
|
||||
};
|
||||
|
||||
const renderBody = () => {
|
||||
@@ -578,11 +508,6 @@ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = ({
|
||||
>
|
||||
{renderBody()}
|
||||
</section>
|
||||
<PreviewZoomOverlay
|
||||
open={Boolean(previewUrl)}
|
||||
onClose={closePreviewOverlay}
|
||||
document={overlayDocument}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user