import { useCallback, useEffect, useMemo } from 'react'; import type { MutableRefObject } from 'react'; import { resolveDocumentAssetUrl } from '../asset_manager'; import { useDetailPanel } from '../app/useDetailPanel'; import { DEFAULT_FOLDER_NAME, getRowId, isDocumentRowKey, } from '../app/appLayoutUtils'; import type { DocumentInfoPanelProps } from '../documents/DocumentInfoPanel'; type Identifier = string | number; interface DocumentLike { id?: Identifier; folder_id?: Identifier | 'root'; title?: string; [key: string]: unknown; } interface FolderNode { id: Identifier | 'root'; name?: string; parentId?: Identifier | 'root'; } type PreviewEntry = { url?: string; canGoPrev?: boolean; canGoNext?: boolean; goPrev?: () => void; goNext?: () => void; } | null; interface UseDetailWorkspaceArgs { documents: DocumentLike[]; searchResults?: DocumentLike[] | null; previewDocuments?: Map | null; selectionOrder: string[]; selectedDocumentIds: Identifier[]; documentLookup: Map; folderNodes: Map; ensureFolderData: (folderId: Identifier | 'root', options?: { force?: boolean; includeDocuments?: boolean }) => Promise; detailPanelControlRef: MutableRefObject<{ open?: (args?: { documentIds?: Identifier[] }) => void; close?: () => void } | null>; detailFolderFetchRef: MutableRefObject>; previewEntries: Map; previewDocumentId?: Identifier | null; activePreviewId?: Identifier | null; openDocumentPreview?: (args: { documentIds: Identifier[] }) => void; handleDocumentTitleUpdate?: (docId: Identifier, title: string) => Promise | boolean; handleDocumentIssuedUpdate?: (docId: Identifier, issued: number | null) => Promise | boolean; handleDocumentTagAdd?: (doc: DocumentLike, value: string, context?: { option?: unknown }) => void; handleTagRemove?: (...args: unknown[]) => void; ensureAssetUrl?: (...args: unknown[]) => unknown; getDocumentAsset?: (...args: unknown[]) => unknown; ensurePreviewData?: (docId: Identifier, options?: Record) => Promise; correspondents?: unknown[]; handleCorrespondentAdd?: (...args: unknown[]) => void; handleCorrespondentRemove?: (...args: unknown[]) => void; resolveApiPath?: (path: string) => string; selectFolder?: (folderId?: Identifier | 'root') => void; tags?: unknown[]; tagLookupById?: Map | null; } interface UseDetailWorkspaceResult { detailPanelProps: DocumentInfoPanelProps; detailPanelOpen: boolean; openDetailPanel: ReturnType['openDetailPanel']; closeDetailPanel: ReturnType['closeDetailPanel']; handleDetailPanelClose: () => void; inspectDocument: (docId: Identifier | null) => void; previewActive: boolean; previewWorkspaceDocument: DocumentLike | null; previewWorkspaceEntry: PreviewEntry; resolveThumbnailUrlForDoc: (doc: DocumentLike | null) => string | null; resolveFolderPath: (folderId?: Identifier | 'root') => Array<{ id: Identifier | 'root'; name: string }>; } const useDetailWorkspace = ({ documents, searchResults, previewDocuments, selectionOrder, selectedDocumentIds, documentLookup, folderNodes, ensureFolderData, detailPanelControlRef, detailFolderFetchRef, previewEntries, previewDocumentId, activePreviewId, openDocumentPreview, handleDocumentTitleUpdate, handleDocumentIssuedUpdate, handleDocumentTagAdd, handleTagRemove, ensureAssetUrl, getDocumentAsset, ensurePreviewData, correspondents, handleCorrespondentAdd, handleCorrespondentRemove, resolveApiPath, selectFolder, tags, tagLookupById, }: UseDetailWorkspaceArgs): UseDetailWorkspaceResult => { const orderedSelectedDocuments = useMemo(() => { const ordered = []; const seen = new Set(); const pushDoc = (doc) => { if (doc?.id && !seen.has(doc.id)) { ordered.push(doc); seen.add(doc.id); } }; selectionOrder.forEach((key) => { if (!isDocumentRowKey(key)) { return; } const docId = getRowId(key); const doc = documentLookup.get(docId) || null; pushDoc(doc); }); selectedDocumentIds.forEach((docId) => { if (seen.has(docId)) { return; } const doc = documentLookup.get(docId) || null; pushDoc(doc); }); return ordered; }, [selectionOrder, documentLookup, selectedDocumentIds]); const { detailPanelOpen, detailPanelDocument, openDetailPanel, closeDetailPanel, } = useDetailPanel({ documentLookup, orderedSelectedDocuments, }); useEffect(() => { detailPanelControlRef.current = { open: openDetailPanel, close: closeDetailPanel, }; }, [detailPanelControlRef, openDetailPanel, closeDetailPanel]); useEffect(() => { if (!orderedSelectedDocuments.length) { return; } const visited = new Set(); orderedSelectedDocuments.forEach((doc) => { const folderId = doc?.folder_id; if (!folderId) { return; } let currentId = folderId; let guard = 0; while (currentId && currentId !== 'root' && guard < 32) { guard += 1; if (visited.has(currentId)) { break; } visited.add(currentId); const node = folderNodes.get(currentId); if (!node) { if (!detailFolderFetchRef.current.has(currentId)) { detailFolderFetchRef.current.add(currentId); ensureFolderData(currentId, { force: false, includeDocuments: false }) .catch((error) => { console.warn('Failed to preload folder metadata for detail path', currentId, error); }) .finally(() => { detailFolderFetchRef.current.delete(currentId); }); } break; } const parentId = node.parentId ?? 'root'; if (!parentId || parentId === 'root') { break; } currentId = parentId; } }); }, [orderedSelectedDocuments, folderNodes, ensureFolderData, detailFolderFetchRef]); const resolveFolderPath = useCallback( (folderId) => { if (!folderId || folderId === 'root') { return []; } const segments = []; const visited = new Set(); let currentId = folderId; let guard = 0; while (currentId && guard < 32 && !visited.has(currentId)) { guard += 1; visited.add(currentId); if (currentId === 'root') { break; } const node = folderNodes.get(currentId); if (!node) { segments.push({ id: currentId, name: '…' }); break; } segments.push({ id: node.id, name: node.name || 'Folder' }); const parentId = node.parentId ?? 'root'; if (!parentId || parentId === 'root') { segments.push({ id: 'root', name: DEFAULT_FOLDER_NAME }); break; } currentId = parentId; } if (!segments.some((segment) => segment.id === 'root')) { segments.push({ id: 'root', name: DEFAULT_FOLDER_NAME }); } return segments.reverse(); }, [folderNodes], ); const detailPanelPreviewEntry = useMemo(() => { if (!detailPanelDocument) { return null; } return previewEntries.get(detailPanelDocument.id) || null; }, [detailPanelDocument, previewEntries]); const previewWorkspaceEntry = useMemo(() => { if (!previewDocumentId) { return null; } return previewEntries.get(previewDocumentId) || null; }, [previewDocumentId, previewEntries]); const previewWorkspaceDocument = useMemo(() => { if (!previewDocumentId) { return null; } const pool = searchResults ?? documents; return pool.find((doc) => doc.id === previewDocumentId) || previewDocuments?.get?.(previewDocumentId) || null; }, [previewDocumentId, searchResults, documents, previewDocuments]); const previewActive = Boolean(previewDocumentId && previewWorkspaceDocument); const resolveThumbnailUrlForDoc = useCallback( (doc) => resolveDocumentAssetUrl(doc, 'thumbnail', { ensureAssetUrl, getAsset: getDocumentAsset, }), [ensureAssetUrl, getDocumentAsset], ); const inspectDocument = useCallback( (documentId) => { if (!documentId) { return; } openDetailPanel({ documentIds: [documentId] }); }, [openDetailPanel], ); const handleDetailPanelClose = useCallback(() => { closeDetailPanel(); }, [closeDetailPanel]); const detailPanelProps = useMemo( () => ({ document: detailPanelDocument, tags, tagLookupById, onTagAdd: handleDocumentTagAdd, onTagRemove: handleTagRemove, previewEntry: detailPanelPreviewEntry, onOpenPreview: openDocumentPreview, activePreviewId, onUpdateTitle: handleDocumentTitleUpdate, onUpdateIssued: handleDocumentIssuedUpdate, ensureAssetUrl, getDocumentAsset, hydrateDocument: ensurePreviewData, correspondents, onCorrespondentAdd: handleCorrespondentAdd, onCorrespondentRemove: handleCorrespondentRemove, resolveApiPath, onFolderNavigate: selectFolder, onClose: handleDetailPanelClose, resolveFolderPath, }), [ activePreviewId, correspondents, detailPanelDocument, ensureAssetUrl, ensurePreviewData, getDocumentAsset, handleCorrespondentAdd, handleCorrespondentRemove, handleDetailPanelClose, handleDocumentTagAdd, handleDocumentIssuedUpdate, handleDocumentTitleUpdate, handleTagRemove, openDocumentPreview, resolveApiPath, resolveFolderPath, selectFolder, detailPanelPreviewEntry, tags, tagLookupById, ], ); return { detailPanelProps, detailPanelOpen, openDetailPanel, closeDetailPanel, handleDetailPanelClose, inspectDocument, previewActive, previewWorkspaceDocument, previewWorkspaceEntry, resolveThumbnailUrlForDoc, resolveFolderPath, }; }; export default useDetailWorkspace;