diff --git a/frontend/src/index.jsx b/frontend/src/index.jsx index 27368f6..618e806 100644 --- a/frontend/src/index.jsx +++ b/frontend/src/index.jsx @@ -1151,6 +1151,7 @@ function App({ routeFolderId = null, routeDocumentId = null, navigate }) { const documentDetailsRef = useRef(documentDetails); const tokenRef = useRef(token); const refreshPromiseRef = useRef(null); + const breadcrumbFetchRef = useRef(new Set()); const refreshAccessToken = useCallback(async () => { console.log('[Auth] Attempting to refresh access token…'); try { @@ -3226,39 +3227,85 @@ const handleDownload = useCallback( }); }, [selectionOrder, selectedDocumentIds, ensureDocumentDetail]); - const breadcrumbs = useMemo(() => { + const { breadcrumbs, missingBreadcrumbAncestors } = useMemo(() => { const chain = []; const seen = new Set(); + const pending = new Set(); let currentId = selectedFolder || 'root'; + let guard = 0; - while (currentId && !seen.has(currentId)) { + while (currentId && !seen.has(currentId) && guard < 32) { + guard += 1; seen.add(currentId); + if (currentId === 'root') { chain.push({ id: 'root', name: DEFAULT_FOLDER_NAME }); + currentId = null; break; } const node = folderNodes.get(currentId); if (node) { - chain.push({ id: currentId, name: node.name || DEFAULT_FOLDER_NAME }); + chain.push({ id: currentId, name: node.name || 'Folder' }); currentId = node.parentId ?? 'root'; - } else { - chain.push({ id: currentId, name: currentFolderName }); - currentId = 'root'; + continue; } + + let fallbackName = '…'; + let parentId = null; + + if (currentFolder && currentFolder.id === currentId) { + fallbackName = currentFolder.name; + parentId = currentFolder.parent_id ?? 'root'; + } + + chain.push({ id: currentId, name: fallbackName }); + pending.add(currentId); + currentId = parentId; } - if (chain[chain.length - 1]?.id !== 'root') { + if (!chain.some((crumb) => crumb.id === 'root')) { chain.push({ id: 'root', name: DEFAULT_FOLDER_NAME }); } - return chain.reverse(); - }, [selectedFolder, folderNodes, currentFolderName]); + const ordered = []; + const seenOrdered = new Set(); + chain + .slice() + .reverse() + .forEach((crumb) => { + if (!seenOrdered.has(crumb.id)) { + seenOrdered.add(crumb.id); + ordered.push(crumb); + } + }); + + return { breadcrumbs: ordered, missingBreadcrumbAncestors: Array.from(pending) }; + }, [selectedFolder, folderNodes, currentFolder]); useEffect(() => { - if (!selectedDocument) return; - ensurePreviewUrl(selectedDocument.id).catch(() => {}); - }, [selectedDocument, ensurePreviewUrl]); + if (!missingBreadcrumbAncestors.length) { + return; + } + + missingBreadcrumbAncestors.forEach((folderId) => { + if (!folderId || folderId === 'root') { + return; + } + if (breadcrumbFetchRef.current.has(folderId)) { + return; + } + + breadcrumbFetchRef.current.add(folderId); + ensureFolderData(folderId, { force: false }) + .catch((error) => { + console.warn('Failed to preload breadcrumb ancestor', folderId, error); + }) + .finally(() => { + breadcrumbFetchRef.current.delete(folderId); + }); + }); + }, [missingBreadcrumbAncestors, ensureFolderData]); const selectedPreviewEntry = useMemo(() => { if (!selectedDocument) {