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