refactor: Replace useWorkspaceBreadcrumbs hook with resolveBreadcrumbs utility

This commit is contained in:
2025-12-07 22:10:34 +01:00
parent d62f462129
commit 53664f2421
5 changed files with 47 additions and 195 deletions
@@ -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<boolean>;
}
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<FolderContentsEntry['folder'] | null>(null);
const [currentSubfolders, setCurrentSubfolders] = useState<FolderContentsEntry['subfolders']>([]);
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,
@@ -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> | void;
moveDocumentsToFolder: (docIds: FolderId[], folderId: FolderKey) => Promise<void>;
@@ -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,
@@ -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<FolderNodeId, FolderNode>
): Breadcrumb[] => {
const chain: Breadcrumb[] = [];
const seen = new Set<FolderNodeId>();
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<FolderNodeId>();
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;
};
@@ -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<FolderId, { id?: FolderId; name?: string | null; parentId?: FolderId | null; parent_id?: FolderId | null }>;
currentFolder: { id?: FolderId; name?: string | null; parentId?: FolderId | null; parent_id?: FolderId | null } | null;
breadcrumbFetchRef: React.MutableRefObject<Set<FolderId>>;
ensureFolderData: (folderId: FolderId, options?: Record<string, unknown>) => Promise<unknown>;
}
const useWorkspaceBreadcrumbs = ({
selectedFolder,
folderNodes,
currentFolder,
breadcrumbFetchRef,
ensureFolderData,
}: UseWorkspaceBreadcrumbsArgs) => {
const { breadcrumbs, missingBreadcrumbAncestors } = useMemo(() => {
const chain: Array<{ id: FolderId; name?: string | null }> = [];
const seen = new Set<FolderId>();
const pending = new Set<FolderId>();
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<FolderId>();
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;
@@ -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],
);