This commit is contained in:
2025-12-04 14:37:09 +01:00
parent 34230cda21
commit dd521f9f1d
5 changed files with 51 additions and 149 deletions
+1 -1
View File
@@ -40,7 +40,7 @@ const DocumentsInner: React.FC<{
const handleOpenSidepanel = useCallback((docId: string) => { const handleOpenSidepanel = useCallback((docId: string) => {
if (openDetailPanel) { if (openDetailPanel) {
openDetailPanel({ documentIds: [docId] }); openDetailPanel(docId);
} }
}, [openDetailPanel]); }, [openDetailPanel]);
+15 -67
View File
@@ -1,97 +1,46 @@
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import type { Identifier } from '../types/identifiers';
interface DetailDocument { interface DetailDocument {
id?: string; id?: Identifier;
[key: string]: unknown; [key: string]: unknown;
} }
interface UseDetailPanelOptions { interface UseDetailPanelOptions {
documentLookup: Map<string, DetailDocument>; documentLookup: Map<Identifier, DetailDocument>;
orderedSelectedDocuments: DetailDocument[];
}
interface OpenDetailPanelArgs {
documentId?: string;
document?: DetailDocument | null;
documentIds?: Array<string>;
documents?: DetailDocument[];
} }
export const useDetailPanel = ({ export const useDetailPanel = ({
documentLookup, documentLookup,
orderedSelectedDocuments,
}: UseDetailPanelOptions) => { }: UseDetailPanelOptions) => {
const [detailPanelOpen, setDetailPanelOpen] = useState(false); const [detailPanelDocId, setDetailPanelDocId] = useState<Identifier | null>(null);
const [detailPanelDocId, setDetailPanelDocId] = useState<string | null>(null);
const [detailPanelDocument, setDetailPanelDocument] = useState<DetailDocument | null>(null); const [detailPanelDocument, setDetailPanelDocument] = useState<DetailDocument | null>(null);
const latestOrderedDocsRef = useRef<DetailDocument[]>([]);
useEffect(() => { const detailPanelOpen = detailPanelDocId !== null;
latestOrderedDocsRef.current = orderedSelectedDocuments;
if (detailPanelOpen && orderedSelectedDocuments.length) {
const nextDoc = orderedSelectedDocuments[orderedSelectedDocuments.length - 1];
if (nextDoc?.id) {
setDetailPanelDocId(nextDoc.id);
setDetailPanelDocument(nextDoc);
}
}
}, [orderedSelectedDocuments, detailPanelOpen]);
useEffect(() => { useEffect(() => {
if (!detailPanelDocId) { if (!detailPanelDocId) {
if (!detailPanelOpen) { setDetailPanelDocument(null);
setDetailPanelDocument(null);
}
return; return;
} }
const resolved = documentLookup.get(detailPanelDocId); const resolved = documentLookup.get(detailPanelDocId) ?? null;
if (resolved && resolved !== detailPanelDocument) { if (resolved !== detailPanelDocument) {
setDetailPanelDocument(resolved); setDetailPanelDocument(resolved);
} }
}, [detailPanelDocId, documentLookup, detailPanelDocument, detailPanelOpen]); }, [detailPanelDocId, documentLookup, detailPanelDocument]);
const openDetailPanel = useCallback( const openDetailPanel = useCallback(
({ documentId, document, documentIds, documents }: OpenDetailPanelArgs = {}) => { (documentId: Identifier) => {
let targetDoc = document || null; if (!documentId) {
let targetId = documentId ?? document?.id ?? null;
if (!targetDoc && Array.isArray(documents) && documents.length) {
targetDoc = documents[documents.length - 1];
targetId = targetDoc?.id ?? targetId;
}
if (!targetDoc && Array.isArray(documentIds) && documentIds.length) {
targetId = documentIds[documentIds.length - 1];
}
if (!targetDoc && targetId != null) {
targetDoc = documentLookup.get(String(targetId)) || null;
}
if (!targetDoc) {
const fallbackDocs = latestOrderedDocsRef.current;
const fallbackDoc = Array.isArray(fallbackDocs) && fallbackDocs.length
? fallbackDocs[fallbackDocs.length - 1]
: null;
if (fallbackDoc) {
targetDoc = fallbackDoc;
targetId = fallbackDoc.id;
}
}
if (!targetDoc && targetId == null) {
return; return;
} }
setDetailPanelDocId(documentId);
setDetailPanelDocId(targetDoc?.id ?? targetId ?? null);
setDetailPanelDocument(targetDoc || null);
setDetailPanelOpen(Boolean(targetDoc || targetId));
}, },
[documentLookup], [],
); );
const closeDetailPanel = useCallback(() => { const closeDetailPanel = useCallback(() => {
setDetailPanelOpen(false); setDetailPanelDocId(null);
}, []); }, []);
return { return {
@@ -99,6 +48,5 @@ export const useDetailPanel = ({
detailPanelDocument, detailPanelDocument,
openDetailPanel, openDetailPanel,
closeDetailPanel, closeDetailPanel,
setDetailPanelOpen,
}; };
}; };
+1 -1
View File
@@ -7,7 +7,7 @@ import type { UseWorkspaceSurfaceArgs } from './useWorkspaceSurface';
import type { Identifier } from '../types/identifiers'; import type { Identifier } from '../types/identifiers';
type WorkspaceSurfaceConfig = Omit<UseWorkspaceSurfaceArgs, 'sidebarHidden' | 'onExpandSidebar'> & { type WorkspaceSurfaceConfig = Omit<UseWorkspaceSurfaceArgs, 'sidebarHidden' | 'onExpandSidebar'> & {
openDetailPanel?: (args: { documentIds?: Identifier[] }) => void; openDetailPanel?: (documentId: Identifier) => void;
closeDetailPanel?: () => void; closeDetailPanel?: () => void;
}; };
+34 -77
View File
@@ -3,7 +3,6 @@ import type { MutableRefObject } from 'react';
import { resolveDocumentAssetUrl } from '../asset_manager'; import { resolveDocumentAssetUrl } from '../asset_manager';
import { useDetailPanel } from '../app/useDetailPanel'; import { useDetailPanel } from '../app/useDetailPanel';
import { DEFAULT_FOLDER_NAME } from '../app/workspaceUtils'; import { DEFAULT_FOLDER_NAME } from '../app/workspaceUtils';
import { getEntryId, isDocumentEntry } from '../app/entryKey';
import type { DocumentInfoPanelProps } from '../documents/DocumentInfoPanel'; import type { DocumentInfoPanelProps } from '../documents/DocumentInfoPanel';
import type { EnsureAssetUrl, GetDocumentAsset } from '../utils/ocr'; import type { EnsureAssetUrl, GetDocumentAsset } from '../utils/ocr';
import type { Identifier } from '../types/identifiers'; import type { Identifier } from '../types/identifiers';
@@ -17,12 +16,10 @@ interface FolderNode {
interface UseDetailWorkspaceArgs { interface UseDetailWorkspaceArgs {
documents: Document[]; documents: Document[];
selectionOrder: string[];
selectedDocumentIds: Identifier[];
documentLookup: Map<Identifier, Document>; documentLookup: Map<Identifier, Document>;
folderNodes: Map<Identifier | 'root', FolderNode>; folderNodes: Map<Identifier | 'root', FolderNode>;
ensureFolderData: (folderId: Identifier | 'root', options?: { force?: boolean; includeDocuments?: boolean }) => Promise<void>; ensureFolderData: (folderId: Identifier | 'root', options?: { force?: boolean; includeDocuments?: boolean }) => Promise<void>;
detailPanelControlRef: MutableRefObject<{ open?: (args?: { documentIds?: Identifier[] }) => void; close?: () => void } | null>; detailPanelControlRef: MutableRefObject<{ open?: (documentId: Identifier) => void; close?: () => void } | null>;
detailFolderFetchRef: MutableRefObject<Set<Identifier | 'root'>>; detailFolderFetchRef: MutableRefObject<Set<Identifier | 'root'>>;
previewDocumentId?: Identifier | null; previewDocumentId?: Identifier | null;
activePreviewId?: Identifier | null; activePreviewId?: Identifier | null;
@@ -56,8 +53,6 @@ interface UseDetailWorkspaceResult {
const useDetailWorkspace = ({ const useDetailWorkspace = ({
documents, documents,
selectionOrder,
selectedDocumentIds,
documentLookup, documentLookup,
folderNodes, folderNodes,
ensureFolderData, ensureFolderData,
@@ -79,37 +74,6 @@ const useDetailWorkspace = ({
tags, tags,
tagLookupById, tagLookupById,
}: UseDetailWorkspaceArgs): UseDetailWorkspaceResult => { }: 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 (!isDocumentEntry(key)) {
return;
}
const docId = getEntryId(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 { const {
detailPanelOpen, detailPanelOpen,
detailPanelDocument, detailPanelDocument,
@@ -117,7 +81,6 @@ const useDetailWorkspace = ({
closeDetailPanel, closeDetailPanel,
} = useDetailPanel({ } = useDetailPanel({
documentLookup, documentLookup,
orderedSelectedDocuments,
}); });
useEffect(() => { useEffect(() => {
@@ -127,52 +90,46 @@ const useDetailWorkspace = ({
}; };
}, [detailPanelControlRef, openDetailPanel, closeDetailPanel]); }, [detailPanelControlRef, openDetailPanel, closeDetailPanel]);
// Prefetch folder ancestors for breadcrumb display
useEffect(() => { useEffect(() => {
if (!orderedSelectedDocuments.length) { const folderId = detailPanelDocument?.folder_id;
if (!folderId) {
return; return;
} }
const visited = new Set(); const visited = new Set();
let currentId = folderId;
let guard = 0;
orderedSelectedDocuments.forEach((doc) => { while (currentId && currentId !== 'root' && guard < 32) {
const folderId = doc?.folder_id; guard += 1;
if (!folderId) { if (visited.has(currentId)) {
return; 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;
} }
let currentId = folderId; const parentId = node.parentId ?? 'root';
let guard = 0; if (!parentId || parentId === 'root') {
break;
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;
} }
}); currentId = parentId;
}, [orderedSelectedDocuments, folderNodes, ensureFolderData, detailFolderFetchRef]); }
}, [detailPanelDocument, folderNodes, ensureFolderData, detailFolderFetchRef]);
const resolveFolderPath = useCallback( const resolveFolderPath = useCallback(
(folderId) => { (folderId) => {
@@ -240,11 +197,11 @@ const useDetailWorkspace = ({
); );
const inspectDocument = useCallback( const inspectDocument = useCallback(
(documentId) => { (documentId: Identifier) => {
if (!documentId) { if (!documentId) {
return; return;
} }
openDetailPanel({ documentIds: [documentId] }); openDetailPanel(documentId);
}, },
[openDetailPanel], [openDetailPanel],
); );
@@ -257,7 +257,6 @@ const useDocumentsWorkspace = ({
selectedDocumentIds, selectedDocumentIds,
selectedFolderIds, selectedFolderIds,
setSelectedEntries, setSelectedEntries,
selectionOrder,
setSelectionOrder, setSelectionOrder,
selectionOrderRef, selectionOrderRef,
selectionAnchorRef, selectionAnchorRef,
@@ -1074,8 +1073,6 @@ const useDocumentsWorkspace = ({
resolveFolderPath, resolveFolderPath,
} = useDetailWorkspace({ } = useDetailWorkspace({
documents: viewDocuments, documents: viewDocuments,
selectionOrder,
selectedDocumentIds,
documentLookup, documentLookup,
folderNodes, folderNodes,
ensureFolderData, ensureFolderData,