feat: Replaced document preview with a new fullscreen viewer

This commit is contained in:
2025-12-11 00:44:11 +01:00
parent f6bd6e2feb
commit e8636ce7da
15 changed files with 177 additions and 173 deletions
+5 -5
View File
@@ -4,7 +4,7 @@ import { useNavigate } from 'react-router-dom';
import { import {
DocumentsFilterProvider, DocumentsFilterProvider,
} from '../documents/context/DocumentsFilterContext'; } from '../documents/context/DocumentsFilterContext';
import { PreviewProvider, usePreviewContext } from '../viewer/PreviewContext'; import { FullscreenPreviewProvider, useFullscreenPreviewContext } from '../viewer/FullscreenPreviewContext';
import { DocumentOpenProvider } from '../lib/context/DocumentOpenContext'; import { DocumentOpenProvider } from '../lib/context/DocumentOpenContext';
import { useWorkspaceSurface } from './useWorkspaceSurface'; import { useWorkspaceSurface } from './useWorkspaceSurface';
import { SidebarProvider, useSidebarContext } from '../sidebar/SidebarContext'; import { SidebarProvider, useSidebarContext } from '../sidebar/SidebarContext';
@@ -16,7 +16,7 @@ const DocumentsInner: React.FC<{
surfaceConfig: any; surfaceConfig: any;
onNavigate: (documentId: string) => void; onNavigate: (documentId: string) => void;
}> = ({ surfaceConfig, onNavigate }) => { }> = ({ surfaceConfig, onNavigate }) => {
const { openPreview } = usePreviewContext(); const { openFullscreenPreview } = useFullscreenPreviewContext();
const { collapsed: sidebarCollapsed } = useSidebarContext(); const { collapsed: sidebarCollapsed } = useSidebarContext();
const { const {
sidebarSuppressed, sidebarSuppressed,
@@ -68,7 +68,7 @@ const DocumentsInner: React.FC<{
return ( return (
<DocumentOpenProvider <DocumentOpenProvider
onOpenViewer={onNavigate} onOpenViewer={onNavigate}
onOpenPreview={openPreview} onOpenFullscreenPreview={openFullscreenPreview}
onOpenDetailPanel={openDetailPanel} onOpenDetailPanel={openDetailPanel}
> >
{renderSurface()} {renderSurface()}
@@ -89,14 +89,14 @@ const DocumentsRouteContent: React.FC = () => {
return ( return (
<DocumentsFilterProvider value={documentsFilter}> <DocumentsFilterProvider value={documentsFilter}>
<PreviewProvider <FullscreenPreviewProvider
onNavigate={handleDocumentNavigate} onNavigate={handleDocumentNavigate}
> >
<DocumentsInner <DocumentsInner
surfaceConfig={surfaceConfig} surfaceConfig={surfaceConfig}
onNavigate={handleDocumentNavigate} onNavigate={handleDocumentNavigate}
/> />
</PreviewProvider> </FullscreenPreviewProvider>
</DocumentsFilterProvider> </DocumentsFilterProvider>
); );
}; };
@@ -12,7 +12,7 @@ type FolderId = DocumentId | 'root';
import type { Document } from '../types/documents'; import type { Document } from '../types/documents';
import useNotifyApiError from '../hooks/useNotifyApiError'; import useNotifyApiError from '../hooks/useNotifyApiError';
interface UseDocumentPreviewArgs { interface UseDocumentViewerArgs {
routeDocumentId?: DocumentId | null; routeDocumentId?: DocumentId | null;
documentsManager: { documentsManager: {
getById: (id: DocumentId) => Document | null; getById: (id: DocumentId) => Document | null;
@@ -28,36 +28,36 @@ interface UseDocumentPreviewArgs {
open?: (args?: { documentIds?: DocumentId[] }) => void; open?: (args?: { documentIds?: DocumentId[] }) => void;
close?: () => void; close?: () => void;
} | null>; } | null>;
setActivePreviewId: Dispatch<SetStateAction<DocumentId | null>>; setActiveViewerId: Dispatch<SetStateAction<DocumentId | null>>;
} }
interface UseDocumentPreviewResult { interface UseDocumentViewerResult {
ensurePreviewData: (documentId: DocumentId) => Promise<Document | null>; ensureViewerData: (documentId: DocumentId) => Promise<Document | null>;
openDocumentPreview: (documentId: DocumentId, options?: { replace?: boolean }) => void; openDocumentViewer: (documentId: DocumentId, options?: { replace?: boolean }) => void;
closeDocumentPreview: (folderId?: FolderId) => void; closeDocumentViewer: (folderId?: FolderId) => void;
resetPreviewState: () => void; resetViewerState: () => void;
previewWorkspaceDocument: Document | null; viewerWorkspaceDocument: Document | null;
previewActive: boolean; viewerActive: boolean;
} }
const useDocumentPreview = ({ const useDocumentViewer = ({
routeDocumentId, routeDocumentId,
documentsManager, documentsManager,
selectedFolder, selectedFolder,
locationPathname, locationPathname,
locationSearch, locationSearch,
detailPanelControlRef, detailPanelControlRef,
setActivePreviewId, setActiveViewerId,
}: UseDocumentPreviewArgs): UseDocumentPreviewResult => { }: UseDocumentViewerArgs): UseDocumentViewerResult => {
const previewReturnPathRef = useRef<string | null>(null); const viewerReturnPathRef = useRef<string | null>(null);
const notifyApiError = useNotifyApiError(); const notifyApiError = useNotifyApiError();
const navigate = useNavigate(); const navigate = useNavigate();
const resetPreviewState = useCallback(() => { const resetViewerState = useCallback(() => {
previewReturnPathRef.current = null; viewerReturnPathRef.current = null;
}, []); }, []);
const ensurePreviewData = useCallback( const ensureViewerData = useCallback(
async (documentId: DocumentId): Promise<Document | null> => { async (documentId: DocumentId): Promise<Document | null> => {
if (!documentId) return null; if (!documentId) return null;
@@ -67,35 +67,35 @@ const useDocumentPreview = ({
throw new Error('Document metadata unavailable.'); throw new Error('Document metadata unavailable.');
} }
if (!previewReturnPathRef.current) { if (!viewerReturnPathRef.current) {
const fallbackFolderId = doc?.folder_id || 'root'; const fallbackFolderId = doc?.folder_id || 'root';
previewReturnPathRef.current = viewerReturnPathRef.current =
fallbackFolderId === 'root' ? '/documents' : `/documents/folder/${fallbackFolderId}`; fallbackFolderId === 'root' ? '/documents' : `/documents/folder/${fallbackFolderId}`;
} }
setActivePreviewId(documentId); setActiveViewerId(documentId);
return doc; return doc;
}, },
[ [
documentsManager, documentsManager,
setActivePreviewId, setActiveViewerId,
], ],
); );
const openDocumentPreview = useCallback( const openDocumentViewer = useCallback(
(documentId: DocumentId, { replace = false }: { replace?: boolean } = {}) => { (documentId: DocumentId, { replace = false }: { replace?: boolean } = {}) => {
if (!documentId) return; if (!documentId) return;
detailPanelControlRef.current?.close?.(); detailPanelControlRef.current?.close?.();
previewReturnPathRef.current = `${locationPathname}${locationSearch}`; viewerReturnPathRef.current = `${locationPathname}${locationSearch}`;
navigate(`/documents/${documentId}`, { replace }); navigate(`/documents/${documentId}`, { replace });
}, },
[navigate, locationPathname, locationSearch, detailPanelControlRef], [navigate, locationPathname, locationSearch, detailPanelControlRef],
); );
const closeDocumentPreview = useCallback( const closeDocumentViewer = useCallback(
(folderId?: FolderId) => { (folderId?: FolderId) => {
const fallbackPath = previewReturnPathRef.current; const fallbackPath = viewerReturnPathRef.current;
previewReturnPathRef.current = null; viewerReturnPathRef.current = null;
if (fallbackPath) { if (fallbackPath) {
navigate(fallbackPath, { replace: false }); navigate(fallbackPath, { replace: false });
@@ -116,36 +116,36 @@ const useDocumentPreview = ({
let cancelled = false; let cancelled = false;
ensurePreviewData(routeDocumentId).catch((error) => { ensureViewerData(routeDocumentId).catch((error) => {
if (cancelled) { if (cancelled) {
return; return;
} }
notifyApiError(error, 'Failed to open document preview.'); notifyApiError(error, 'Failed to open document preview.');
closeDocumentPreview(); closeDocumentViewer();
}); });
return () => { return () => {
cancelled = true; cancelled = true;
}; };
}, [routeDocumentId, ensurePreviewData, notifyApiError, closeDocumentPreview]); }, [routeDocumentId, ensureViewerData, notifyApiError, closeDocumentViewer]);
const previewWorkspaceDocument = useMemo(() => { const viewerWorkspaceDocument = useMemo(() => {
if (!routeDocumentId) { if (!routeDocumentId) {
return null; return null;
} }
return documentsManager.getById(routeDocumentId); return documentsManager.getById(routeDocumentId);
}, [routeDocumentId, documentsManager]); }, [routeDocumentId, documentsManager]);
const previewActive = Boolean(routeDocumentId && previewWorkspaceDocument); const viewerActive = Boolean(routeDocumentId && viewerWorkspaceDocument);
return { return {
ensurePreviewData, ensureViewerData,
openDocumentPreview, openDocumentViewer,
closeDocumentPreview, closeDocumentViewer,
resetPreviewState, resetViewerState,
previewWorkspaceDocument, viewerWorkspaceDocument,
previewActive, viewerActive,
}; };
}; };
export default useDocumentPreview; export default useDocumentViewer;
+4 -4
View File
@@ -30,10 +30,10 @@ const useDocumentsShell = (): DocumentsShellView => {
detailPanelOpen: Boolean(shell.detailPanel?.detailPanelOpen), detailPanelOpen: Boolean(shell.detailPanel?.detailPanelOpen),
openDetailPanel: shell.detailPanel?.openDetailPanel as WorkspaceSurfaceConfig['openDetailPanel'], openDetailPanel: shell.detailPanel?.openDetailPanel as WorkspaceSurfaceConfig['openDetailPanel'],
closeDetailPanel: shell.detailPanel?.closeDetailPanel as WorkspaceSurfaceConfig['closeDetailPanel'], closeDetailPanel: shell.detailPanel?.closeDetailPanel as WorkspaceSurfaceConfig['closeDetailPanel'],
previewWorkspaceDocument: shell.preview?.previewWorkspaceDocument, viewerWorkspaceDocument: shell.preview?.viewerWorkspaceDocument,
previewDocumentId: (shell.preview?.previewDocumentId as Identifier) ?? null, viewerDocumentId: (shell.preview?.viewerDocumentId as Identifier) ?? null,
closeDocumentPreview: shell.preview?.closeDocumentPreview as WorkspaceSurfaceConfig['closeDocumentPreview'], closeDocumentViewer: shell.preview?.closeDocumentViewer as WorkspaceSurfaceConfig['closeDocumentViewer'],
ensurePreviewData: shell.preview?.ensurePreviewData as WorkspaceSurfaceConfig['ensurePreviewData'], ensureViewerData: shell.preview?.ensureViewerData as WorkspaceSurfaceConfig['ensureViewerData'],
ensureAssetUrl: shell.preview?.ensureAssetUrl as WorkspaceSurfaceConfig['ensureAssetUrl'], ensureAssetUrl: shell.preview?.ensureAssetUrl as WorkspaceSurfaceConfig['ensureAssetUrl'],
getDocumentAsset: shell.preview?.getDocumentAsset as WorkspaceSurfaceConfig['getDocumentAsset'], getDocumentAsset: shell.preview?.getDocumentAsset as WorkspaceSurfaceConfig['getDocumentAsset'],
notifyApiError: shell.ui?.notifyApiError as WorkspaceSurfaceConfig['notifyApiError'], notifyApiError: shell.ui?.notifyApiError as WorkspaceSurfaceConfig['notifyApiError'],
+26 -26
View File
@@ -12,14 +12,14 @@ type EnsureAssetUrl = (
asset: unknown, asset: unknown,
options?: Record<string, unknown>, options?: Record<string, unknown>,
) => Promise<unknown> | void; ) => Promise<unknown> | void;
type EnsurePreviewData = (docId: Identifier, options?: Record<string, unknown>) => Promise<unknown>; type EnsureViewerData = (docId: Identifier, options?: Record<string, unknown>) => Promise<unknown>;
type GetDocumentAsset = (document: unknown, assetType: string) => unknown; type GetDocumentAsset = (document: unknown, assetType: string) => unknown;
type NotifyApiError = (error: unknown, fallbackMessage?: string) => void; type NotifyApiError = (error: unknown, fallbackMessage?: string) => void;
type DocumentsTableProps = ComponentProps<typeof DocumentsPanel> | null; type DocumentsTableProps = ComponentProps<typeof DocumentsPanel> | null;
type DetailPanelProps = (ComponentProps<typeof DocumentViewerPanel> & { type DetailPanelProps = (ComponentProps<typeof DocumentViewerPanel> & {
onClose?: () => void; onClose?: () => void;
onOpenPreview?: (args: { documentIds: Array<string> }) => void; onOpenViewer?: (args: { documentIds: Array<string> }) => void;
folderNodes?: Map<Identifier | 'root', unknown>; folderNodes?: Map<Identifier | 'root', unknown>;
ensureFolderData?: ( ensureFolderData?: (
folderId: Identifier | 'root', folderId: Identifier | 'root',
@@ -35,13 +35,13 @@ export interface UseWorkspaceSurfaceArgs {
documentsTableProps?: DocumentsTableProps; documentsTableProps?: DocumentsTableProps;
detailPanelProps?: DetailPanelProps; detailPanelProps?: DetailPanelProps;
detailPanelOpen?: boolean; detailPanelOpen?: boolean;
previewWorkspaceDocument?: unknown; viewerWorkspaceDocument?: unknown;
previewDocumentId?: Identifier | null; viewerDocumentId?: Identifier | null;
ensureAssetUrl?: EnsureAssetUrl; ensureAssetUrl?: EnsureAssetUrl;
ensurePreviewData?: EnsurePreviewData; ensureViewerData?: EnsureViewerData;
getDocumentAsset?: GetDocumentAsset; getDocumentAsset?: GetDocumentAsset;
notifyApiError?: NotifyApiError; notifyApiError?: NotifyApiError;
closeDocumentPreview?: () => void; closeDocumentViewer?: () => void;
} }
interface UseWorkspaceSurfaceResult { interface UseWorkspaceSurfaceResult {
@@ -54,13 +54,13 @@ export const useWorkspaceSurface = ({
documentsTableProps, documentsTableProps,
detailPanelProps, detailPanelProps,
detailPanelOpen = false, detailPanelOpen = false,
previewWorkspaceDocument, viewerWorkspaceDocument,
previewDocumentId, viewerDocumentId,
ensureAssetUrl, ensureAssetUrl,
ensurePreviewData, ensureViewerData,
getDocumentAsset, getDocumentAsset,
notifyApiError, notifyApiError,
closeDocumentPreview, closeDocumentViewer,
}: UseWorkspaceSurfaceArgs): UseWorkspaceSurfaceResult => { }: UseWorkspaceSurfaceArgs): UseWorkspaceSurfaceResult => {
const { registerDetailCloseHandler, setDetailActive } = usePanelManager(); const { registerDetailCloseHandler, setDetailActive } = usePanelManager();
@@ -118,7 +118,7 @@ export const useWorkspaceSurface = ({
? (() => { ? (() => {
const { const {
onClose, onClose,
onOpenPreview, onOpenViewer,
tags: tagOptions, tags: tagOptions,
folderNodes, folderNodes,
ensureFolderData, ensureFolderData,
@@ -129,7 +129,7 @@ export const useWorkspaceSurface = ({
variant="sidebar" variant="sidebar"
sidebarMode={sidebarMode} sidebarMode={sidebarMode}
onClose={onClose} onClose={onClose}
onMaximize={onOpenPreview} onMaximize={onOpenViewer}
tagOptions={tagOptions} tagOptions={tagOptions}
{...restDetailProps} {...restDetailProps}
/> />
@@ -164,10 +164,10 @@ export const useWorkspaceSurface = ({
detailPanelProps, detailPanelProps,
]); ]);
const showPreviewWorkspace = Boolean(previewDocumentId); const showViewerWorkspace = Boolean(viewerDocumentId);
const previewSurface = useMemo<WorkspaceSurface>(() => { const viewerSurface = useMemo<WorkspaceSurface>(() => {
if (!showPreviewWorkspace) { if (!showViewerWorkspace) {
return null; return null;
} }
@@ -191,8 +191,8 @@ export const useWorkspaceSurface = ({
const viewer = ( const viewer = (
<DocumentViewerPanel <DocumentViewerPanel
document={previewWorkspaceDocument || null} document={viewerWorkspaceDocument || null}
hydrateDocument={ensurePreviewData} hydrateDocument={ensureViewerData}
tagLookupById={tagLookupById} tagLookupById={tagLookupById}
tagOptions={tagOptions} tagOptions={tagOptions}
onTagAdd={onTagAdd} onTagAdd={onTagAdd}
@@ -205,10 +205,10 @@ export const useWorkspaceSurface = ({
onUpdateIssued={onUpdateIssued} onUpdateIssued={onUpdateIssued}
ensureAssetUrl={ensureAssetUrl} ensureAssetUrl={ensureAssetUrl}
getDocumentAsset={getDocumentAsset} getDocumentAsset={getDocumentAsset}
ensurePreviewData={ensurePreviewData} ensurePreviewData={ensureViewerData}
notifyApiError={notifyApiError} notifyApiError={notifyApiError}
sidebarToggle={sidebarToggle} sidebarToggle={sidebarToggle}
onClose={closeDocumentPreview} onClose={closeDocumentViewer}
resolveFolderPath={resolveFolderPath} resolveFolderPath={resolveFolderPath}
/> />
); );
@@ -223,23 +223,23 @@ export const useWorkspaceSurface = ({
return { content, detail: null, detailMode: null }; return { content, detail: null, detailMode: null };
}, [ }, [
showPreviewWorkspace, showViewerWorkspace,
previewWorkspaceDocument, viewerWorkspaceDocument,
ensurePreviewData, ensureViewerData,
ensureAssetUrl, ensureAssetUrl,
getDocumentAsset, getDocumentAsset,
notifyApiError, notifyApiError,
renderSidebarToggle, renderSidebarToggle,
closeDocumentPreview, closeDocumentViewer,
detailPanelProps, detailPanelProps,
]); ]);
const surface = useMemo<WorkspaceSurface>(() => { const surface = useMemo<WorkspaceSurface>(() => {
if (showPreviewWorkspace) { if (showViewerWorkspace) {
return previewSurface; return viewerSurface;
} }
return documentsSurface; return documentsSurface;
}, [showPreviewWorkspace, previewSurface, documentsSurface]); }, [showViewerWorkspace, viewerSurface, documentsSurface]);
return { surface }; return { surface };
}; };
@@ -57,8 +57,8 @@ interface UseDocumentMutationsArgs {
selectionState: SelectionState; selectionState: SelectionState;
tagsState: TagsState; tagsState: TagsState;
correspondentsState: CorrespondentsState; correspondentsState: CorrespondentsState;
closeDocumentPreview: () => void; closeDocumentViewer: () => void;
previewDocumentId?: DocumentId | null; viewerDocumentId?: DocumentId | null;
resolveTargetDocumentIds: (ids?: Identifier[] | null) => Identifier[]; resolveTargetDocumentIds: (ids?: Identifier[] | null) => Identifier[];
} }
@@ -103,8 +103,8 @@ const useDocumentMutations = ({
selectionState, selectionState,
tagsState, tagsState,
correspondentsState, correspondentsState,
closeDocumentPreview, closeDocumentViewer,
previewDocumentId, viewerDocumentId,
resolveTargetDocumentIds, resolveTargetDocumentIds,
}: UseDocumentMutationsArgs): UseDocumentMutationsResult => { }: UseDocumentMutationsArgs): UseDocumentMutationsResult => {
const { showToast } = useStatusToast(); const { showToast } = useStatusToast();
@@ -140,14 +140,14 @@ const useDocumentMutations = ({
await queueDocumentReanalysis(documentId); await queueDocumentReanalysis(documentId);
showToast('Analysis queued.', 'info'); showToast('Analysis queued.', 'info');
// Close preview if it's the current one to allow refresh? // Close preview if it's the current one to allow refresh?
if (previewDocumentId === documentId) { if (viewerDocumentId === documentId) {
closeDocumentPreview(); closeDocumentViewer();
} }
} catch (error) { } catch (error) {
notifyApiError(error, 'Failed to queue analysis.'); notifyApiError(error, 'Failed to queue analysis.');
} }
}, },
[closeDocumentPreview, notifyApiError, previewDocumentId, showToast], [closeDocumentViewer, notifyApiError, viewerDocumentId, showToast],
); );
const handleDocumentsDelete = useCallback( const handleDocumentsDelete = useCallback(
@@ -55,7 +55,7 @@ import { useAppDispatch, useAppState } from '../../lib/store/appState';
import { listFolderContents } from '../../lib/api/apiClient'; import { listFolderContents } from '../../lib/api/apiClient';
import { useApi } from '../../lib/context/ApiContext'; import { useApi } from '../../lib/context/ApiContext';
import { useWorkspaceSelection } from '../../app/useWorkspaceSelection'; import { useWorkspaceSelection } from '../../app/useWorkspaceSelection';
import useDocumentPreview from '../../app/useDocumentPreview'; import useDocumentViewer from '../../app/useDocumentViewer';
import type { DocumentId, FolderNodeId, Identifier } from '../../types/identifiers'; import type { DocumentId, FolderNodeId, Identifier } from '../../types/identifiers';
import type { Document } from '../../types/documents'; import type { Document } from '../../types/documents';
@@ -117,7 +117,7 @@ const useDocumentsWorkspace = ({
const docMatch = matchPath('/documents/:documentId', location.pathname); const docMatch = matchPath('/documents/:documentId', location.pathname);
const routeFolderId = folderMatch?.params?.folderId || null; const routeFolderId = folderMatch?.params?.folderId || null;
const routeDocumentId = docMatch?.params?.documentId || null; const routeDocumentId = docMatch?.params?.documentId || null;
const previewDocumentId = routeDocumentId; const viewerDocumentId = routeDocumentId;
const handleBreadcrumbNavigate = useCallback((crumb: { id?: Identifier | string } | null) => { const handleBreadcrumbNavigate = useCallback((crumb: { id?: Identifier | string } | null) => {
if (!crumb || !crumb.id) { if (!crumb || !crumb.id) {
@@ -157,7 +157,7 @@ const useDocumentsWorkspace = ({
const [draggedDocumentIds, setDraggedDocumentIds] = useState<DocumentId[]>([]); const [draggedDocumentIds, setDraggedDocumentIds] = useState<DocumentId[]>([]);
const [draggedFolderId, setDraggedFolderId] = useState<FolderNodeId | null>(null); const [draggedFolderId, setDraggedFolderId] = useState<FolderNodeId | null>(null);
const [activePreviewId, setActivePreviewId] = useState<DocumentId | null>(routeDocumentId || null); const [activeViewerId, setActiveViewerId] = useState<DocumentId | null>(routeDocumentId || null);
const shellRef = useRef(null); const shellRef = useRef(null);
const assetManagerRef = useRef(null); const assetManagerRef = useRef(null);
if (!assetManagerRef.current) { if (!assetManagerRef.current) {
@@ -402,7 +402,7 @@ const useDocumentsWorkspace = ({
documentsManager, documentsManager,
}); });
const documentsFilter = documentsSearch.documentsFilterValue;
const showingSearchResults = documentsSearch.searchResultIds !== null; const showingSearchResults = documentsSearch.searchResultIds !== null;
// Live Filter: Ensure we only show documents that actually belong to the current folder. // Live Filter: Ensure we only show documents that actually belong to the current folder.
@@ -432,30 +432,30 @@ const useDocumentsWorkspace = ({
}); });
const { const {
openDocumentPreview, openDocumentViewer,
closeDocumentPreview, closeDocumentViewer,
resetPreviewState, resetViewerState,
previewWorkspaceDocument, viewerWorkspaceDocument,
previewActive, viewerActive,
} = useDocumentPreview({ } = useDocumentViewer({
routeDocumentId: previewDocumentId, routeDocumentId: viewerDocumentId,
documentsManager, documentsManager,
selectedFolder, selectedFolder,
locationPathname: location.pathname, locationPathname: location.pathname,
locationSearch: location.search, locationSearch: location.search,
detailPanelControlRef, detailPanelControlRef,
setActivePreviewId, setActiveViewerId,
}); });
const openDocumentPreviewForDetail = useCallback( const openDocumentViewerForDetail = useCallback(
({ documentIds }: { documentIds?: Identifier[] } = {}) => { ({ documentIds }: { documentIds?: Identifier[] } = {}) => {
const targetId = documentIds?.find((value): value is Identifier => value != null); const targetId = documentIds?.find((value): value is Identifier => value != null);
if (targetId == null) { if (targetId == null) {
return; return;
} }
openDocumentPreview(targetId, { replace: true }); openDocumentViewer(targetId, { replace: true });
}, },
[openDocumentPreview], [openDocumentViewer],
); );
const getDocumentAsset = useCallback((doc, type) => { const getDocumentAsset = useCallback((doc, type) => {
@@ -475,8 +475,8 @@ const useDocumentsWorkspace = ({
selectionAnchorRef, selectionAnchorRef,
setFocusedDocumentId, setFocusedDocumentId,
selectedDocumentIds, selectedDocumentIds,
activePreviewId, activeViewerId,
setActivePreviewId, setActiveViewerId,
selectionInitializedRef, selectionInitializedRef,
}); });
@@ -573,10 +573,10 @@ const useDocumentsWorkspace = ({
documentsSearch.setSearchQuery(''); documentsSearch.setSearchQuery('');
documentsSearch.setActiveTagFilters([]); documentsSearch.setActiveTagFilters([]);
documentsSearch.setActiveCorrespondentFilters([]); documentsSearch.setActiveCorrespondentFilters([]);
setActivePreviewId(null); setActiveViewerId(null);
detailPanelControlRef.current.close(); detailPanelControlRef.current.close();
assetManager.reset(); assetManager.reset();
resetPreviewState(); resetViewerState();
upload.resetUploadsState(); upload.resetUploadsState();
upload.clearUploadQueue(); upload.clearUploadQueue();
@@ -598,8 +598,8 @@ const useDocumentsWorkspace = ({
setDraggedDocumentIds, setDraggedDocumentIds,
setDraggedFolderId, setDraggedFolderId,
documentsSearch, documentsSearch,
setActivePreviewId, setActiveViewerId,
resetPreviewState, resetViewerState,
upload, upload,
]); ]);
@@ -631,8 +631,8 @@ const useDocumentsWorkspace = ({
...correspondentsState, ...correspondentsState,
correspondentManager, correspondentManager,
}, },
closeDocumentPreview, closeDocumentViewer,
previewDocumentId, viewerDocumentId,
resolveTargetDocumentIds, resolveTargetDocumentIds,
}); });
@@ -693,7 +693,7 @@ const useDocumentsWorkspace = ({
selectionAnchorRef, selectionAnchorRef,
promoteSelectionOrderRaw, promoteSelectionOrderRaw,
setFocusedDocumentId, setFocusedDocumentId,
setActivePreviewId, setActiveViewerId,
clearSelection, clearSelection,
focusedDocumentId, focusedDocumentId,
setFocusedEntryKey, setFocusedEntryKey,
@@ -884,12 +884,12 @@ const useDocumentsWorkspace = ({
return () => window.removeEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown);
}, [settingsOpen]); }, [settingsOpen]);
const detailPanel = useDetailWorkspace({ const detailWorkspace = useDetailWorkspace({
documentLookup, documentLookup,
folderNodes, folderNodes,
detailPanelControlRef, detailPanelControlRef,
detailFolderFetchRef, detailFolderFetchRef,
openDocumentPreview: openDocumentPreviewForDetail, openDocumentViewer: openDocumentViewerForDetail,
handleDocumentTitleUpdate, handleDocumentTitleUpdate,
handleDocumentIssuedUpdate, handleDocumentIssuedUpdate,
handleDocumentTagAdd, handleDocumentTagAdd,
@@ -906,6 +906,8 @@ const useDocumentsWorkspace = ({
resolveFolderPath, resolveFolderPath,
}); });
const handleEntryPointerCore = useEntryPointerCore({ const handleEntryPointerCore = useEntryPointerCore({
onSelectEntry: (entry, event, { rowKey, modifierClick, primaryClick }) => { onSelectEntry: (entry, event, { rowKey, modifierClick, primaryClick }) => {
const { type, id } = entry; const { type, id } = entry;
@@ -979,17 +981,18 @@ const useDocumentsWorkspace = ({
}; };
const preview = { const preview = {
previewActive, openDocumentViewerForDetail,
previewWorkspaceDocument, viewerActive,
previewDocumentId, viewerWorkspaceDocument,
closeDocumentPreview, viewerDocumentId,
closeDocumentViewer,
selectionContext,
ensureAssetUrl, ensureAssetUrl,
getDocumentAsset, getDocumentAsset,
}; };
const search = { const search = {
searchQuery: documentsSearch.searchQuery, searchQuery: documentsSearch.searchQuery,
documentsFilter,
searchLoading: documentsSearch.searchLoading, searchLoading: documentsSearch.searchLoading,
documentsViewMode, documentsViewMode,
handleDocumentsViewModeChange, handleDocumentsViewModeChange,
@@ -999,6 +1002,7 @@ const useDocumentsWorkspace = ({
handleDocumentsSortDirectionToggle, handleDocumentsSortDirectionToggle,
searchResultIds: documentsSearch.searchResultIds, searchResultIds: documentsSearch.searchResultIds,
documents: viewDocuments, documents: viewDocuments,
documentsFilter: documentsSearch.documentsFilterValue,
}; };
const folderTree = { const folderTree = {
@@ -1043,7 +1047,7 @@ const useDocumentsWorkspace = ({
correspondents, correspondents,
passkeys, passkeys,
preview, preview,
detailPanel, detailPanel: detailWorkspace,
search, search,
folderTree, folderTree,
selection, selection,
@@ -29,7 +29,7 @@ interface UseDocumentsSelectionOptions {
selectionAnchorRef: { current: string | null }; selectionAnchorRef: { current: string | null };
promoteSelectionOrderRaw: (id: DocumentId) => void; promoteSelectionOrderRaw: (id: DocumentId) => void;
setFocusedDocumentId: (id: DocumentId | null) => void; setFocusedDocumentId: (id: DocumentId | null) => void;
setActivePreviewId: (id: DocumentId | null) => void; setActiveViewerId: (id: DocumentId | null) => void;
clearSelection: () => void; clearSelection: () => void;
focusedDocumentId: DocumentId | null; focusedDocumentId: DocumentId | null;
setFocusedEntryKey: Dispatch<SetStateAction<string | null>>; setFocusedEntryKey: Dispatch<SetStateAction<string | null>>;
@@ -46,7 +46,7 @@ const useDocumentsSelection = ({
selectionAnchorRef, selectionAnchorRef,
promoteSelectionOrderRaw, promoteSelectionOrderRaw,
setFocusedDocumentId, setFocusedDocumentId,
setActivePreviewId, setActiveViewerId,
clearSelection, clearSelection,
focusedDocumentId, focusedDocumentId,
setFocusedEntryKey, setFocusedEntryKey,
@@ -92,9 +92,9 @@ const useDocumentsSelection = ({
selectionAnchorRef.current = rowKey; selectionAnchorRef.current = rowKey;
} }
setFocusedDocumentId(docId); setFocusedDocumentId(docId);
setActivePreviewId(docId); setActiveViewerId(docId);
}, },
[promoteSelectionOrderRaw, selectionAnchorRef, setFocusedDocumentId, setActivePreviewId], [promoteSelectionOrderRaw, selectionAnchorRef, setFocusedDocumentId, setActiveViewerId],
); );
const clearDocumentSelection = useCallback(() => { const clearDocumentSelection = useCallback(() => {
@@ -11,8 +11,8 @@ interface UseWorkspaceSelectionSyncArgs {
selectionAnchorRef: MutableRefObject<Identifier | string | null>; selectionAnchorRef: MutableRefObject<Identifier | string | null>;
setFocusedDocumentId: (id: Identifier | null) => void; setFocusedDocumentId: (id: Identifier | null) => void;
selectedDocumentIds: Identifier[]; selectedDocumentIds: Identifier[];
activePreviewId: Identifier | null; activeViewerId: Identifier | null;
setActivePreviewId: (id: Identifier | null) => void; setActiveViewerId: (id: Identifier | null) => void;
selectionInitializedRef: MutableRefObject<boolean>; selectionInitializedRef: MutableRefObject<boolean>;
} }
@@ -25,8 +25,8 @@ const useWorkspaceSelectionSync = ({
selectionAnchorRef, selectionAnchorRef,
setFocusedDocumentId, setFocusedDocumentId,
selectedDocumentIds, selectedDocumentIds,
activePreviewId, activeViewerId,
setActivePreviewId, setActiveViewerId,
selectionInitializedRef, selectionInitializedRef,
}: UseWorkspaceSelectionSyncArgs) => { }: UseWorkspaceSelectionSyncArgs) => {
useEffect(() => { useEffect(() => {
@@ -52,11 +52,11 @@ const useWorkspaceSelectionSync = ({
if (!selectedDocumentIds.length) { if (!selectedDocumentIds.length) {
return; return;
} }
if (!selectedDocumentIds.includes(activePreviewId as Identifier)) { if (!selectedDocumentIds.includes(activeViewerId as Identifier)) {
setActivePreviewId(selectedDocumentIds[selectedDocumentIds.length - 1]); setActiveViewerId(selectedDocumentIds[selectedDocumentIds.length - 1]);
} }
selectionInitializedRef.current = true; selectionInitializedRef.current = true;
}, [selectedDocumentIds, activePreviewId, selectionInitializedRef, setActivePreviewId]); }, [selectedDocumentIds, activeViewerId, selectionInitializedRef, setActiveViewerId]);
}; };
export default useWorkspaceSelectionSync; export default useWorkspaceSelectionSync;
@@ -14,14 +14,14 @@ const [DocumentOpenContext, useDocumentOpen] = createSafeContext<DocumentOpenCon
interface DocumentOpenProviderProps { interface DocumentOpenProviderProps {
children: React.ReactNode; children: React.ReactNode;
onOpenViewer?: (docId: Identifier) => void; onOpenViewer?: (docId: Identifier) => void;
onOpenPreview?: (doc: Document) => void; onOpenFullscreenPreview?: (doc: Document) => void;
onOpenDetailPanel?: (docId: Identifier) => void; onOpenDetailPanel?: (docId: Identifier) => void;
} }
export const DocumentOpenProvider: React.FC<DocumentOpenProviderProps> = ({ export const DocumentOpenProvider: React.FC<DocumentOpenProviderProps> = ({
children, children,
onOpenViewer, onOpenViewer,
onOpenPreview, onOpenFullscreenPreview,
onOpenDetailPanel, onOpenDetailPanel,
}) => { }) => {
const openDocument = useCallback((doc: Document, intent: DocumentOpenIntent = 'preview') => { const openDocument = useCallback((doc: Document, intent: DocumentOpenIntent = 'preview') => {
@@ -29,8 +29,8 @@ export const DocumentOpenProvider: React.FC<DocumentOpenProviderProps> = ({
switch (intent) { switch (intent) {
case 'preview': case 'preview':
if (onOpenPreview) { if (onOpenFullscreenPreview) {
onOpenPreview(doc); onOpenFullscreenPreview(doc);
} }
break; break;
case 'inspect': case 'inspect':
@@ -51,7 +51,7 @@ export const DocumentOpenProvider: React.FC<DocumentOpenProviderProps> = ({
} }
break; break;
} }
}, [onOpenPreview, onOpenDetailPanel, onOpenViewer]); }, [onOpenFullscreenPreview, onOpenDetailPanel, onOpenViewer]);
return ( return (
<DocumentOpenContext.Provider value={{ openDocument }}> <DocumentOpenContext.Provider value={{ openDocument }}>
+2 -2
View File
@@ -2,7 +2,7 @@
@import './base/controls.css'; @import './base/controls.css';
@import './layout/panel-header-controls.css'; @import './layout/panel-header-controls.css';
@import './components/resize-handle.css'; @import './components/resize-handle.css';
@import '../viewer/preview-zoom.css'; @import '../viewer/fullscreen-preview.css';
@import './base/iconography.css'; @import './base/iconography.css';
@import '../documents/styles/controls.css'; @import '../documents/styles/controls.css';
@import './layout/structure.css'; @import './layout/structure.css';
@@ -21,4 +21,4 @@
@import '../login/login.css'; @import '../login/login.css';
@import '../documents/styles/shared-snippets.css'; @import '../documents/styles/shared-snippets.css';
@import './uploads/overlay.css'; @import './uploads/overlay.css';
@import './base/text-button.css'; @import './base/text-button.css';
+4 -4
View File
@@ -16,7 +16,7 @@ import {
sortCorrespondents, sortCorrespondents,
} from './components/DocumentSummarySection'; } from './components/DocumentSummarySection';
import DocumentDownloadLink from '../documents/components/DocumentDownloadLink'; import DocumentDownloadLink from '../documents/components/DocumentDownloadLink';
import { usePreviewContext } from './PreviewContext'; import { useFullscreenPreviewContext } from './FullscreenPreviewContext';
import type { DocumentSummarySectionProps } from './components/DocumentSummarySection'; import type { DocumentSummarySectionProps } from './components/DocumentSummarySection';
import { extractDocumentMetadataPayload } from './logic/documentSummary'; import { extractDocumentMetadataPayload } from './logic/documentSummary';
import { resolveDocumentAssetUrl } from '../lib/assets/AssetManager'; import { resolveDocumentAssetUrl } from '../lib/assets/AssetManager';
@@ -218,14 +218,14 @@ const DocumentViewerPanel: React.FC<DocumentViewerPanelProps> = ({
[hasOcr, loadOcrContent], [hasOcr, loadOcrContent],
); );
const { openPreview } = usePreviewContext(); const { openFullscreenPreview } = useFullscreenPreviewContext();
const handleZoomOpen = useCallback(() => { const handleZoomOpen = useCallback(() => {
if (!document) { if (!document) {
return; return;
} }
openPreview(document); openFullscreenPreview(document);
}, [document, openPreview]); }, [document, openFullscreenPreview]);
const panelRef = useRef<HTMLDivElement | HTMLFormElement | HTMLElement | null>(null); const panelRef = useRef<HTMLDivElement | HTMLFormElement | HTMLElement | null>(null);
const isStackedLayout = useViewerLayoutMode(panelRef, document?.id); const isStackedLayout = useViewerLayoutMode(panelRef, document?.id);
@@ -1,33 +1,33 @@
import React, { useState, useCallback, useMemo, useRef } from 'react'; import React, { useState, useCallback, useMemo, useRef } from 'react';
import PreviewZoomOverlay from './components/PreviewZoomOverlay'; import FullscreenPreviewOverlay from './components/FullscreenPreviewOverlay';
import type { Document } from '../types/documents'; import type { Document } from '../types/documents';
import type { Identifier } from '../types/identifiers'; import type { Identifier } from '../types/identifiers';
import { createSafeContext } from '../utils/createSafeContext'; import { createSafeContext } from '../utils/createSafeContext';
interface PreviewContextType { interface FullscreenPreviewContextType {
openPreview: (doc: Document) => void; openFullscreenPreview: (doc: Document) => void;
closePreview: () => void; closeFullscreenPreview: () => void;
} }
const [PreviewContext, usePreviewContext] = createSafeContext<PreviewContextType>('Preview'); const [FullscreenPreviewContext, useFullscreenPreviewContext] = createSafeContext<FullscreenPreviewContextType>('FullscreenPreview');
interface PreviewProviderProps { interface FullscreenPreviewProviderProps {
children: React.ReactNode; children: React.ReactNode;
onNavigate?: (documentId: Identifier) => void; onNavigate?: (documentId: Identifier) => void;
} }
export const PreviewProvider: React.FC<PreviewProviderProps> = ({ children, onNavigate }) => { export const FullscreenPreviewProvider: React.FC<FullscreenPreviewProviderProps> = ({ children, onNavigate }) => {
const [previewDoc, setPreviewDoc] = useState<Document | null>(null); const [previewDoc, setPreviewDoc] = useState<Document | null>(null);
const lastFocusedElement = useRef<HTMLElement | null>(null); const lastFocusedElement = useRef<HTMLElement | null>(null);
const openPreview = useCallback((doc: Document) => { const openFullscreenPreview = useCallback((doc: Document) => {
if (!lastFocusedElement.current) { if (!lastFocusedElement.current) {
lastFocusedElement.current = document.activeElement as HTMLElement; lastFocusedElement.current = document.activeElement as HTMLElement;
} }
setPreviewDoc(doc); setPreviewDoc(doc);
}, []); }, []);
const closePreview = useCallback(() => { const closeFullscreenPreview = useCallback(() => {
setPreviewDoc(null); setPreviewDoc(null);
if (lastFocusedElement.current) { if (lastFocusedElement.current) {
lastFocusedElement.current.focus(); lastFocusedElement.current.focus();
@@ -38,26 +38,26 @@ export const PreviewProvider: React.FC<PreviewProviderProps> = ({ children, onNa
const handleMaximize = useCallback(() => { const handleMaximize = useCallback(() => {
if (previewDoc && onNavigate) { if (previewDoc && onNavigate) {
onNavigate(previewDoc.id); onNavigate(previewDoc.id);
closePreview(); closeFullscreenPreview();
} }
}, [previewDoc, onNavigate, closePreview]); }, [previewDoc, onNavigate, closeFullscreenPreview]);
const value = useMemo(() => ({ const value = useMemo(() => ({
openPreview, openFullscreenPreview,
closePreview, closeFullscreenPreview,
}), [openPreview, closePreview]); }), [openFullscreenPreview, closeFullscreenPreview]);
return ( return (
<PreviewContext.Provider value={value}> <FullscreenPreviewContext.Provider value={value}>
{children} {children}
<PreviewZoomOverlay <FullscreenPreviewOverlay
open={Boolean(previewDoc)} open={Boolean(previewDoc)}
onClose={closePreview} onClose={closeFullscreenPreview}
onMaximize={onNavigate ? handleMaximize : undefined} onMaximize={onNavigate ? handleMaximize : undefined}
document={previewDoc} document={previewDoc}
/> />
</PreviewContext.Provider> </FullscreenPreviewContext.Provider>
); );
}; };
export { usePreviewContext }; export { useFullscreenPreviewContext };
@@ -7,14 +7,14 @@ import { IconX, FileInfoIcon } from '../../components/icons';
import type { Document } from '../../types/documents'; import type { Document } from '../../types/documents';
interface PreviewZoomOverlayProps { interface FullscreenPreviewOverlayProps {
open?: boolean; open?: boolean;
onClose: () => void; onClose: () => void;
onMaximize?: () => void; onMaximize?: () => void;
document?: Document | null; document?: Document | null;
} }
const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({ const FullscreenPreviewOverlay: React.FC<FullscreenPreviewOverlayProps> = ({
open = false, open = false,
onClose, onClose,
onMaximize, onMaximize,
@@ -81,12 +81,12 @@ const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
} }
}; };
const stageClassName = 'preview-zoom__stage'; const stageClassName = 'fullscreen-preview__stage';
const containerClassName = 'preview-zoom__scroll'; const containerClassName = 'fullscreen-preview__scroll';
const backdropClassName = [ const backdropClassName = [
'preview-zoom-backdrop', 'fullscreen-preview-backdrop',
isBackdropVisible ? 'preview-zoom-backdrop--visible' : '', isBackdropVisible ? 'fullscreen-preview-backdrop--visible' : '',
] ]
.filter(Boolean) .filter(Boolean)
.join(' '); .join(' ');
@@ -160,4 +160,4 @@ const PreviewZoomOverlay: React.FC<PreviewZoomOverlayProps> = ({
); );
}; };
export default PreviewZoomOverlay; export default FullscreenPreviewOverlay;
@@ -1,4 +1,4 @@
.preview-zoom-backdrop { .fullscreen-preview-backdrop {
position: fixed; position: fixed;
inset: 0; inset: 0;
background: var(--overlay-backdrop); background: var(--overlay-backdrop);
@@ -14,13 +14,13 @@
-webkit-backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
} }
.preview-zoom-backdrop--visible { .fullscreen-preview-backdrop--visible {
opacity: 1; opacity: 1;
background: var(--overlay-backdrop); background: var(--overlay-backdrop);
pointer-events: auto; pointer-events: auto;
} }
.preview-zoom__stage { .fullscreen-preview__stage {
position: relative; position: relative;
display: flex; display: flex;
align-items: center; align-items: center;
@@ -32,7 +32,7 @@
--header-clearance: calc(2.5rem + 0.5rem); --header-clearance: calc(2.5rem + 0.5rem);
} }
.preview-zoom__scroll .document-viewer__object--image { .fullscreen-preview__scroll .document-viewer__object--image {
width: auto; width: auto;
height: auto; height: auto;
box-shadow: 0 32px 120px var(--shadow-deep); box-shadow: 0 32px 120px var(--shadow-deep);
@@ -42,7 +42,7 @@
touch-action: manipulation; touch-action: manipulation;
} }
.preview-zoom__scroll { .fullscreen-preview__scroll {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -55,21 +55,21 @@
--pdf-viewer-stack-padding: var(--preview-padding); --pdf-viewer-stack-padding: var(--preview-padding);
} }
.preview-zoom__scroll:focus { .fullscreen-preview__scroll:focus {
outline: none; outline: none;
} }
.preview-zoom__scroll .document-viewer__object--pdf { .fullscreen-preview__scroll .document-viewer__object--pdf {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
.preview-zoom__stage .pdf-viewer__page-wrapper { .fullscreen-preview__stage .pdf-viewer__page-wrapper {
position: relative; position: relative;
overflow: visible; overflow: visible;
} }
.preview-zoom__stage .pdf-viewer__page-wrapper::after { .fullscreen-preview__stage .pdf-viewer__page-wrapper::after {
content: ''; content: '';
position: absolute; position: absolute;
inset: 0; inset: 0;
@@ -80,7 +80,7 @@
} }
/* Header positioning for preview zoom overlay */ /* Header positioning for preview zoom overlay */
.preview-zoom-backdrop .panel-header--dark { .fullscreen-preview-backdrop .panel-header--dark {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
@@ -94,6 +94,6 @@
} }
/* Prevent header clicks from closing backdrop */ /* Prevent header clicks from closing backdrop */
.preview-zoom-backdrop .panel-header--dark * { .fullscreen-preview-backdrop .panel-header--dark * {
pointer-events: auto; pointer-events: auto;
} }
@@ -22,7 +22,7 @@ interface UseDetailWorkspaceArgs {
folderNodes: Map<Identifier | 'root', FolderNode>; folderNodes: Map<Identifier | 'root', FolderNode>;
detailPanelControlRef: MutableRefObject<{ open?: (documentId: Identifier) => void; close?: () => void } | null>; detailPanelControlRef: MutableRefObject<{ open?: (documentId: Identifier) => void; close?: () => void } | null>;
detailFolderFetchRef: MutableRefObject<Set<Identifier | 'root'>>; detailFolderFetchRef: MutableRefObject<Set<Identifier | 'root'>>;
openDocumentPreview?: (args: { documentIds: Identifier[] }) => void; openDocumentViewer?: (args: { documentIds: Identifier[] }) => void;
handleDocumentTitleUpdate?: (docId: Identifier, title: string) => Promise<boolean> | boolean; handleDocumentTitleUpdate?: (docId: Identifier, title: string) => Promise<boolean> | boolean;
handleDocumentIssuedUpdate?: (docId: Identifier, issued: number | null) => Promise<boolean> | boolean; handleDocumentIssuedUpdate?: (docId: Identifier, issued: number | null) => Promise<boolean> | boolean;
handleDocumentTagAdd?: (doc: Document, value: string, context?: { option?: unknown }) => void; handleDocumentTagAdd?: (doc: Document, value: string, context?: { option?: unknown }) => void;
@@ -54,7 +54,7 @@ const useDetailWorkspace = ({
folderNodes, folderNodes,
detailPanelControlRef, detailPanelControlRef,
detailFolderFetchRef, detailFolderFetchRef,
openDocumentPreview, openDocumentViewer,
handleDocumentTitleUpdate, handleDocumentTitleUpdate,
handleDocumentIssuedUpdate, handleDocumentIssuedUpdate,
handleDocumentTagAdd, handleDocumentTagAdd,
@@ -176,7 +176,7 @@ const useDetailWorkspace = ({
correspondentLookupById, correspondentLookupById,
onTagAdd: handleDetailTagAdd, onTagAdd: handleDetailTagAdd,
onTagRemove: handleDetailTagRemove, onTagRemove: handleDetailTagRemove,
onOpenPreview: openDocumentPreview, onOpenViewer: openDocumentViewer,
onUpdateTitle: handleDocumentTitleUpdate, onUpdateTitle: handleDocumentTitleUpdate,
onUpdateIssued: handleDocumentIssuedUpdate, onUpdateIssued: handleDocumentIssuedUpdate,
ensureAssetUrl, ensureAssetUrl,