From b76f1df27fcff6c0683828ba562825ea4a16986a Mon Sep 17 00:00:00 2001 From: Nils Schneider Date: Wed, 19 Nov 2025 23:36:04 +0100 Subject: [PATCH] documentsmanager --- frontend/package-lock.json | 39 ++++ frontend/package.json | 1 + frontend/src/app/useDocumentPreview.ts | 117 ++-------- frontend/src/app/useDocumentsSearch.ts | 43 ++-- frontend/src/app/useWorkspaceSurface.tsx | 4 +- frontend/src/desktop/DesktopWorkspace.tsx | 12 +- frontend/src/detail/useDetailWorkspace.ts | 18 +- frontend/src/documents/DocumentsManager.ts | 200 +++++++++++++++++ .../context/DocumentsFilterContext.tsx | 2 +- .../documents/hooks/useDocumentsPanelProps.ts | 8 +- .../src/documents/panel/DocumentsPanel.tsx | 21 +- .../hooks/documents/useDocumentMutations.ts | 10 +- frontend/src/hooks/documents/useDocuments.ts | 126 ++++++++--- .../hooks/documents/useDocumentsWorkspace.ts | 211 +++++++++--------- .../hooks/documents/useFolderTreeActions.ts | 8 +- frontend/src/preview/DocumentViewerPanel.tsx | 41 +++- frontend/src/routes/DocumentViewerRoute.tsx | 3 +- 17 files changed, 564 insertions(+), 300 deletions(-) create mode 100644 frontend/src/documents/DocumentsManager.ts diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 7fa0ea4..a8365a8 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -14,6 +14,7 @@ "pdfjs-dist": "^5.4.394", "react": "^19.2.0", "react-dom": "^19.2.0", + "react-redux": "^9.2.0", "react-router-dom": "^7.9.5" }, "devDependencies": { @@ -3094,6 +3095,12 @@ "@types/node": "*" } }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@types/ws": { "version": "8.18.1", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", @@ -8732,6 +8739,29 @@ "dev": true, "license": "MIT" }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-router": { "version": "7.9.5", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.5.tgz", @@ -10489,6 +10519,15 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 3b2c8cf..7f20a5f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,6 +16,7 @@ "pdfjs-dist": "^5.4.394", "react": "^19.2.0", "react-dom": "^19.2.0", + "react-redux": "^9.2.0", "react-router-dom": "^7.9.5" }, "devDependencies": { diff --git a/frontend/src/app/useDocumentPreview.ts b/frontend/src/app/useDocumentPreview.ts index 9b59cfa..8477a3e 100644 --- a/frontend/src/app/useDocumentPreview.ts +++ b/frontend/src/app/useDocumentPreview.ts @@ -36,8 +36,13 @@ type NavigateHandler = (path: string, options?: { replace?: boolean }) => void; interface UseDocumentPreviewArgs { routeDocumentId?: DocumentId | null; - documents: DocumentLike[]; - searchResults: DocumentLike[] | null; + documentsManager: { + getById: (id: DocumentId) => DocumentLike | null; + ensure: (id: DocumentId) => Promise; + getMany: (ids: DocumentId[]) => DocumentLike[]; + subscribe: (listener: () => void) => () => void; + ingest: (docs: unknown[]) => { canonical: DocumentLike[]; changed: boolean }; + }; selectedFolder?: FolderId | null; assetManager: AssetManagerLike; api: ApiClient; @@ -55,7 +60,6 @@ interface UseDocumentPreviewArgs { interface UseDocumentPreviewResult { documentLinks: Map; - previewDocuments: Map; ensureDownloadUrl: (documentId: DocumentId, options?: { force?: boolean }) => Promise; ensurePreviewData: (documentId: DocumentId) => Promise; openDocumentPreview: (documentId: DocumentId, options?: { replace?: boolean }) => void; @@ -66,10 +70,9 @@ interface UseDocumentPreviewResult { const useDocumentPreview = ({ routeDocumentId, - documents, - searchResults, + documentsManager, selectedFolder, - assetManager, + assetManager: _assetManager, api, resolveApiPath, notifyApiError, @@ -80,7 +83,6 @@ const useDocumentPreview = ({ setActivePreviewId, }: UseDocumentPreviewArgs): UseDocumentPreviewResult => { const [documentLinks, setDocumentLinks] = useState>(() => new Map()); - const [previewDocuments, setPreviewDocuments] = useState>(() => new Map()); const previewInflightRef = useRef>>(new Map()); const previewReturnPathRef = useRef(null); @@ -110,35 +112,6 @@ const useDocumentPreview = ({ }); }, []); - const cachePreviewDocument = useCallback((doc: DocumentLike) => { - if (!doc?.id) { - return; - } - setPreviewDocuments((prev) => { - const existing = prev.get(doc.id); - if (existing === doc) { - return prev; - } - const next = new Map(prev); - next.set(doc.id, doc); - return next; - }); - }, []); - - const removeCachedPreviewDocument = useCallback((documentId: DocumentId) => { - if (!documentId) { - return; - } - setPreviewDocuments((prev) => { - if (!prev.has(documentId)) { - return prev; - } - const next = new Map(prev); - next.delete(documentId); - return next; - }); - }, []); - const ensureDownloadUrl = useCallback( async (documentId: DocumentId, { force = false }: { force?: boolean } = {}): Promise => { if (!documentId) return null; @@ -193,32 +166,22 @@ const useDocumentPreview = ({ async (documentId: DocumentId): Promise => { if (!documentId) return null; - const findInCache = () => { - const pool = searchResults ?? documents; - return pool.find((item) => item.id === documentId) || null; - }; + const findInCache = () => documentsManager.getById(documentId); let doc = findInCache(); + if (!doc) { + doc = await documentsManager.ensure(documentId); + } + if (!doc) { const { data } = await api.get(`/documents/${documentId}`); - const hydratedDetail = assetManager.hydrateDetail(data); - const fetched = hydratedDetail?.document || (data as { document?: DocumentLike })?.document || data; - doc = fetched ? assetManager.hydrateDocument(fetched) || null : null; + const fetched = (data as { document?: DocumentLike })?.document || data; + const { canonical } = documentsManager.ingest([fetched as unknown]); + doc = (canonical[0] as DocumentLike | undefined) || null; if (!doc) { throw new Error('Document metadata unavailable.'); } - - const existsInDocuments = documents.some((item) => item.id === doc?.id); - const existsInSearch = Array.isArray(searchResults) - ? searchResults.some((item) => item.id === doc?.id) - : false; - - if (doc?.id && (existsInDocuments || existsInSearch)) { - removeCachedPreviewDocument(doc.id); - } else { - cachePreviewDocument(doc); - } } if (!previewReturnPathRef.current) { @@ -232,14 +195,10 @@ const useDocumentPreview = ({ return doc; }, [ - searchResults, - documents, - assetManager, + documentsManager, ensureDownloadUrl, setActivePreviewId, api, - cachePreviewDocument, - removeCachedPreviewDocument, ], ); @@ -270,21 +229,6 @@ const useDocumentPreview = ({ [navigate, selectedFolder], ); - useEffect(() => { - if (!routeDocumentId) { - return undefined; - } - - const handleKeyDown = (event: KeyboardEvent) => { - if (event.key === 'Escape') { - closeDocumentPreview(); - } - }; - - window.addEventListener('keydown', handleKeyDown); - return () => window.removeEventListener('keydown', handleKeyDown); - }, [routeDocumentId, closeDocumentPreview]); - useEffect(() => { if (!routeDocumentId) { return undefined; @@ -305,33 +249,8 @@ const useDocumentPreview = ({ }; }, [routeDocumentId, ensurePreviewData, notifyApiError, closeDocumentPreview]); - useEffect(() => { - setPreviewDocuments((prev) => { - if (!prev.size) { - return prev; - } - const next = new Map(prev); - let changed = false; - const prune = (list?: DocumentLike[] | null) => { - if (!Array.isArray(list)) { - return; - } - list.forEach((doc) => { - if (doc?.id && next.has(doc.id)) { - next.delete(doc.id); - changed = true; - } - }); - }; - prune(documents); - prune(searchResults); - return changed ? next : prev; - }); - }, [documents, searchResults]); - return { documentLinks, - previewDocuments, ensureDownloadUrl, ensurePreviewData, openDocumentPreview, diff --git a/frontend/src/app/useDocumentsSearch.ts b/frontend/src/app/useDocumentsSearch.ts index b0c2b12..5dbbff4 100644 --- a/frontend/src/app/useDocumentsSearch.ts +++ b/frontend/src/app/useDocumentsSearch.ts @@ -10,13 +10,8 @@ type ApiClient = { get: (url: string, config?: { params?: Record }) => Promise<{ data: T }>; }; -type AssetManagerLike = { - hydrateDocuments: (payload: unknown[]) => DocumentLike[]; -}; - interface UseDocumentsSearchArgs { api: ApiClient; - assetManager: AssetManagerLike; token?: string | null; selectedFolder?: Identifier | 'root' | null; navigate?: (path: string, options?: { replace?: boolean }) => void; @@ -28,13 +23,16 @@ interface UseDocumentsSearchArgs { notifyApiError: (error: unknown, message: string) => void; setLoading: (state: boolean) => void; setSearchIncludeDescendants: (value: boolean) => void; + documentsManager: { + ingest: (docs: unknown[]) => { canonical: DocumentLike[]; changed: boolean }; + }; } interface UseDocumentsSearchResult { searchQuery: string; setSearchQuery: Dispatch>; - searchResults: DocumentLike[] | null; - setSearchResults: Dispatch>; + searchResultIds: Identifier[] | null; + setSearchResultIds: Dispatch>; searchLoading: boolean; setSearchLoading: Dispatch>; activeTagFilters: Identifier[]; @@ -49,7 +47,7 @@ interface UseDocumentsSearchResult { handleSearchSubmit: () => void; documentsFilterValue: { query: string; - searchResults: DocumentLike[] | null; + searchResultIds: Identifier[] | null; searchLoading: boolean; includeDescendants: boolean; activeTagIds: Identifier[]; @@ -66,7 +64,6 @@ interface UseDocumentsSearchResult { const useDocumentsSearch = ({ api, - assetManager, token, selectedFolder, navigate, @@ -78,11 +75,12 @@ const useDocumentsSearch = ({ notifyApiError, setLoading, setSearchIncludeDescendants, + documentsManager, }: UseDocumentsSearchArgs): UseDocumentsSearchResult => { const [searchQuery, setSearchQuery] = useState(''); const [activeTagFilters, setActiveTagFilters] = useState([]); const [activeCorrespondentFilters, setActiveCorrespondentFilters] = useState([]); - const [searchResults, setSearchResults] = useState(null); + const [searchResultIds, setSearchResultIds] = useState(null); const [searchLoading, setSearchLoading] = useState(false); const toggleTagFilter = useCallback((tagId: Identifier) => { @@ -122,6 +120,7 @@ const useDocumentsSearch = ({ setActiveCorrespondentFilters([]); setSearchLoading(false); setSearchIncludeDescendants(true); + setSearchResultIds(null); }, [ setSearchIncludeDescendants, ]); @@ -142,7 +141,7 @@ const useDocumentsSearch = ({ const documentsFilterValue = useMemo( () => ({ query: searchQuery, - searchResults, + searchResultIds, searchLoading, includeDescendants: Boolean(searchIncludeDescendants), activeTagIds: activeTagFilters, @@ -157,7 +156,7 @@ const useDocumentsSearch = ({ }), [ searchQuery, - searchResults, + searchResultIds, searchLoading, searchIncludeDescendants, activeTagFilters, @@ -176,7 +175,7 @@ const useDocumentsSearch = ({ if (!token) return undefined; if (!isFilterActive) { - setSearchResults(null); + setSearchResultIds(null); setSearchLoading(false); return undefined; } @@ -225,17 +224,21 @@ const useDocumentsSearch = ({ const { data } = await api.get('/documents', { params }); if (cancelled) return; - const results = assetManager.hydrateDocuments(data || []); - setSearchResults(results); + const results = Array.isArray(data) ? data : []; + const { canonical } = documentsManager.ingest(results); + const ids = canonical + .map((doc) => (doc?.id ?? null) as Identifier | null) + .filter((id): id is Identifier => id != null); + setSearchResultIds(ids); - if (!results.length) { + if (!ids.length) { setSearchLoading(false); return; } } catch (error) { if (cancelled) return; notifyApiError(error, 'Search failed. Please try again.'); - setSearchResults(null); + setSearchResultIds(null); } finally { if (!cancelled && started) { setLoading(false); @@ -264,15 +267,15 @@ const useDocumentsSearch = ({ documentsSortDirection, selectedFolder, notifyApiError, - assetManager, setLoading, + documentsManager, ]); return { searchQuery, setSearchQuery, - searchResults, - setSearchResults, + searchResultIds, + setSearchResultIds, searchLoading, setSearchLoading, activeTagFilters, diff --git a/frontend/src/app/useWorkspaceSurface.tsx b/frontend/src/app/useWorkspaceSurface.tsx index 4e98b78..99ed49a 100644 --- a/frontend/src/app/useWorkspaceSurface.tsx +++ b/frontend/src/app/useWorkspaceSurface.tsx @@ -123,7 +123,7 @@ export const useWorkspaceSurface = ({ const showPreviewWorkspace = Boolean(previewDocumentId); const previewSurface = useMemo(() => { - if (!showPreviewWorkspace || !previewWorkspaceDocument) { + if (!showPreviewWorkspace) { return null; } @@ -145,7 +145,7 @@ export const useWorkspaceSurface = ({ return { content: ( void; onEntryPointer?: (...args: unknown[]) => void; onDocumentStackSelect?: (docIds: Identifier[]) => void; @@ -195,7 +194,6 @@ const defaultGetDocumentAsset: GetAsset = () => null; const DesktopWorkspace: React.FC = ({ documents = [], - searchResults = null, onInspectDocument = null, onEntryPointer = null, onDocumentStackSelect = null, @@ -211,10 +209,7 @@ const DesktopWorkspace: React.FC = ({ documentLinks, ensureDownloadUrl, }) => { - const items = useMemo( - () => (searchResults ? searchResults : documents), - [documents, searchResults], - ); + const items = useMemo(() => documents, [documents]); const allowLayoutPersistence = Boolean(tenantId && viewId && viewId.startsWith('folder:')); const documentLinkMap = documentLinks instanceof Map ? documentLinks : null; @@ -264,9 +259,8 @@ const DesktopWorkspace: React.FC = ({ }, [engine, allowLayoutPersistence, tenantId, viewId]); useEffect(() => { - const nextItems = searchResults ? searchResults : documents; - engine.setItems(nextItems || []); - }, [engine, documents, searchResults]); + engine.setItems(items || []); + }, [engine, items]); useEffect(() => { const map = new Map(); diff --git a/frontend/src/detail/useDetailWorkspace.ts b/frontend/src/detail/useDetailWorkspace.ts index f0256c7..a2be9d3 100644 --- a/frontend/src/detail/useDetailWorkspace.ts +++ b/frontend/src/detail/useDetailWorkspace.ts @@ -32,8 +32,6 @@ type DocumentLink = { interface UseDetailWorkspaceArgs { documents: DocumentLike[]; - searchResults?: DocumentLike[] | null; - previewDocuments?: Map | null; selectionOrder: string[]; selectedDocumentIds: Identifier[]; documentLookup: Map; @@ -77,8 +75,6 @@ interface UseDetailWorkspaceResult { const useDetailWorkspace = ({ documents, - searchResults, - previewDocuments, selectionOrder, selectedDocumentIds, documentLookup, @@ -245,17 +241,19 @@ const useDetailWorkspace = ({ [folderNodes], ); - const documentLink = useMemo(() => (detailPanelDocument ? documentLinks.get(detailPanelDocument.id) || null : null), [detailPanelDocument, documentLinks]); + const documentLink = useMemo( + () => (detailPanelDocument ? documentLinks.get(detailPanelDocument.id) || null : null), + [detailPanelDocument, documentLinks], + ); - const previewWorkspaceDocument = useMemo(() => { + const previewWorkspaceDocument = useMemo(() => { if (!previewDocumentId) { return null; } - const pool = searchResults ?? documents; - return pool.find((doc) => doc.id === previewDocumentId) - || previewDocuments?.get?.(previewDocumentId) + return documentLookup.get(previewDocumentId) + || documents.find((doc) => doc.id === previewDocumentId) || null; - }, [previewDocumentId, searchResults, documents, previewDocuments]); + }, [previewDocumentId, documentLookup, documents]); const previewActive = Boolean(previewDocumentId && previewWorkspaceDocument); diff --git a/frontend/src/documents/DocumentsManager.ts b/frontend/src/documents/DocumentsManager.ts new file mode 100644 index 0000000..c1c8e8a --- /dev/null +++ b/frontend/src/documents/DocumentsManager.ts @@ -0,0 +1,200 @@ +import { shallowEqual } from 'react-redux'; + +type DocumentId = string | number; + +export type ManagedDocument = { id?: DocumentId | null } & Record; + +type FetchDocument = (id: DocumentId) => Promise; +type HydrateDocument = (payload: unknown) => T | null; +type HydrateDocuments = (payload: unknown[]) => T[]; + +class DocumentsManager { + private byId: Map; + + private fetcher?: FetchDocument; + + private hydrateDocument?: HydrateDocument; + + private hydrateDocuments?: HydrateDocuments; + + private extractDocument?: HydrateDocument; + + private inflight: Map>; + + private listeners: Set<() => void>; + + constructor( + fetchDocument?: FetchDocument, + options?: { + hydrateDocument?: HydrateDocument; + hydrateDocuments?: HydrateDocuments; + extractDocument?: HydrateDocument; + }, + ) { + this.byId = new Map(); + this.fetcher = fetchDocument; + this.hydrateDocument = options?.hydrateDocument; + this.hydrateDocuments = options?.hydrateDocuments; + this.extractDocument = options?.extractDocument; + this.inflight = new Map(); + this.listeners = new Set(); + } + + private emit() { + this.listeners.forEach((fn) => fn()); + } + + subscribe(listener: () => void) { + this.listeners.add(listener); + return () => this.listeners.delete(listener); + } + + setFetcher(fetchDocument?: FetchDocument) { + this.fetcher = fetchDocument; + } + + ingest(rawDocs: unknown[] = []): { canonical: T[]; changed: boolean } { + const normalize = (docs: unknown[]) => { + if (this.hydrateDocuments) { + return this.hydrateDocuments(docs).filter(Boolean) as T[]; + } + if (this.hydrateDocument) { + return docs + .map((doc) => this.hydrateDocument ? this.hydrateDocument(doc) : (doc as T | null)) + .filter(Boolean) as T[]; + } + return docs.filter(Boolean) as T[]; + }; + + const docs = normalize(rawDocs); + let changed = false; + let nextById = this.byId; + const canonical: T[] = []; + + docs.forEach((doc) => { + const id = doc?.id; + if (id == null) { + canonical.push(doc); + return; + } + + const existing = nextById.get(id as DocumentId); + const merged = existing ? ({ ...existing, ...doc } as T) : ({ ...(doc as T) } as T); + const useExisting = existing && shallowEqual(existing, merged); + const nextDoc = useExisting ? (existing as T) : merged; + + if (!useExisting) { + if (!changed) { + nextById = new Map(this.byId); + } + nextById.set(id as DocumentId, nextDoc); + changed = true; + } + canonical.push(nextDoc); + }); + + if (changed) { + this.byId = nextById; + this.emit(); + } + + return { canonical, changed }; + } + + async ensure(id: DocumentId, fetcherOverride?: FetchDocument): Promise { + if (id == null) { + return null; + } + + const cached = this.byId.get(id); + if (cached) { + return cached; + } + + const fetcher = fetcherOverride || this.fetcher; + if (!fetcher) { + return null; + } + + const inflight = this.inflight.get(id); + if (inflight) { + return inflight; + } + + const request = (async () => { + try { + const fetched = await fetcher(id); + const extracted = this.extractDocument ? this.extractDocument(fetched) : fetched; + const { canonical } = this.ingest([extracted as unknown]); + return canonical[0] ?? null; + } finally { + this.inflight.delete(id); + } + })(); + + this.inflight.set(id, request); + return request; + } + + map(mapper: (doc: T) => T | undefined): boolean { + if (!this.byId.size) { + return false; + } + + let changed = false; + const next = new Map(); + this.byId.forEach((doc, key) => { + const updated = mapper(doc); + const nextDoc = updated === undefined ? doc : updated; + if (nextDoc !== doc) { + changed = true; + } + next.set(key, nextDoc ?? doc); + }); + + if (changed) { + this.byId = next; + this.emit(); + } + + return changed; + } + + remove(ids: Array): boolean { + if (!Array.isArray(ids) || ids.length === 0) { + return false; + } + let changed = false; + let next = this.byId; + ids.forEach((id) => { + if (next.has(id)) { + if (!changed) { + next = new Map(this.byId); + } + next.delete(id); + changed = true; + } + }); + if (changed) { + this.byId = next; + this.emit(); + } + return changed; + } + + getById(id: DocumentId): T | null { + return this.byId.get(id) ?? null; + } + + getMany(ids: Array = []): T[] { + return ids + .map((id) => this.byId.get(id) || null) + .filter((doc): doc is T => Boolean(doc)); + } + + getSnapshot(): Map { + return this.byId; + } +} + +export default DocumentsManager; diff --git a/frontend/src/documents/context/DocumentsFilterContext.tsx b/frontend/src/documents/context/DocumentsFilterContext.tsx index 93ca4fd..618b683 100644 --- a/frontend/src/documents/context/DocumentsFilterContext.tsx +++ b/frontend/src/documents/context/DocumentsFilterContext.tsx @@ -4,7 +4,7 @@ type Identifier = string | number; export interface DocumentsFilterValue { query: string; - searchResults: Array> | null; + searchResultIds: Array | null; searchLoading: boolean; includeDescendants: boolean; activeTagIds: Identifier[]; diff --git a/frontend/src/documents/hooks/useDocumentsPanelProps.ts b/frontend/src/documents/hooks/useDocumentsPanelProps.ts index 19751c4..7c687bd 100644 --- a/frontend/src/documents/hooks/useDocumentsPanelProps.ts +++ b/frontend/src/documents/hooks/useDocumentsPanelProps.ts @@ -27,7 +27,7 @@ export interface UseDocumentsPanelPropsArgs { refreshCurrentFolder?: () => void | Promise; currentSubfolders?: unknown[]; documents?: unknown[]; - searchResults?: unknown[] | null; + searchResultIds?: Identifier[] | null; folderClickHandlers: FolderClickHandlers; selectFolder?: (...args: unknown[]) => void; handleFolderDragStart?: (...args: unknown[]) => void; @@ -80,7 +80,7 @@ const useDocumentsPanelProps = (props: UseDocumentsPanelPropsArgs) => { refreshCurrentFolder, currentSubfolders, documents, - searchResults, + searchResultIds, folderClickHandlers, selectFolder, handleFolderDragStart, @@ -130,7 +130,7 @@ const useDocumentsPanelProps = (props: UseDocumentsPanelPropsArgs) => { onRefresh: refreshCurrentFolder, subfolders: currentSubfolders, documents, - searchResults, + searchResultIds, onFolderSelect: selectFolder, onFolderDrop: folderClickHandlers.onDrop, onFolderDragOver: folderClickHandlers.onDragOver, @@ -211,7 +211,7 @@ const useDocumentsPanelProps = (props: UseDocumentsPanelPropsArgs) => { openDocumentPreview, refreshCurrentFolder, searchLoading, - searchResults, + searchResultIds, selectFolder, tagLookupById, tags, diff --git a/frontend/src/documents/panel/DocumentsPanel.tsx b/frontend/src/documents/panel/DocumentsPanel.tsx index 97a8530..7a172a4 100644 --- a/frontend/src/documents/panel/DocumentsPanel.tsx +++ b/frontend/src/documents/panel/DocumentsPanel.tsx @@ -47,7 +47,7 @@ const DocumentsPanelInner: React.FC = ({ breadcrumbs, subfolders, documents, - searchResults, + searchResultIds, onFolderSelect, onFolderDrop, onFolderDragOver, @@ -104,10 +104,19 @@ const DocumentsPanelInner: React.FC = ({ toggleTag: toggleTagFilter, toggleCorrespondent: toggleCorrespondentFilter, } = useDocumentsFilter(); - const showingSearchResults = searchResults !== null; - const rows = showingSearchResults ? searchResults : documents; + const searchDocuments = useMemo( + () => + Array.isArray(searchResultIds) + ? searchResultIds + .map((id) => documentLookup?.get?.(id) || null) + .filter((doc): doc is Record => Boolean(doc)) + : null, + [searchResultIds, documentLookup], + ); + const showingSearchResults = Array.isArray(searchResultIds); + const rows = showingSearchResults && searchDocuments ? searchDocuments : documents; const documentLinkMap = documentLinks instanceof Map ? documentLinks : null; - const searchResultCount = Array.isArray(searchResults) ? searchResults.length : 0; + const searchResultCount = Array.isArray(searchResultIds) ? searchResultIds.length : 0; const headerTitle = showingSearchResults ? 'Search results' : currentFolderName || 'Documents'; @@ -201,7 +210,7 @@ const DocumentsPanelInner: React.FC = ({ const selectionContextRef = useRef(null); useEffect(() => { const nextContext = showingSearchResults - ? { type: 'search', marker: searchResults } + ? { type: 'search', marker: searchResultIds } : { type: 'folder', marker: currentFolderId || 'root' }; const previous = selectionContextRef.current; selectionContextRef.current = nextContext; @@ -213,7 +222,7 @@ const DocumentsPanelInner: React.FC = ({ if (changed) { clearSelection(); } - }, [showingSearchResults, currentFolderId, searchResults, clearSelection]); + }, [showingSearchResults, currentFolderId, searchResultIds, clearSelection]); const entries = useMemo(() => { const list = []; diff --git a/frontend/src/hooks/documents/useDocumentMutations.ts b/frontend/src/hooks/documents/useDocumentMutations.ts index 5734358..a82bd4d 100644 --- a/frontend/src/hooks/documents/useDocumentMutations.ts +++ b/frontend/src/hooks/documents/useDocumentMutations.ts @@ -114,7 +114,7 @@ interface UseDocumentMutationsArgs { setSelectedFolder: Dispatch>; setDocuments: Dispatch>; setFolderContents: Dispatch>>; - setSearchResults: Dispatch>; + setSearchResultIds: Dispatch>; setSelectedEntries: Dispatch>; setSelectionOrder: Dispatch>; selectionOrderRef: MutableRefObject; @@ -190,7 +190,7 @@ const useDocumentMutations = ({ setSelectedFolder, setDocuments, setFolderContents, - setSearchResults, + setSearchResultIds, setSelectedEntries, setSelectionOrder, selectionOrderRef, @@ -319,11 +319,11 @@ const useDocumentMutations = ({ } if (uniqueIdSet.size) { - setSearchResults((prev) => { + setSearchResultIds((prev) => { if (!Array.isArray(prev) || !prev.length) { return prev; } - const filtered = prev.filter((doc) => doc && !uniqueIdSet.has(doc.id as DocumentId)); + const filtered = prev.filter((id) => !uniqueIdSet.has(id as DocumentId)); return filtered.length === prev.length ? prev : filtered; }); setDocuments((prev) => prev.filter((doc) => !uniqueIdSet.has(doc.id as DocumentId))); @@ -387,7 +387,7 @@ const useDocumentMutations = ({ folderLabelMap, ensureFolderData, selectedFolder, - setSearchResults, + setSearchResultIds, setDocuments, setFolderContents, setSelectedEntries, diff --git a/frontend/src/hooks/documents/useDocuments.ts b/frontend/src/hooks/documents/useDocuments.ts index 98e99d1..412243d 100644 --- a/frontend/src/hooks/documents/useDocuments.ts +++ b/frontend/src/hooks/documents/useDocuments.ts @@ -1,7 +1,17 @@ -import { Dispatch, SetStateAction, useCallback, useState } from 'react'; +import { + Dispatch, + SetStateAction, + useCallback, + useEffect, + useRef, + useState, +} from 'react'; +import DocumentsManager from '../../documents/DocumentsManager'; + +type DocumentId = string | number; interface DocumentLike { - id?: string | number; + id?: DocumentId; [key: string]: unknown; } @@ -11,36 +21,74 @@ interface FolderContentsEntry { } interface UseDocumentsOptions { - setSearchResults: Dispatch>; setFolderContents: Dispatch>>; + fetchDocumentById?: (id: DocumentId) => Promise; + hydrateDocument?: (payload: unknown) => DocumentLike | null; + hydrateDocuments?: (payload: unknown[]) => DocumentLike[]; + extractDocument?: (payload: unknown) => DocumentLike | null; } -const useDocuments = ({ setSearchResults, setFolderContents }: UseDocumentsOptions) => { - const [documents, setDocuments] = useState([]); +const useDocuments = ({ + setFolderContents, + fetchDocumentById, + hydrateDocument, + hydrateDocuments, + extractDocument, +}: UseDocumentsOptions) => { + const managerRef = useRef( + new DocumentsManager(fetchDocumentById, { + hydrateDocument, + hydrateDocuments, + extractDocument, + }), + ); + const [documents, setDocumentsState] = useState([]); + + useEffect(() => { + managerRef.current.setFetcher(fetchDocumentById); + }, [fetchDocumentById]); + + const setDocuments = useCallback( + (value: DocumentLike[] | ((prev: DocumentLike[]) => DocumentLike[])) => { + setDocumentsState((prev) => { + const resolved = typeof value === 'function' ? value(prev) : value; + if (!Array.isArray(resolved)) { + return resolved; + } + const { canonical } = managerRef.current.ingest(resolved); + return canonical; + }); + }, + [], + ); const mapDocumentCaches = useCallback( (mapper: (doc: DocumentLike) => DocumentLike | undefined) => { + managerRef.current.map(mapper); + const lookupSnapshot = managerRef.current.getSnapshot(); - const applyToList = (list?: DocumentLike[] | null) => { - let changed = false; - const safeList = Array.isArray(list) ? list : []; - const next = safeList.map((doc) => { - const updated = mapper(doc); - if (updated === undefined || updated === doc) { - return doc; - } - changed = true; - return updated; - }); - return changed ? next : safeList; - }; - - setDocuments((prev) => applyToList(prev)); - setSearchResults((prev) => { - if (!Array.isArray(prev)) { + setDocumentsState((prev) => { + if (!Array.isArray(prev) || prev.length === 0) { return prev; } - return applyToList(prev); + let changed = false; + const next = prev.map((doc) => { + const id = doc?.id; + if (id != null && lookupSnapshot.has(id as DocumentId)) { + const canonical = lookupSnapshot.get(id as DocumentId) as DocumentLike; + if (canonical !== doc) { + changed = true; + } + return canonical; + } + const updated = mapper(doc); + const nextDoc = updated === undefined ? doc : updated; + if (nextDoc !== doc) { + changed = true; + } + return nextDoc; + }); + return changed ? next : prev; }); setFolderContents((prev) => { if (!prev.size) { @@ -56,12 +104,20 @@ const useDocuments = ({ setSearchResults, setFolderContents }: UseDocumentsOptio } let docsChanged = false; const updatedDocs = docs.map((doc) => { - const updated = mapper(doc); - if (updated === undefined || updated === doc) { - return doc; + const id = doc?.id; + if (id != null && lookupSnapshot.has(id as DocumentId)) { + const canonical = lookupSnapshot.get(id as DocumentId) as DocumentLike; + if (canonical !== doc) { + docsChanged = true; + } + return canonical; } - docsChanged = true; - return updated; + const updated = mapper(doc); + const nextDoc = updated === undefined ? doc : updated; + if (nextDoc !== doc) { + docsChanged = true; + } + return nextDoc; }); if (docsChanged) { changed = true; @@ -73,7 +129,7 @@ const useDocuments = ({ setSearchResults, setFolderContents }: UseDocumentsOptio return changed ? next : prev; }); }, - [setFolderContents, setSearchResults], + [setFolderContents], ); const updateDocumentCaches = useCallback( @@ -93,11 +149,23 @@ const useDocuments = ({ setSearchResults, setFolderContents }: UseDocumentsOptio [mapDocumentCaches], ); + const removeDocumentsFromLookup = useCallback( + (documentIds: Array) => { + if (!Array.isArray(documentIds) || !documentIds.length) { + return; + } + managerRef.current.remove(documentIds); + }, + [], + ); + return { documents, setDocuments, + removeDocumentsFromLookup, mapDocumentCaches, updateDocumentCaches, + documentsManager: managerRef.current, }; }; diff --git a/frontend/src/hooks/documents/useDocumentsWorkspace.ts b/frontend/src/hooks/documents/useDocumentsWorkspace.ts index 11e5967..d05aa12 100644 --- a/frontend/src/hooks/documents/useDocumentsWorkspace.ts +++ b/frontend/src/hooks/documents/useDocumentsWorkspace.ts @@ -1,4 +1,12 @@ -import { MutableRefObject, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { + MutableRefObject, + useCallback, + useEffect, + useMemo, + useRef, + useState, + useSyncExternalStore, +} from 'react'; import { matchPath, useLocation, @@ -220,10 +228,20 @@ const useDocumentsWorkspace = ({ if (!payload) { return null; } - const hydratedDetail = assetManager.hydrateDetail(payload); - return hydratedDetail?.document || payload.document || payload; + return payload.document || payload; }, - [assetManager], + [], + ); + + const fetchDocumentById = useCallback( + async (documentId: DocumentId) => { + if (!documentId) { + return null; + } + const { data } = await api.get(`/documents/${documentId}`); + return extractDocumentFromResponse(data); + }, + [extractDocumentFromResponse], ); const tagManagerRef = useRef(null); @@ -290,21 +308,27 @@ const useDocumentsWorkspace = ({ folderContentsRef.current = folderContents; }, [folderContents]); - const setSearchResultsRef = useRef<(value: unknown) => void>(() => {}); - const setSearchResultsProxy = useCallback((value) => { - setSearchResultsRef.current(value); - }, []); - const { documents, setDocuments, + removeDocumentsFromLookup, mapDocumentCaches, updateDocumentCaches, + documentsManager, } = useDocuments({ - setSearchResults: setSearchResultsProxy, setFolderContents, + fetchDocumentById, + hydrateDocument: (payload) => assetManager.hydrateDocument(payload), + hydrateDocuments: (payload) => assetManager.hydrateDocuments(payload), + extractDocument: extractDocumentFromResponse, }); + const documentLookup = useSyncExternalStore( + (onStoreChange) => documentsManager.subscribe(onStoreChange), + () => documentsManager.getSnapshot(), + () => documentsManager.getSnapshot(), + ); + const { folderNodes, setFolderNodes, @@ -338,8 +362,8 @@ const useDocumentsWorkspace = ({ const { searchQuery, setSearchQuery, - searchResults, - setSearchResults, + searchResultIds, + setSearchResultIds, searchLoading, activeTagFilters, setActiveTagFilters, @@ -349,7 +373,6 @@ const useDocumentsWorkspace = ({ documentsFilterValue, } = useDocumentsSearch({ api, - assetManager, token, selectedFolder, navigate, @@ -361,17 +384,66 @@ const useDocumentsWorkspace = ({ notifyApiError, setLoading, setSearchIncludeDescendants, + documentsManager, }); const documentsFilter = documentsFilterValue; + const [visibleDocumentIds, setVisibleDocumentIds] = useState([]); + + const showingSearchResults = searchResultIds !== null; + useEffect(() => { - setSearchResultsRef.current = setSearchResults; - }, [setSearchResults]); + const arraysEqual = (a: DocumentId[], b: DocumentId[]) => + a.length === b.length && a.every((value, index) => value === b[index]); + + if (showingSearchResults && Array.isArray(searchResultIds)) { + const ids = searchResultIds.filter((id): id is DocumentId => id != null); + setVisibleDocumentIds((prev) => (arraysEqual(prev, ids) ? prev : ids)); + return; + } + + const folderIds = documents + .map((doc) => (doc?.id ?? null) as DocumentId | null) + .filter((id): id is DocumentId => id != null); + setVisibleDocumentIds((prev) => (arraysEqual(prev, folderIds) ? prev : folderIds)); + }, [showingSearchResults, searchResultIds, documents]); + + const viewDocuments = useMemo( + () => + visibleDocumentIds + .map((id) => documentLookup.get(id) || null) + .filter((doc): doc is DocumentLike => Boolean(doc)), + [visibleDocumentIds, documentLookup], + ); + + const visibleDocumentKeys = useMemo( + () => visibleDocumentIds.map((id) => resolveDocumentRowKey(id)).filter(Boolean), + [visibleDocumentIds], + ); + + const visibleFolderKeys = useMemo( + () => + showingSearchResults + ? [] + : currentSubfolders + .map((folder) => resolveFolderRowKey(folder.id)) + .filter(Boolean), + [showingSearchResults, currentSubfolders], + ); + + const visibleRowKeys = useMemo( + () => [...visibleFolderKeys, ...visibleDocumentKeys], + [visibleFolderKeys, visibleDocumentKeys], + ); + + const visibleRowKeySet = useMemo( + () => new Set(visibleRowKeys), + [visibleRowKeys], + ); const { documentLinks, - previewDocuments, ensurePreviewData, ensureDownloadUrl, openDocumentPreview, @@ -380,8 +452,7 @@ const useDocumentsWorkspace = ({ removeDocumentLinks, } = useDocumentPreview({ routeDocumentId: previewDocumentId, - documents, - searchResults, + documentsManager, selectedFolder, assetManager, api, @@ -414,65 +485,6 @@ const useDocumentsWorkspace = ({ const detailFolderFetchRef = useRef(new Set()); - const showingSearchResults = searchResults !== null; - - const visibleDocuments = useMemo( - () => (showingSearchResults ? searchResults : documents), - [showingSearchResults, searchResults, documents], - ); - - const visibleDocumentIds = useMemo( - () => visibleDocuments.map((doc) => doc.id), - [visibleDocuments], - ); - - const visibleDocumentKeys = useMemo( - () => visibleDocumentIds.map((id) => resolveDocumentRowKey(id)).filter(Boolean), - [visibleDocumentIds], - ); - - const visibleFolderKeys = useMemo( - () => - showingSearchResults - ? [] - : currentSubfolders - .map((folder) => resolveFolderRowKey(folder.id)) - .filter(Boolean), - [showingSearchResults, currentSubfolders], - ); - - const visibleRowKeys = useMemo( - () => [...visibleFolderKeys, ...visibleDocumentKeys], - [visibleFolderKeys, visibleDocumentKeys], - ); - - const visibleRowKeySet = useMemo( - () => new Set(visibleRowKeys), - [visibleRowKeys], - ); - - const documentLookup = useMemo(() => { - const map = new Map(); - const push = (items) => { - (items || []).forEach((doc) => { - if (doc?.id) { - map.set(doc.id, doc); - } - }); - }; - - push(documents); - if (Array.isArray(searchResults)) { - push(searchResults); - } - previewDocuments.forEach((doc, id) => { - if (doc && id && !map.has(id)) { - map.set(id, doc); - } - }); - return map; - }, [documents, searchResults, previewDocuments]); - useEffect(() => { if (!showingSearchResults) { return; @@ -696,7 +708,7 @@ const useDocumentsWorkspace = ({ selectionAnchorRef.current = null; setDraggedDocumentIds([]); setDraggedFolderId(null); - setSearchResults(null); + setSearchResultIds(null); setTags([]); setCorrespondents([]); setSearchQuery(''); @@ -729,7 +741,7 @@ const useDocumentsWorkspace = ({ setDocuments, setDraggedDocumentIds, setDraggedFolderId, - setSearchResults, + setSearchResultIds, setTags, setCorrespondents, setSearchQuery, @@ -757,11 +769,11 @@ const useDocumentsWorkspace = ({ const idSet = new Set(documentIds); setDocuments((prev) => prev.filter((doc) => !idSet.has(doc.id))); - setSearchResults((prev) => { + setSearchResultIds((prev) => { if (!Array.isArray(prev)) { return prev; } - const filtered = prev.filter((doc) => !idSet.has(doc.id)); + const filtered = prev.filter((id) => !idSet.has(id as DocumentId)); return filtered.length === prev.length ? prev : filtered; }); @@ -788,9 +800,16 @@ const useDocumentsWorkspace = ({ return changed ? next : prev; }); + removeDocumentsFromLookup(Array.from(idSet)); removeDocumentLinks(Array.from(idSet)); }, - [setDocuments, setSearchResults, setFolderContents, removeDocumentLinks], + [ + setDocuments, + setSearchResultIds, + setFolderContents, + removeDocumentsFromLookup, + removeDocumentLinks, + ], ); const { @@ -812,7 +831,7 @@ const useDocumentsWorkspace = ({ setSelectedFolder, setDocuments, setFolderContents, - setSearchResults, + setSearchResultIds, setSelectedEntries, setSelectionOrder, selectionOrderRef, @@ -864,7 +883,7 @@ const useDocumentsWorkspace = ({ setLoading, setFolderContents, setCurrentFolder, - setSearchResults, + setSearchResultIds, isFilterActive, navigate, handleFileDrop, @@ -883,7 +902,7 @@ const useDocumentsWorkspace = ({ } = useDocumentsSelection({ showingSearchResults, currentSubfolders, - visibleDocuments, + visibleDocuments: viewDocuments, resolveFolderRowKey, resolveDocumentRowKey, configureSelectionEnvironment, @@ -1021,19 +1040,13 @@ const useDocumentsWorkspace = ({ prev.map((doc) => (doc.id === documentId ? mergeAssetIntoDocument(doc, entry) : doc)), ); - setSearchResults((prev) => - Array.isArray(prev) - ? prev.map((doc) => (doc.id === documentId ? mergeAssetIntoDocument(doc, entry) : doc)) - : prev, - ); - return entry; } catch (error) { notifyApiError(error, 'Unable to refresh document asset.'); throw error; } }, - [assetManager, setDocuments, setSearchResults, notifyApiError], + [assetManager, setDocuments, notifyApiError], ); @@ -1233,9 +1246,7 @@ const useDocumentsWorkspace = ({ documentLink, resolveFolderPath, } = useDetailWorkspace({ - documents, - searchResults, - previewDocuments, + documents: viewDocuments, selectionOrder, selectedDocumentIds, documentLookup, @@ -1446,8 +1457,7 @@ const useDocumentsWorkspace = ({ const deskWorkspaceProps = useMemo( () => ({ - documents, - searchResults, + documents: viewDocuments, onInspectDocument: inspectDocumentForDesk, onEntryPointer: handleEntryPointerCore, onDocumentStackSelect: handleDeskDocumentStackSelect, @@ -1464,8 +1474,7 @@ const useDocumentsWorkspace = ({ ensureDownloadUrl, }), [ - documents, - searchResults, + viewDocuments, inspectDocumentForDesk, handleEntryPointerCore, handleDeskDocumentStackSelect, @@ -1488,8 +1497,8 @@ const useDocumentsWorkspace = ({ breadcrumbs, refreshCurrentFolder, currentSubfolders, - documents, - searchResults, + documents: viewDocuments, + searchResultIds, folderClickHandlers, handleFolderDragStart, handleFolderDragEnd, diff --git a/frontend/src/hooks/documents/useFolderTreeActions.ts b/frontend/src/hooks/documents/useFolderTreeActions.ts index eab66ff..40d0bf0 100644 --- a/frontend/src/hooks/documents/useFolderTreeActions.ts +++ b/frontend/src/hooks/documents/useFolderTreeActions.ts @@ -70,7 +70,7 @@ interface UseFolderTreeActionsOptions { updater: (prev: Map) => Map, ) => void; setCurrentFolder: (updater: (prev: any) => any) => void; - setSearchResults: (value: any) => void; + setSearchResultIds: (value: any) => void; isFilterActive: boolean; navigate?: (path: string, options?: { replace?: boolean }) => void; handleFileDrop: (dataTransfer: DataTransfer, folderId: FolderKey) => Promise | void; @@ -99,7 +99,7 @@ const useFolderTreeActions = ({ setLoading, setFolderContents, setCurrentFolder, - setSearchResults, + setSearchResultIds, isFilterActive, navigate, handleFileDrop, @@ -235,7 +235,7 @@ const useFolderTreeActions = ({ } applySelectedFolder(targetId, contents); if (!preserveSearch) { - setSearchResults(null); + setSearchResultIds(null); } } catch (error) { notifyApiError(error, 'Failed to load folder contents.'); @@ -250,7 +250,7 @@ const useFolderTreeActions = ({ expandFolderAncestors, notifyApiError, setLoading, - setSearchResults, + setSearchResultIds, setSelectedFolder, ], ); diff --git a/frontend/src/preview/DocumentViewerPanel.tsx b/frontend/src/preview/DocumentViewerPanel.tsx index e4c287b..61fe4e3 100644 --- a/frontend/src/preview/DocumentViewerPanel.tsx +++ b/frontend/src/preview/DocumentViewerPanel.tsx @@ -253,12 +253,35 @@ const DocumentViewerPanel: React.FC = ({ const [zoomOverlayOpen, setZoomOverlayOpen] = useState(false); + const fallbackDocumentLink = useMemo(() => { + if (!document) { + return null; + } + const downloadPath = document.current_version?.download_path; + if (!downloadPath) { + return null; + } + const href = resolveApiPath ? resolveApiPath(downloadPath) : downloadPath; + if (!href) { + return null; + } + const contentType = document.current_version?.version?.content_type || document.content_type || null; + const filename = document.current_version?.filename || document.filename || document.title || null; + return { + url: href, + contentType, + filename, + }; + }, [document, resolveApiPath]); + + const effectiveDocumentLink = documentLink?.url ? documentLink : fallbackDocumentLink; + const handleZoomOpen = useCallback(() => { - if (!documentLink?.url) { + if (!effectiveDocumentLink?.url) { return; } setZoomOverlayOpen(true); - }, [documentLink?.url]); + }, [effectiveDocumentLink?.url]); const handleZoomClose = useCallback(() => { setZoomOverlayOpen(false); @@ -266,7 +289,7 @@ const DocumentViewerPanel: React.FC = ({ useEffect(() => { setZoomOverlayOpen(false); - }, [documentLink?.url, document?.id]); + }, [effectiveDocumentLink?.url, document?.id]); useEffect(() => { if (hydrateDocument && document?.id) { @@ -355,23 +378,23 @@ const DocumentViewerPanel: React.FC = ({ }, [breadcrumbs, handleBreadcrumbNavigate]); const zoomDisplay = useMemo(() => { - if (!documentLink?.url || !document) { + if (!effectiveDocumentLink?.url || !document) { return null; } const docContentType = document.content_type; const versionContentType = document.current_version?.version?.content_type; - const normalizedContentType = documentLink.contentType || docContentType || versionContentType || null; + const normalizedContentType = effectiveDocumentLink.contentType || docContentType || versionContentType || null; return { - url: documentLink.url, + url: effectiveDocumentLink.url, alt: document.title, contentType: normalizedContentType || undefined, }; - }, [documentLink?.url, documentLink?.contentType, document]); + }, [effectiveDocumentLink?.url, effectiveDocumentLink?.contentType, document]); const headerActions = createDocumentViewerHeaderActions({ document, actionState, - documentLink, + documentLink: effectiveDocumentLink, onZoom: zoomDisplay ? handleZoomOpen : null, canZoom: Boolean(zoomDisplay), }); @@ -471,7 +494,7 @@ const DocumentViewerPanel: React.FC = ({
{ return ; } - if (!previewWorkspaceDocument || previewWorkspaceDocument.id !== documentId) { + const previewId = previewWorkspaceDocument?.id; + if (!previewWorkspaceDocument || String(previewId) !== String(documentId)) { return
Loading preview…
; }