From 53664f2421858d7ee1c654c13e83db6e4d79050a Mon Sep 17 00:00:00 2001 From: Nils Schneider Date: Sun, 7 Dec 2025 22:10:34 +0100 Subject: [PATCH] refactor: Replace `useWorkspaceBreadcrumbs` hook with `resolveBreadcrumbs` utility --- .../documents/data/useDocumentsWorkspace.ts | 51 +-------- .../features/folders/useFolderTreeActions.ts | 4 - frontend/src/documents/logic/breadcrumbs.ts | 40 +++++++ .../logic/useWorkspaceBreadcrumbs.ts | 105 ------------------ .../src/viewer/logic/useDetailWorkspace.ts | 42 +------ 5 files changed, 47 insertions(+), 195 deletions(-) create mode 100644 frontend/src/documents/logic/breadcrumbs.ts delete mode 100644 frontend/src/documents/logic/useWorkspaceBreadcrumbs.ts diff --git a/frontend/src/documents/data/useDocumentsWorkspace.ts b/frontend/src/documents/data/useDocumentsWorkspace.ts index 62b4534..3d5a439 100644 --- a/frontend/src/documents/data/useDocumentsWorkspace.ts +++ b/frontend/src/documents/data/useDocumentsWorkspace.ts @@ -58,7 +58,7 @@ import useTags from './useTags'; import useCorrespondents from './useCorrespondents'; import useDocumentCorrespondentActions from '../features/correspondents/useDocumentCorrespondentActions'; import usePasskeys from '../../settings/usePasskeys'; -import useWorkspaceBreadcrumbs from '../logic/useWorkspaceBreadcrumbs'; +import { resolveBreadcrumbs } from '../logic/breadcrumbs'; import useWorkspaceSelectionSync from '../features/selection/useWorkspaceSelectionSync'; import type { DocumentId, FolderNodeId, Identifier } from '../../types/identifiers'; @@ -100,7 +100,6 @@ interface UseDocumentsWorkspaceOptions { onDocumentsSortDirectionToggle?: () => void; searchIncludeDescendants?: boolean; onSetSearchIncludeDescendants?: (value: boolean) => void; - sortRefreshReadyRef?: MutableRefObject; } const useDocumentsWorkspace = ({ @@ -114,7 +113,6 @@ const useDocumentsWorkspace = ({ onDocumentsSortDirectionToggle, searchIncludeDescendants = true, onSetSearchIncludeDescendants, - sortRefreshReadyRef, }: UseDocumentsWorkspaceOptions = {}) => { const handleDocumentsViewModeChange = onDocumentsViewModeChange || noop; const handleDocumentsSortFieldChange = onDocumentsSortFieldChange || noop; @@ -133,8 +131,6 @@ const useDocumentsWorkspace = ({ activeSortDirectionRef.current = documentsSortDirection; }, [documentsSortDirection, activeSortDirectionRef]); - const fallbackSortRefreshReadyRef = useRef(false); - const activeSortRefreshReadyRef = sortRefreshReadyRef || fallbackSortRefreshReadyRef; const navigate = useNavigate(); const location = useLocation(); const appState = useAppState(); @@ -332,7 +328,6 @@ const useDocumentsWorkspace = ({ foldersManager, }); - const [currentFolder, setCurrentFolder] = useState(null); const [currentSubfolders, setCurrentSubfolders] = useState([]); const ensureFolderData = useCallback( @@ -358,7 +353,6 @@ const useDocumentsWorkspace = ({ setDocuments((data.documents || []) as Document[]); } setCurrentSubfolders((data.subfolders || []) as any[]); - setCurrentFolder(data.folder || null); // Update selection state based on new documents if (includeDocuments) { @@ -396,7 +390,6 @@ const useDocumentsWorkspace = ({ activeSortFieldRef, activeSortDirectionRef, setDocuments, - setCurrentFolder, setSelectedEntries, notifyApiError, setCurrentSubfolders, @@ -404,20 +397,11 @@ const useDocumentsWorkspace = ({ ] ); - // Reactively fetch documents when selectedFolder changes useEffect(() => { if (selectedFolder) { ensureFolderData(selectedFolder); } - }, [selectedFolder, ensureFolderData]); - - // Also re-fetch when sort changes - useEffect(() => { - if (selectedFolder && sortRefreshReadyRef?.current) { - ensureFolderData(selectedFolder); - } - }, [documentsSortField, documentsSortDirection, selectedFolder, ensureFolderData, sortRefreshReadyRef]); - + }, [selectedFolder, documentsSortField, documentsSortDirection, ensureFolderData]); const { searchQuery, @@ -690,28 +674,10 @@ const useDocumentsWorkspace = ({ handleEntrySelection, }); - useEffect(() => { - if (!activeSortRefreshReadyRef.current) { - activeSortRefreshReadyRef.current = true; - return; - } - if (!isFilterActive && token) { - refreshCurrentFolder(); - } - }, [ - documentsSortField, - documentsSortDirection, - isFilterActive, - refreshCurrentFolder, - token, - activeSortRefreshReadyRef, - ]); - const resetWorkspaceState = useCallback(() => { const rootNode = createRootNode(); setFolderNodes(new Map([[rootNode.id, rootNode]])); setSelectedFolder('root'); - setCurrentFolder(null); setCurrentSubfolders([]); setDocuments([]); setSelectedEntries([]); @@ -747,9 +713,7 @@ const useDocumentsWorkspace = ({ setSelectedEntries, setSelectionOrder, setFolderNodes, - // setFolderContents, setSelectedFolder, - setCurrentFolder, setCurrentSubfolders, setDocuments, setDraggedDocumentIds, @@ -858,7 +822,6 @@ const useDocumentsWorkspace = ({ setSelectedFolder, notifyApiError, setStatusMessage, - setCurrentFolder, navigate, handleFileDrop, moveDocumentsToFolder, @@ -1184,13 +1147,9 @@ const useDocumentsWorkspace = ({ }, }); - const breadcrumbs = useWorkspaceBreadcrumbs({ - selectedFolder, - folderNodes, - currentFolder, - breadcrumbFetchRef, - ensureFolderData, - }); + const breadcrumbs = useMemo(() => { + return resolveBreadcrumbs(selectedFolder || 'root', folderNodes as any); + }, [selectedFolder, folderNodes]); const { handleTenantSelect } = useTenantManager({ appDispatch, diff --git a/frontend/src/documents/features/folders/useFolderTreeActions.ts b/frontend/src/documents/features/folders/useFolderTreeActions.ts index 360dd15..2022fde 100644 --- a/frontend/src/documents/features/folders/useFolderTreeActions.ts +++ b/frontend/src/documents/features/folders/useFolderTreeActions.ts @@ -36,7 +36,6 @@ interface UseFolderTreeActionsOptions { setSelectedFolder: (folderId: FolderKey) => void; notifyApiError: (error: unknown, message?: string) => void; setStatusMessage: (message: string, level?: string) => void; - setCurrentFolder: (updater: (prev: any) => any) => void; navigate?: (path: string, options?: { replace?: boolean }) => void; handleFileDrop: (dataTransfer: DataTransfer, folderId: FolderKey) => Promise | void; moveDocumentsToFolder: (docIds: FolderId[], folderId: FolderKey) => Promise; @@ -56,7 +55,6 @@ const useFolderTreeActions = ({ setSelectedFolder, notifyApiError, setStatusMessage, - setCurrentFolder, navigate, handleFileDrop, moveDocumentsToFolder, @@ -202,7 +200,6 @@ const useFolderTreeActions = ({ return next; }); - setCurrentFolder((prev) => (prev?.id === folderId ? { ...prev, name: trimmed } : prev)); setStatusMessage('Folder renamed.', 'success'); return true; } catch (error) { @@ -213,7 +210,6 @@ const useFolderTreeActions = ({ }, [ notifyApiError, - setCurrentFolder, setFolderNodes, setStatusMessage, token, diff --git a/frontend/src/documents/logic/breadcrumbs.ts b/frontend/src/documents/logic/breadcrumbs.ts new file mode 100644 index 0000000..4ddb70c --- /dev/null +++ b/frontend/src/documents/logic/breadcrumbs.ts @@ -0,0 +1,40 @@ +import type { FolderNode } from '../../types/documents'; +import type { FolderNodeId } from '../../types/identifiers'; + +export type Breadcrumb = FolderNode; + +export const resolveBreadcrumbs = ( + startFolderId: FolderNodeId, + folderNodes: Map +): Breadcrumb[] => { + const chain: Breadcrumb[] = []; + const seen = new Set(); + let currentId: FolderNodeId | null = startFolderId; + let guard = 0; + + while (currentId && !seen.has(currentId) && guard < 64) { + guard += 1; + seen.add(currentId); + + const node = folderNodes.get(currentId); + if (node) { + chain.push(node); + currentId = node.parentId as FolderNodeId; + } else { + break; + } + } + + const ordered: Breadcrumb[] = []; + const seenOrdered = new Set(); + + for (let i = chain.length - 1; i >= 0; i--) { + const crumb = chain[i]; + if (!seenOrdered.has(crumb.id)) { + seenOrdered.add(crumb.id); + ordered.push(crumb); + } + } + + return ordered; +}; diff --git a/frontend/src/documents/logic/useWorkspaceBreadcrumbs.ts b/frontend/src/documents/logic/useWorkspaceBreadcrumbs.ts deleted file mode 100644 index 3033686..0000000 --- a/frontend/src/documents/logic/useWorkspaceBreadcrumbs.ts +++ /dev/null @@ -1,105 +0,0 @@ -import React, { useEffect, useMemo } from 'react'; -import { DEFAULT_FOLDER_NAME } from '../../app/workspaceUtils'; -import type { FolderId as FolderIdentifier } from '../../types/identifiers'; - -type FolderId = FolderIdentifier | 'root'; - -interface UseWorkspaceBreadcrumbsArgs { - selectedFolder: FolderId | null; - folderNodes: Map; - currentFolder: { id?: FolderId; name?: string | null; parentId?: FolderId | null; parent_id?: FolderId | null } | null; - breadcrumbFetchRef: React.MutableRefObject>; - ensureFolderData: (folderId: FolderId, options?: Record) => Promise; -} - -const useWorkspaceBreadcrumbs = ({ - selectedFolder, - folderNodes, - currentFolder, - breadcrumbFetchRef, - ensureFolderData, -}: UseWorkspaceBreadcrumbsArgs) => { - const { breadcrumbs, missingBreadcrumbAncestors } = useMemo(() => { - const chain: Array<{ id: FolderId; name?: string | null }> = []; - const seen = new Set(); - const pending = new Set(); - let currentId: FolderId | null = (selectedFolder || 'root') as FolderId; - let guard = 0; - - 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 as FolderId); - if (node) { - chain.push({ id: currentId, name: node.name || 'Folder' }); - currentId = (node.parentId ?? node.parent_id ?? 'root') as FolderId; - continue; - } - - let fallbackName: string | null = '…'; - let parentId: FolderId | null = null; - - if (currentFolder && currentFolder.id === currentId) { - fallbackName = currentFolder.name; - parentId = (currentFolder.parent_id ?? currentFolder.parentId ?? 'root') as FolderId; - } - - chain.push({ id: currentId, name: fallbackName }); - pending.add(currentId); - currentId = parentId as FolderId | null; - } - - if (!chain.some((crumb) => crumb.id === 'root')) { - chain.push({ id: 'root', name: DEFAULT_FOLDER_NAME }); - } - - const ordered: Array<{ id: FolderId; name?: string | null }> = []; - 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 (!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, breadcrumbFetchRef]); - - return breadcrumbs; -}; - -export default useWorkspaceBreadcrumbs; diff --git a/frontend/src/viewer/logic/useDetailWorkspace.ts b/frontend/src/viewer/logic/useDetailWorkspace.ts index ecac2ae..f552c19 100644 --- a/frontend/src/viewer/logic/useDetailWorkspace.ts +++ b/frontend/src/viewer/logic/useDetailWorkspace.ts @@ -2,11 +2,11 @@ import { useCallback, useEffect, useMemo } from 'react'; import type { MutableRefObject } from 'react'; import { resolveDocumentAssetUrl } from '../../lib/assets/AssetManager'; import { useDetailPanel } from '../../app/useDetailPanel'; -import { DEFAULT_FOLDER_NAME } from '../../app/workspaceUtils'; import type { DocumentInfoPanelProps } from '../components/DocumentInfoPanel'; import type { EnsureAssetUrl, GetAsset } from '../../lib/assets/AssetManager'; import type { Identifier } from '../../types/identifiers'; import type { Document } from '../../types/documents'; +import { resolveBreadcrumbs } from '../../documents/logic/breadcrumbs'; interface FolderNode { id: Identifier | 'root'; @@ -139,45 +139,7 @@ const useDetailWorkspace = ({ 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(); + return resolveBreadcrumbs(folderId || 'root', folderNodes as any); }, [folderNodes], );