feat: Centralize document preview logic into useDocumentPreview and add correspondentLookupByName to useCorrespondents

This commit is contained in:
2025-12-10 02:33:22 +01:00
parent ed8450c0e2
commit b9f0278329
8 changed files with 69 additions and 125 deletions
+14 -1
View File
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useRef } from 'react';
import { useCallback, useEffect, useRef, useMemo } from 'react';
import type {
Dispatch,
MutableRefObject,
@@ -36,6 +36,8 @@ interface UseDocumentPreviewResult {
openDocumentPreview: (documentId: DocumentId, options?: { replace?: boolean }) => void;
closeDocumentPreview: (folderId?: FolderId) => void;
resetPreviewState: () => void;
previewWorkspaceDocument: Document | null;
previewActive: boolean;
}
const useDocumentPreview = ({
@@ -127,11 +129,22 @@ const useDocumentPreview = ({
};
}, [routeDocumentId, ensurePreviewData, notifyApiError, closeDocumentPreview]);
const previewWorkspaceDocument = useMemo(() => {
if (!routeDocumentId) {
return null;
}
return documentsManager.getById(routeDocumentId);
}, [routeDocumentId, documentsManager]);
const previewActive = Boolean(routeDocumentId && previewWorkspaceDocument);
return {
ensurePreviewData,
openDocumentPreview,
closeDocumentPreview,
resetPreviewState,
previewWorkspaceDocument,
previewActive,
};
};
+1
View File
@@ -19,6 +19,7 @@ interface UseManagementModalsArgs {
onTagDelete?: (...args: any[]) => void | Promise<void>;
correspondents?: Correspondent[];
correspondentLookupById?: Map<Identifier, Correspondent> | null;
correspondentLookupByName?: Map<string, Correspondent> | null;
refreshCorrespondents?: () => void | Promise<void>;
onCorrespondentCreate?: (...args: any[]) => void | Promise<void>;
onCorrespondentUpdate?: (...args: any[]) => void | Promise<void>;
@@ -1,4 +1,4 @@
import { useCallback, useSyncExternalStore } from 'react';
import { useCallback, useSyncExternalStore, useMemo } from 'react';
import { useStatusToast } from '../../lib/context/StatusToastContext';
import type { Correspondent } from '../../types/documents';
import type { Identifier } from '../../types/identifiers';
@@ -114,9 +114,20 @@ const useCorrespondents = ({
[documentsManager, notifyApiError, correspondentManager, showToast],
);
const correspondentLookupByName = useMemo(() => {
const map = new Map<string, Correspondent>();
for (const correspondent of correspondents) {
if (correspondent.name) {
map.set(correspondent.name.toLowerCase(), correspondent);
}
}
return map;
}, [correspondents]);
return {
correspondents,
correspondentLookupById: correspondentsSnapshot,
correspondentLookupByName,
refreshCorrespondents,
handleCorrespondentCreate,
handleCorrespondentUpdate,
@@ -1,4 +1,4 @@
import { useCallback, useMemo } from 'react';
import { useCallback } from 'react';
import { useStatusToast } from '../../lib/context/StatusToastContext';
import type { Identifier } from '../../types/identifiers';
import type { Correspondent } from '../../types/documents';
@@ -21,22 +21,12 @@ const useDocumentCorrespondentMutations = ({
const notifyApiError = useNotifyApiError();
const {
correspondents,
correspondentManager,
correspondentLookupByName,
} = correspondentsState;
const { documentsManager } = documentsState;
const correspondentLookupByName = useMemo(() => {
const map = new Map<string, Correspondent>();
correspondents.forEach((correspondent) => {
if (correspondent?.name) {
map.set(correspondent.name.toLowerCase(), correspondent);
}
});
return map;
}, [correspondents]);
const handleDocumentCorrespondentAttach = useCallback(
async (
{
@@ -17,7 +17,6 @@ import type {
SelectionState,
TagsState,
CorrespondentsState,
ActionsState,
} from '../types/workspaceTypes';
import type { Tag, Correspondent } from '../../types/documents';
import useDocumentCorrespondentMutations from './useDocumentCorrespondentMutations';
@@ -36,7 +35,7 @@ interface UseDocumentMutationsArgs {
selectionState: SelectionState;
tagsState: TagsState;
correspondentsState: CorrespondentsState;
actions: ActionsState;
closeDocumentPreview: () => void;
previewDocumentId?: DocumentId | null;
}
@@ -68,7 +67,6 @@ interface UseDocumentMutationsResult {
handleDocumentCorrespondentAttach: (args: { documentId: DocumentId; correspondentId: DocumentId; correspondent?: Correspondent | Partial<Correspondent> | null }) => Promise<boolean>;
handleDocumentCorrespondentDetach: (args: { documentId: DocumentId; correspondentId: DocumentId }) => Promise<boolean>;
handleDocumentCorrespondentAdd: (args: { document: { id?: string }; name?: string; input?: HTMLInputElement | null; option?: Correspondent | Partial<Correspondent> | string | null }) => Promise<void>;
correspondentLookupByName: Map<string, Correspondent>;
}
const useDocumentMutations = ({
@@ -77,7 +75,7 @@ const useDocumentMutations = ({
selectionState,
tagsState,
correspondentsState,
actions,
closeDocumentPreview,
previewDocumentId,
}: UseDocumentMutationsArgs): UseDocumentMutationsResult => {
const { showToast } = useStatusToast();
@@ -102,7 +100,6 @@ const useDocumentMutations = ({
handleDocumentCorrespondentAttach,
handleDocumentCorrespondentDetach,
handleDocumentCorrespondentAdd,
correspondentLookupByName,
} = useDocumentCorrespondentMutations({
correspondentsState,
documentsState: { documentsManager: documentsState.documentsManager },
@@ -115,13 +112,13 @@ const useDocumentMutations = ({
showToast('Analysis queued.', 'info');
// Close preview if it's the current one to allow refresh?
if (previewDocumentId === documentId) {
actions.closeDocumentPreview();
closeDocumentPreview();
}
} catch (error) {
notifyApiError(error, 'Failed to queue analysis.');
}
},
[actions, notifyApiError, previewDocumentId, showToast],
[closeDocumentPreview, notifyApiError, previewDocumentId, showToast],
);
const handleDocumentsDelete = useCallback(
@@ -240,7 +237,6 @@ const useDocumentMutations = ({
handleDocumentCorrespondentAttach,
handleDocumentCorrespondentDetach,
handleDocumentCorrespondentAdd,
correspondentLookupByName,
};
};
@@ -273,6 +273,13 @@ const useDocumentsWorkspace = ({
foldersManager,
};
const resolveFolderPath = useCallback(
(folderId) => {
return resolveBreadcrumbs(folderId || 'root', folderNodes as any);
},
[folderNodes],
);
const [currentSubfolders, setCurrentSubfolders] = useState<Array<{ id?: FolderNodeId; name?: string | null;[key: string]: unknown }>>([]);
const foldersSnapshot = useSyncExternalStore(
@@ -384,18 +391,7 @@ const useDocumentsWorkspace = ({
}
}, [selectedFolder, documentsSortField, documentsSortDirection, fetchFolderData, updateViewState, notifyApiError]);
const {
searchQuery,
setSearchQuery,
searchResultIds,
setSearchResultIds,
searchLoading,
activeTagFilters,
setActiveTagFilters,
activeCorrespondentFilters,
setActiveCorrespondentFilters,
documentsFilterValue,
} = useDocumentsSearch({
const documentsSearch = useDocumentsSearch({
api: apiClient,
selectedFolder,
locationPathname: location.pathname,
@@ -407,8 +403,8 @@ const useDocumentsWorkspace = ({
documentsManager,
});
const documentsFilter = documentsFilterValue;
const showingSearchResults = searchResultIds !== null;
const documentsFilter = documentsSearch.documentsFilterValue;
const showingSearchResults = documentsSearch.searchResultIds !== null;
// Live Filter: Ensure we only show documents that actually belong to the current folder.
// Since 'documents' is reactive, if a document is moved, its folder_id updates immediately.
@@ -431,7 +427,7 @@ const useDocumentsWorkspace = ({
} = useWorkspaceViewData({
documents: liveFilteredDocuments,
documentLookup,
searchResultIds,
searchResultIds: documentsSearch.searchResultIds,
showingSearchResults,
currentSubfolders: visibleSubfolders,
});
@@ -440,6 +436,8 @@ const useDocumentsWorkspace = ({
openDocumentPreview,
closeDocumentPreview,
resetPreviewState,
previewWorkspaceDocument,
previewActive,
} = useDocumentPreview({
routeDocumentId: previewDocumentId,
documentsManager,
@@ -471,7 +469,7 @@ const useDocumentsWorkspace = ({
useWorkspaceSelectionSync({
showingSearchResults,
searchQuery,
searchQuery: documentsSearch.searchQuery,
setSelectedEntries,
setSelectionOrder,
selectionOrderRef,
@@ -486,7 +484,7 @@ const useDocumentsWorkspace = ({
const tagsState = useTags({
tenantIdRef,
tagManager,
setActiveTagFilters,
setActiveTagFilters: documentsSearch.setActiveTagFilters,
documentsManager,
});
@@ -584,10 +582,10 @@ const useDocumentsWorkspace = ({
selectionAnchorRef.current = null;
setDraggedDocumentIds([]);
setDraggedFolderId(null);
setSearchResultIds(null);
setSearchQuery('');
setActiveTagFilters([]);
setActiveCorrespondentFilters([]);
documentsSearch.setSearchResultIds(null);
documentsSearch.setSearchQuery('');
documentsSearch.setActiveTagFilters([]);
documentsSearch.setActiveCorrespondentFilters([]);
setActivePreviewId(null);
detailPanelControlRef.current.close();
assetManager.reset();
@@ -612,10 +610,7 @@ const useDocumentsWorkspace = ({
setDocuments,
setDraggedDocumentIds,
setDraggedFolderId,
setSearchResultIds,
setSearchQuery,
setActiveTagFilters,
setActiveCorrespondentFilters,
documentsSearch,
setActivePreviewId,
resetPreviewState,
upload,
@@ -631,16 +626,12 @@ const useDocumentsWorkspace = ({
const documentsState = {
documentLookup,
setDocuments,
setSearchResultIds,
setSearchResultIds: documentsSearch.setSearchResultIds,
documentsManager,
extractDocumentFromResponse,
ingestDocuments: (docs: unknown[]) => documentsManager.ingest(docs),
};
const actionsState = {
closeDocumentPreview,
};
const documentMutationsResult = useDocumentMutations({
documentsState,
folderState,
@@ -653,13 +644,12 @@ const useDocumentsWorkspace = ({
...correspondentsState,
correspondentManager,
},
actions: actionsState,
closeDocumentPreview,
previewDocumentId,
});
const {
moveDocumentsToFolder,
handleThumbnailRegeneration,
handleDocumentsDelete,
handleDocumentTagAdd,
handleDocumentTagAttach,
@@ -669,7 +659,6 @@ const useDocumentsWorkspace = ({
handleDocumentCorrespondentAttach,
handleDocumentCorrespondentDetach,
handleDocumentCorrespondentAdd,
correspondentLookupByName,
} = documentMutationsResult;
const mutations = {
@@ -802,7 +791,7 @@ const useDocumentsWorkspace = ({
handleDeleteSelection,
} = useBulkDocumentActions({
resolveTargetDocumentIds,
correspondentLookupByName,
correspondentLookupByName: correspondentsState.correspondentLookupByName,
handleCorrespondentCreate: correspondentsState.handleCorrespondentCreate,
selectedDocumentIds,
selectedFolderIds,
@@ -874,7 +863,8 @@ const useDocumentsWorkspace = ({
onTagDelete: async (tagId: string) => { await tagsState.handleTagDelete(tagId); },
correspondents: correspondentsState.correspondents,
correspondentLookupById: correspondentsState.correspondentLookupById,
refreshCorrespondents: correspondentsState.refreshCorrespondents,
correspondentLookupByName: correspondentsState.correspondentLookupByName,
refreshCorrespondents,
onCorrespondentCreate: correspondentsState.handleCorrespondentCreate,
onCorrespondentUpdate: correspondentsState.handleCorrespondentUpdate,
onCorrespondentDelete: correspondentsState.handleCorrespondentDelete,
@@ -903,21 +893,11 @@ const useDocumentsWorkspace = ({
return () => window.removeEventListener('keydown', handleKeyDown);
}, [settingsOpen]);
const {
detailPanelProps,
detailPanelOpen,
openDetailPanel,
previewActive,
previewWorkspaceDocument,
resolveFolderPath,
} = useDetailWorkspace({
documents: viewDocuments,
const detailPanel = useDetailWorkspace({
documentLookup,
folderNodes,
detailPanelControlRef,
detailFolderFetchRef,
previewDocumentId,
activePreviewId,
openDocumentPreview: openDocumentPreviewForDetail,
handleDocumentTitleUpdate,
handleDocumentIssuedUpdate,
@@ -932,6 +912,7 @@ const useDocumentsWorkspace = ({
tags: tagsState.tags,
tagLookupById: tagsState.tagLookupById,
correspondentLookupById: correspondentsState.correspondentLookupById,
resolveFolderPath,
});
const handleEntryPointerCore = useEntryPointerCore({
@@ -985,7 +966,7 @@ const useDocumentsWorkspace = ({
const tags = {
...tagsState,
tagManager,
activeTagFilters,
activeTagFilters: documentsSearch.activeTagFilters,
// Add derived/action handlers that were previously in tagsContext
handleDocumentTagAttach,
handleDocumentTagDetach,
@@ -996,7 +977,7 @@ const useDocumentsWorkspace = ({
const correspondents = {
...correspondentsState,
activeCorrespondentFilters,
activeCorrespondentFilters: documentsSearch.activeCorrespondentFilters,
// Add derived/action handlers
handleDocumentCorrespondentAttach,
handleDocumentCorrespondentDetach,
@@ -1013,26 +994,19 @@ const useDocumentsWorkspace = ({
closeDocumentPreview,
ensureAssetUrl,
getDocumentAsset,
handleThumbnailRegeneration,
};
const detailPanel = {
detailPanelProps,
detailPanelOpen,
openDetailPanel,
};
const search = {
searchQuery,
searchQuery: documentsSearch.searchQuery,
documentsFilter,
searchLoading,
searchLoading: documentsSearch.searchLoading,
documentsViewMode,
handleDocumentsViewModeChange,
documentsSortField,
documentsSortDirection,
handleDocumentsSortFieldChange,
handleDocumentsSortDirectionToggle,
searchResultIds,
searchResultIds: documentsSearch.searchResultIds,
documents: viewDocuments,
};
@@ -22,8 +22,6 @@ export interface DocumentsManagerInterface {
remove(ids: Array<DocumentId>): boolean;
}
type CloseDocumentPreview = () => void;
export interface DocumentsState {
documentLookup: Map<DocumentId, Document>;
setDocuments: Dispatch<SetStateAction<Document[]>>;
@@ -62,16 +60,11 @@ export interface TagsState {
export interface CorrespondentsState {
correspondents: Correspondent[];
correspondentLookupById: Map<Identifier, Correspondent>;
correspondentLookupByName?: Map<string, Correspondent>;
refreshCorrespondents: () => Promise<void>;
correspondentManager: CorrespondentManager;
}
export interface ActionsState {
closeDocumentPreview: CloseDocumentPreview;
handleFileDrop?: (dataTransfer: DataTransfer, folderId: FolderId) => Promise<void> | void;
moveDocumentsToFolder?: (docIds: FolderId[], folderId: FolderId) => Promise<void>;
}
export interface DragState {
draggedDocumentIds: DocumentId[];
draggedFolderId: FolderId | null;
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo } from 'react';
import { useCallback, useEffect } from 'react';
import type { MutableRefObject } from 'react';
import { resolveDocumentAssetUrl } from '../../lib/assets/AssetManager';
import { useDetailPanel } from '../../app/useDetailPanel';
@@ -6,7 +6,6 @@ import type { DocumentInfoPanelProps } from '../components/DocumentInfoPanel';
import type { EnsureAssetUrl, GetAsset } from '../../lib/assets/AssetManager';
import type { Identifier } from '../../types/identifiers';
import type { Document } from '../../types/documents';
import { resolveBreadcrumbs } from '../../documents/logic/breadcrumbs';
import type { Tag, Correspondent } from '../../types/documents';
import { listFolderContents } from '../../lib/api/apiClient';
import { useStatusToast } from '../../lib/context/StatusToastContext';
@@ -19,13 +18,10 @@ interface FolderNode {
}
interface UseDetailWorkspaceArgs {
documents: Document[];
documentLookup: Map<Identifier, Document>;
folderNodes: Map<Identifier | 'root', FolderNode>;
detailPanelControlRef: MutableRefObject<{ open?: (documentId: Identifier) => void; close?: () => void } | null>;
detailFolderFetchRef: MutableRefObject<Set<Identifier | 'root'>>;
previewDocumentId?: Identifier | null;
activePreviewId?: Identifier | null;
openDocumentPreview?: (args: { documentIds: Identifier[] }) => void;
handleDocumentTitleUpdate?: (docId: Identifier, title: string) => Promise<boolean> | boolean;
handleDocumentIssuedUpdate?: (docId: Identifier, issued: number | null) => Promise<boolean> | boolean;
@@ -40,6 +36,7 @@ interface UseDetailWorkspaceArgs {
tags?: unknown[];
tagLookupById?: Map<Identifier, Tag> | null;
correspondentLookupById?: Map<Identifier, Correspondent> | null;
resolveFolderPath?: (folderId?: Identifier | 'root') => Array<{ id: Identifier | 'root'; name: string }>;
}
interface UseDetailWorkspaceResult {
@@ -49,20 +46,15 @@ interface UseDetailWorkspaceResult {
closeDetailPanel: ReturnType<typeof useDetailPanel>['closeDetailPanel'];
handleDetailPanelClose: () => void;
inspectDocument: (docId: Identifier | null) => void;
previewActive: boolean;
previewWorkspaceDocument: Document | null;
resolveThumbnailUrlForDoc: (doc: Document | null) => string | null;
resolveFolderPath: (folderId?: Identifier | 'root') => Array<{ id: Identifier | 'root'; name: string }>;
}
const useDetailWorkspace = ({
documents,
documentLookup,
folderNodes,
detailPanelControlRef,
detailFolderFetchRef,
previewDocumentId,
activePreviewId,
openDocumentPreview,
handleDocumentTitleUpdate,
handleDocumentIssuedUpdate,
@@ -77,6 +69,7 @@ const useDetailWorkspace = ({
tags,
tagLookupById,
correspondentLookupById,
resolveFolderPath,
}: UseDetailWorkspaceArgs): UseDetailWorkspaceResult => {
const { showToast } = useStatusToast();
const notifyApiError = useNotifyApiError();
@@ -97,12 +90,6 @@ const useDetailWorkspace = ({
};
}, [detailPanelControlRef, openDetailPanel, closeDetailPanel]);
useEffect(() => {
if (detailPanelOpen && activePreviewId) {
openDetailPanel(activePreviewId);
}
}, [detailPanelOpen, activePreviewId, openDetailPanel]);
// Prefetch folder ancestors for breadcrumb display
useEffect(() => {
const folderId = detailPanelDocument?.folder_id;
@@ -144,24 +131,6 @@ const useDetailWorkspace = ({
}
}, [detailPanelDocument, folderNodes, detailFolderFetchRef]);
const resolveFolderPath = useCallback(
(folderId) => {
return resolveBreadcrumbs(folderId || 'root', folderNodes as any);
},
[folderNodes],
);
const previewWorkspaceDocument = useMemo(() => {
if (!previewDocumentId) {
return null;
}
return documentLookup.get(previewDocumentId)
|| documents.find((doc) => doc.id === previewDocumentId)
|| null;
}, [previewDocumentId, documentLookup, documents]);
const previewActive = Boolean(previewDocumentId && previewWorkspaceDocument);
const resolveThumbnailUrlForDoc = useCallback(
(doc) =>
resolveDocumentAssetUrl(doc, 'thumbnail', {
@@ -219,7 +188,6 @@ const useDetailWorkspace = ({
onTagAdd: handleDetailTagAdd,
onTagRemove: handleDetailTagRemove,
onOpenPreview: openDocumentPreview,
activePreviewId,
onUpdateTitle: handleDocumentTitleUpdate,
onUpdateIssued: handleDocumentIssuedUpdate,
ensureAssetUrl,
@@ -240,8 +208,6 @@ const useDetailWorkspace = ({
closeDetailPanel,
handleDetailPanelClose,
inspectDocument,
previewActive,
previewWorkspaceDocument,
resolveThumbnailUrlForDoc,
resolveFolderPath,
};