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 {
|
import {
|
||||||
DocumentsFilterProvider,
|
DocumentsFilterProvider,
|
||||||
} from '../documents/context/DocumentsFilterContext';
|
} from '../documents/context/DocumentsFilterContext';
|
||||||
|
import { PreviewProvider } from '../preview/PreviewContext';
|
||||||
import { useWorkspaceSurface } from './useWorkspaceSurface';
|
import { useWorkspaceSurface } from './useWorkspaceSurface';
|
||||||
import { DocumentsHeaderBreadcrumb } from '../documents/panel/DocumentsPanelHeader';
|
import { DocumentsHeaderBreadcrumb } from '../documents/panel/DocumentsPanelHeader';
|
||||||
import { SidebarProvider, useSidebarContext } from '../sidebar/SidebarContext';
|
import { SidebarProvider, useSidebarContext } from '../sidebar/SidebarContext';
|
||||||
@@ -75,7 +76,9 @@ const DocumentsRouteContent: React.FC = () => {
|
|||||||
const content = renderSurface();
|
const content = renderSurface();
|
||||||
return (
|
return (
|
||||||
<DocumentsFilterProvider value={documentsFilter}>
|
<DocumentsFilterProvider value={documentsFilter}>
|
||||||
|
<PreviewProvider ensureDownloadUrl={surfaceConfig.documentsTableProps?.ensureDownloadUrl}>
|
||||||
{content}
|
{content}
|
||||||
|
</PreviewProvider>
|
||||||
</DocumentsFilterProvider>
|
</DocumentsFilterProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { createDocumentEntryKey } from '../app/entryKey';
|
|||||||
import { PointerTrackingProvider, usePointerTracking } from './PointerTrackingContext';
|
import { PointerTrackingProvider, usePointerTracking } from './PointerTrackingContext';
|
||||||
import type { Identifier } from '../types/identifiers';
|
import type { Identifier } from '../types/identifiers';
|
||||||
import type { DocumentsListEntry, Document } from '../types/documents';
|
import type { DocumentsListEntry, Document } from '../types/documents';
|
||||||
|
import { usePreviewContext } from '../preview/PreviewContext';
|
||||||
|
|
||||||
type TagLike = { id?: Identifier | null; label?: string; color?: string | null } | null;
|
type TagLike = { id?: Identifier | null; label?: string; color?: string | null } | null;
|
||||||
type OverlaySource = { url: string; alt?: string | null; mimeType?: string | null; };
|
type OverlaySource = { url: string; alt?: string | null; mimeType?: string | null; };
|
||||||
@@ -50,7 +51,6 @@ export interface DesktopWorkspaceProps {
|
|||||||
onDocumentTagDrop?: (docId: Identifier, tag: any) => void;
|
onDocumentTagDrop?: (docId: Identifier, tag: any) => void;
|
||||||
tenantId?: Identifier | null;
|
tenantId?: Identifier | null;
|
||||||
viewId?: string | null;
|
viewId?: string | null;
|
||||||
onPreview?: (doc: DeskDocument) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wrapper to handle hooks per card
|
// Wrapper to handle hooks per card
|
||||||
@@ -84,8 +84,8 @@ const DesktopWorkspaceContent: React.FC<DesktopWorkspaceProps> = ({
|
|||||||
onDocumentTagDrop,
|
onDocumentTagDrop,
|
||||||
tenantId,
|
tenantId,
|
||||||
viewId,
|
viewId,
|
||||||
onPreview,
|
|
||||||
}) => {
|
}) => {
|
||||||
|
const { openPreview } = usePreviewContext();
|
||||||
const { addPointer, removePointer } = usePointerTracking();
|
const { addPointer, removePointer } = usePointerTracking();
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const [isLayoutReady, setIsLayoutReady] = useState(false);
|
const [isLayoutReady, setIsLayoutReady] = useState(false);
|
||||||
@@ -207,16 +207,16 @@ const DesktopWorkspaceContent: React.FC<DesktopWorkspaceProps> = ({
|
|||||||
// Preview the last selected document
|
// Preview the last selected document
|
||||||
const lastId = selectedDocumentIds[selectedDocumentIds.length - 1];
|
const lastId = selectedDocumentIds[selectedDocumentIds.length - 1];
|
||||||
const doc = items.find(i => String(i.id) === lastId);
|
const doc = items.find(i => String(i.id) === lastId);
|
||||||
if (doc && onPreview) {
|
if (doc) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
onPreview(doc);
|
openPreview(doc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('keydown', handleWindowKeyDown);
|
window.addEventListener('keydown', handleWindowKeyDown);
|
||||||
return () => window.removeEventListener('keydown', handleWindowKeyDown);
|
return () => window.removeEventListener('keydown', handleWindowKeyDown);
|
||||||
}, [selectedDocumentIds, items, onPreview]);
|
}, [selectedDocumentIds, items, openPreview]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, { useEffect, useCallback } from 'react';
|
|||||||
import { useDocumentViewLogic, DocumentViewLogic } from './hooks/useDocumentViewLogic';
|
import { useDocumentViewLogic, DocumentViewLogic } from './hooks/useDocumentViewLogic';
|
||||||
import { useDocumentsNavigation } from './hooks/useDocumentsNavigation';
|
import { useDocumentsNavigation } from './hooks/useDocumentsNavigation';
|
||||||
import { useWorkspaceSelectionContext } from '../app/WorkspaceSelectionContext';
|
import { useWorkspaceSelectionContext } from '../app/WorkspaceSelectionContext';
|
||||||
|
import { usePreviewContext } from '../preview/PreviewContext';
|
||||||
import DocumentsListRow from './components/DocumentsListRow';
|
import DocumentsListRow from './components/DocumentsListRow';
|
||||||
import DocumentsGridCard from './components/DocumentsGridCard';
|
import DocumentsGridCard from './components/DocumentsGridCard';
|
||||||
import DocumentsListContainer from './components/DocumentsListContainer';
|
import DocumentsListContainer from './components/DocumentsListContainer';
|
||||||
@@ -21,7 +22,8 @@ const AbstractDocumentsView = <CProps extends { clearSelection: () => void; chil
|
|||||||
containerProps,
|
containerProps,
|
||||||
...props
|
...props
|
||||||
}: AbstractDocumentsViewProps<CProps>) => {
|
}: 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({
|
const viewLogic = useDocumentViewLogic({
|
||||||
onDocumentRename,
|
onDocumentRename,
|
||||||
onFolderRename,
|
onFolderRename,
|
||||||
@@ -29,7 +31,7 @@ const AbstractDocumentsView = <CProps extends { clearSelection: () => void; chil
|
|||||||
const { handleKeyDown, handleFocus } = useDocumentsNavigation({
|
const { handleKeyDown, handleFocus } = useDocumentsNavigation({
|
||||||
entries,
|
entries,
|
||||||
onFolderSelect,
|
onFolderSelect,
|
||||||
onPreview,
|
onPreview: openPreview,
|
||||||
});
|
});
|
||||||
const { clearSelection } = viewLogic;
|
const { clearSelection } = viewLogic;
|
||||||
|
|
||||||
@@ -80,6 +82,7 @@ const AbstractDocumentsView = <CProps extends { clearSelection: () => void; chil
|
|||||||
entry={entry}
|
entry={entry}
|
||||||
viewLogic={viewLogic}
|
viewLogic={viewLogic}
|
||||||
{...props}
|
{...props}
|
||||||
|
onPreview={openPreview}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</ContainerComponent>
|
</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 { DocumentsList, DocumentsGrid } from '../DocumentsView';
|
||||||
import type { DragEvent, ReactNode, RefObject } from 'react';
|
import type { DragEvent, ReactNode, RefObject } from 'react';
|
||||||
import type {
|
import type {
|
||||||
@@ -10,7 +10,7 @@ import type {
|
|||||||
} from '../../types/documents';
|
} from '../../types/documents';
|
||||||
import DesktopWorkspace from '../../desktop/DesktopWorkspace';
|
import DesktopWorkspace from '../../desktop/DesktopWorkspace';
|
||||||
import { isTagTransferEvent } from '../tagTransfer';
|
import { isTagTransferEvent } from '../tagTransfer';
|
||||||
import PreviewZoomOverlay from '../../detail/PreviewZoomOverlay';
|
import { usePreviewContext } from '../../preview/PreviewContext';
|
||||||
import { isPointerModifierEvent, isPrimaryPointerEvent } from '../useEntryPointer';
|
import { isPointerModifierEvent, isPrimaryPointerEvent } from '../useEntryPointer';
|
||||||
import {
|
import {
|
||||||
WorkspaceSelectionProvider,
|
WorkspaceSelectionProvider,
|
||||||
@@ -109,7 +109,6 @@ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = ({
|
|||||||
isSearchLoading = false,
|
isSearchLoading = false,
|
||||||
viewMode = 'list',
|
viewMode = 'list',
|
||||||
onViewModeChange,
|
onViewModeChange,
|
||||||
ensureDownloadUrl,
|
|
||||||
onRefresh = () => { },
|
onRefresh = () => { },
|
||||||
sortField,
|
sortField,
|
||||||
sortDirection,
|
sortDirection,
|
||||||
@@ -317,73 +316,7 @@ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = ({
|
|||||||
const isDeskView = viewMode === 'desk';
|
const isDeskView = viewMode === 'desk';
|
||||||
const gridIconSize = DEFAULT_GRID_ICON_SIZE;
|
const gridIconSize = DEFAULT_GRID_ICON_SIZE;
|
||||||
|
|
||||||
const [previewDoc, setPreviewDoc] = useState<Document | null>(null);
|
const { openPreview } = usePreviewContext();
|
||||||
|
|
||||||
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 isTagDragEvent = useCallback((event) => isTagTransferEvent(event), []);
|
const isTagDragEvent = useCallback((event) => isTagTransferEvent(event), []);
|
||||||
|
|
||||||
@@ -431,17 +364,15 @@ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = ({
|
|||||||
if (!doc) {
|
if (!doc) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (event) {
|
if (event && (event.altKey || (event.button === 1))) {
|
||||||
event.preventDefault();
|
openPreview(doc);
|
||||||
event.stopPropagation();
|
|
||||||
}
|
|
||||||
if (event?.altKey) {
|
|
||||||
handleDocumentPreviewZoom(doc);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
onDocumentActivate?.(doc.id);
|
if (onDocumentActivate) {
|
||||||
|
onDocumentActivate(doc);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[handleDocumentPreviewZoom, onDocumentActivate],
|
[onDocumentActivate, openPreview],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleFolderClick = useCallback(
|
const handleFolderClick = useCallback(
|
||||||
@@ -521,7 +452,6 @@ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = ({
|
|||||||
onCorrespondentClick: toggleCorrespondentFilter,
|
onCorrespondentClick: toggleCorrespondentFilter,
|
||||||
tenantId: currentTenantId,
|
tenantId: currentTenantId,
|
||||||
viewId,
|
viewId,
|
||||||
onPreview: handleDocumentPreviewZoom,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderBody = () => {
|
const renderBody = () => {
|
||||||
@@ -578,11 +508,6 @@ const DocumentsPanelInner: React.FC<DocumentsPanelInnerProps> = ({
|
|||||||
>
|
>
|
||||||
{renderBody()}
|
{renderBody()}
|
||||||
</section>
|
</section>
|
||||||
<PreviewZoomOverlay
|
|
||||||
open={Boolean(previewUrl)}
|
|
||||||
onClose={closePreviewOverlay}
|
|
||||||
document={overlayDocument}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import React, {
|
import React, {
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
|
||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
@@ -14,11 +12,11 @@ import {
|
|||||||
IconX,
|
IconX,
|
||||||
WindowMaximizeIcon,
|
WindowMaximizeIcon,
|
||||||
} from '../ui/icons';
|
} from '../ui/icons';
|
||||||
import PreviewZoomOverlay from '../detail/PreviewZoomOverlay';
|
|
||||||
import {
|
import {
|
||||||
buildCorrespondentOptions,
|
buildCorrespondentOptions,
|
||||||
sortCorrespondents,
|
sortCorrespondents,
|
||||||
} from '../documents/DocumentSummarySection';
|
} from '../documents/DocumentSummarySection';
|
||||||
|
import { usePreviewContext } from '../preview/PreviewContext';
|
||||||
import type { DocumentSummarySectionProps } from '../documents/DocumentSummarySection';
|
import type { DocumentSummarySectionProps } from '../documents/DocumentSummarySection';
|
||||||
import { extractDocumentMetadataPayload } from '../documents/documentSummary';
|
import { extractDocumentMetadataPayload } from '../documents/documentSummary';
|
||||||
import { createDocumentActionState } from '../documents/documentActions';
|
import { createDocumentActionState } from '../documents/documentActions';
|
||||||
@@ -233,7 +231,17 @@ const DocumentViewerPanel: React.FC<DocumentViewerPanelProps> = ({
|
|||||||
[hasOcr, loadOcrContent],
|
[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(() => {
|
const resolvedDocumentLink = useMemo(() => {
|
||||||
if (!document) {
|
if (!document) {
|
||||||
@@ -253,24 +261,6 @@ const DocumentViewerPanel: React.FC<DocumentViewerPanelProps> = ({
|
|||||||
};
|
};
|
||||||
}, [document]);
|
}, [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 {
|
const {
|
||||||
panelStyle: managedDetailPanelStyle,
|
panelStyle: managedDetailPanelStyle,
|
||||||
handleProps: managedResizeHandleProps,
|
handleProps: managedResizeHandleProps,
|
||||||
@@ -333,23 +323,11 @@ const DocumentViewerPanel: React.FC<DocumentViewerPanelProps> = ({
|
|||||||
}));
|
}));
|
||||||
}, [breadcrumbs, navigateToFolder]);
|
}, [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({
|
const headerActions = createDocumentViewerHeaderActions({
|
||||||
document,
|
document,
|
||||||
actionState,
|
actionState,
|
||||||
onZoom: zoomDisplay ? handleZoomOpen : null,
|
onZoom: handleZoomOpen,
|
||||||
canZoom: Boolean(zoomDisplay),
|
canZoom: Boolean(document),
|
||||||
});
|
});
|
||||||
|
|
||||||
const collapseButton = isSidebarVariant && onCollapsePanel
|
const collapseButton = isSidebarVariant && onCollapsePanel
|
||||||
@@ -469,20 +447,6 @@ const DocumentViewerPanel: React.FC<DocumentViewerPanelProps> = ({
|
|||||||
document?.title || 'Document preview'
|
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) {
|
if (isSidebarVariant) {
|
||||||
const sidebarClass = `detail-panel panel${sidebarMode === 'inline' ? ' detail-panel--inline' : ''}${isPanelResizing ? ' detail-panel--resizing' : ''}`;
|
const sidebarClass = `detail-panel panel${sidebarMode === 'inline' ? ' detail-panel--inline' : ''}${isPanelResizing ? ' detail-panel--resizing' : ''}`;
|
||||||
return (
|
return (
|
||||||
@@ -501,7 +465,6 @@ const DocumentViewerPanel: React.FC<DocumentViewerPanelProps> = ({
|
|||||||
/>
|
/>
|
||||||
<div className="panel-body detail-panel__content">{viewerSection}</div>
|
<div className="panel-body detail-panel__content">{viewerSection}</div>
|
||||||
</aside>
|
</aside>
|
||||||
{overlay}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -517,7 +480,6 @@ const DocumentViewerPanel: React.FC<DocumentViewerPanelProps> = ({
|
|||||||
/>
|
/>
|
||||||
{viewerSection}
|
{viewerSection}
|
||||||
</section>
|
</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