273 lines
7.9 KiB
TypeScript
273 lines
7.9 KiB
TypeScript
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<Identifier, Document>;
|
|
folderNodes: Map<Identifier | 'root', FolderNode>;
|
|
ensureFolderData: (folderId: Identifier | 'root', options?: { force?: boolean; includeDocuments?: boolean }) => Promise<void>;
|
|
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;
|
|
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<Identifier, unknown> | null;
|
|
}
|
|
|
|
interface UseDetailWorkspaceResult {
|
|
detailPanelProps: DocumentInfoPanelProps;
|
|
detailPanelOpen: boolean;
|
|
openDetailPanel: ReturnType<typeof useDetailPanel>['openDetailPanel'];
|
|
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,
|
|
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;
|