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 } from '../app/workspaceUtils'; import type { DocumentInfoPanelProps } from '../documents/DocumentInfoPanel'; import type { EnsureAssetUrl, GetDocumentAsset } from '../utils/ocr'; import type { Identifier } from '../types/identifiers'; import type { Document } from '../types/documents'; interface FolderNode { id: Identifier | 'root'; name?: string; parentId?: Identifier | 'root'; } interface UseDetailWorkspaceArgs { documents: Document[]; documentLookup: Map; folderNodes: Map; ensureFolderData: (folderId: Identifier | 'root', options?: { force?: boolean; includeDocuments?: boolean }) => Promise; detailPanelControlRef: MutableRefObject<{ open?: (documentId: Identifier) => void; close?: () => void } | null>; detailFolderFetchRef: MutableRefObject>; 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: Document, value: string, context?: { option?: unknown }) => void; handleTagRemove?: (...args: unknown[]) => void; ensureAssetUrl?: EnsureAssetUrl; getDocumentAsset?: GetDocumentAsset; correspondents?: unknown[]; handleCorrespondentAdd?: (...args: unknown[]) => void; handleCorrespondentRemove?: (...args: unknown[]) => void; 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: Document | null; resolveThumbnailUrlForDoc: (doc: Document | null) => string | null; resolveFolderPath: (folderId?: Identifier | 'root') => Array<{ id: Identifier | 'root'; name: string }>; } const useDetailWorkspace = ({ documents, documentLookup, folderNodes, ensureFolderData, detailPanelControlRef, detailFolderFetchRef, previewDocumentId, activePreviewId, openDocumentPreview, handleDocumentTitleUpdate, handleDocumentIssuedUpdate, handleDocumentTagAdd, handleTagRemove, ensureAssetUrl, getDocumentAsset, correspondents, handleCorrespondentAdd, handleCorrespondentRemove, selectFolder, tags, tagLookupById, }: UseDetailWorkspaceArgs): UseDetailWorkspaceResult => { const { detailPanelOpen, detailPanelDocument, openDetailPanel, closeDetailPanel, } = useDetailPanel({ documentLookup, }); useEffect(() => { detailPanelControlRef.current = { open: openDetailPanel, close: closeDetailPanel, }; }, [detailPanelControlRef, openDetailPanel, closeDetailPanel]); // Prefetch folder ancestors for breadcrumb display useEffect(() => { const folderId = detailPanelDocument?.folder_id; if (!folderId) { return; } const visited = new Set(); 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; } }, [detailPanelDocument, 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 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', { ensureAssetUrl, getAsset: getDocumentAsset, }), [ensureAssetUrl, getDocumentAsset], ); const inspectDocument = useCallback( (documentId: Identifier) => { if (!documentId) { return; } openDetailPanel(documentId); }, [openDetailPanel], ); const handleDetailPanelClose = useCallback(() => { closeDetailPanel(); }, [closeDetailPanel]); const detailPanelProps = useMemo( () => ({ document: detailPanelDocument, tags, tagLookupById, onTagAdd: handleDocumentTagAdd, onTagRemove: handleTagRemove, onOpenPreview: openDocumentPreview, activePreviewId, onUpdateTitle: handleDocumentTitleUpdate, onUpdateIssued: handleDocumentIssuedUpdate, ensureAssetUrl, getDocumentAsset, correspondents, onCorrespondentAdd: handleCorrespondentAdd, onCorrespondentRemove: handleCorrespondentRemove, onFolderNavigate: selectFolder, onClose: handleDetailPanelClose, resolveFolderPath, folderNodes, ensureFolderData, }), [ activePreviewId, correspondents, detailPanelDocument, ensureAssetUrl, getDocumentAsset, handleCorrespondentAdd, handleCorrespondentRemove, handleDetailPanelClose, handleDocumentTagAdd, handleDocumentIssuedUpdate, handleDocumentTitleUpdate, handleTagRemove, folderNodes, ensureFolderData, openDocumentPreview, resolveFolderPath, selectFolder, tags, tagLookupById, ], ); return { detailPanelProps, detailPanelOpen, openDetailPanel, closeDetailPanel, handleDetailPanelClose, inspectDocument, previewActive, previewWorkspaceDocument, resolveThumbnailUrlForDoc, resolveFolderPath, }; }; export default useDetailWorkspace;