refactor: Replace useWorkspaceBreadcrumbs hook with resolveBreadcrumbs utility
This commit is contained in:
@@ -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],
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user