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 useCorrespondents from './useCorrespondents';
|
||||||
import useDocumentCorrespondentActions from '../features/correspondents/useDocumentCorrespondentActions';
|
import useDocumentCorrespondentActions from '../features/correspondents/useDocumentCorrespondentActions';
|
||||||
import usePasskeys from '../../settings/usePasskeys';
|
import usePasskeys from '../../settings/usePasskeys';
|
||||||
import useWorkspaceBreadcrumbs from '../logic/useWorkspaceBreadcrumbs';
|
import { resolveBreadcrumbs } from '../logic/breadcrumbs';
|
||||||
import useWorkspaceSelectionSync from '../features/selection/useWorkspaceSelectionSync';
|
import useWorkspaceSelectionSync from '../features/selection/useWorkspaceSelectionSync';
|
||||||
import type { DocumentId, FolderNodeId, Identifier } from '../../types/identifiers';
|
import type { DocumentId, FolderNodeId, Identifier } from '../../types/identifiers';
|
||||||
|
|
||||||
@@ -100,7 +100,6 @@ interface UseDocumentsWorkspaceOptions {
|
|||||||
onDocumentsSortDirectionToggle?: () => void;
|
onDocumentsSortDirectionToggle?: () => void;
|
||||||
searchIncludeDescendants?: boolean;
|
searchIncludeDescendants?: boolean;
|
||||||
onSetSearchIncludeDescendants?: (value: boolean) => void;
|
onSetSearchIncludeDescendants?: (value: boolean) => void;
|
||||||
sortRefreshReadyRef?: MutableRefObject<boolean>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const useDocumentsWorkspace = ({
|
const useDocumentsWorkspace = ({
|
||||||
@@ -114,7 +113,6 @@ const useDocumentsWorkspace = ({
|
|||||||
onDocumentsSortDirectionToggle,
|
onDocumentsSortDirectionToggle,
|
||||||
searchIncludeDescendants = true,
|
searchIncludeDescendants = true,
|
||||||
onSetSearchIncludeDescendants,
|
onSetSearchIncludeDescendants,
|
||||||
sortRefreshReadyRef,
|
|
||||||
}: UseDocumentsWorkspaceOptions = {}) => {
|
}: UseDocumentsWorkspaceOptions = {}) => {
|
||||||
const handleDocumentsViewModeChange = onDocumentsViewModeChange || noop;
|
const handleDocumentsViewModeChange = onDocumentsViewModeChange || noop;
|
||||||
const handleDocumentsSortFieldChange = onDocumentsSortFieldChange || noop;
|
const handleDocumentsSortFieldChange = onDocumentsSortFieldChange || noop;
|
||||||
@@ -133,8 +131,6 @@ const useDocumentsWorkspace = ({
|
|||||||
activeSortDirectionRef.current = documentsSortDirection;
|
activeSortDirectionRef.current = documentsSortDirection;
|
||||||
}, [documentsSortDirection, activeSortDirectionRef]);
|
}, [documentsSortDirection, activeSortDirectionRef]);
|
||||||
|
|
||||||
const fallbackSortRefreshReadyRef = useRef(false);
|
|
||||||
const activeSortRefreshReadyRef = sortRefreshReadyRef || fallbackSortRefreshReadyRef;
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const appState = useAppState();
|
const appState = useAppState();
|
||||||
@@ -332,7 +328,6 @@ const useDocumentsWorkspace = ({
|
|||||||
foldersManager,
|
foldersManager,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [currentFolder, setCurrentFolder] = useState<FolderContentsEntry['folder'] | null>(null);
|
|
||||||
const [currentSubfolders, setCurrentSubfolders] = useState<FolderContentsEntry['subfolders']>([]);
|
const [currentSubfolders, setCurrentSubfolders] = useState<FolderContentsEntry['subfolders']>([]);
|
||||||
|
|
||||||
const ensureFolderData = useCallback(
|
const ensureFolderData = useCallback(
|
||||||
@@ -358,7 +353,6 @@ const useDocumentsWorkspace = ({
|
|||||||
setDocuments((data.documents || []) as Document[]);
|
setDocuments((data.documents || []) as Document[]);
|
||||||
}
|
}
|
||||||
setCurrentSubfolders((data.subfolders || []) as any[]);
|
setCurrentSubfolders((data.subfolders || []) as any[]);
|
||||||
setCurrentFolder(data.folder || null);
|
|
||||||
|
|
||||||
// Update selection state based on new documents
|
// Update selection state based on new documents
|
||||||
if (includeDocuments) {
|
if (includeDocuments) {
|
||||||
@@ -396,7 +390,6 @@ const useDocumentsWorkspace = ({
|
|||||||
activeSortFieldRef,
|
activeSortFieldRef,
|
||||||
activeSortDirectionRef,
|
activeSortDirectionRef,
|
||||||
setDocuments,
|
setDocuments,
|
||||||
setCurrentFolder,
|
|
||||||
setSelectedEntries,
|
setSelectedEntries,
|
||||||
notifyApiError,
|
notifyApiError,
|
||||||
setCurrentSubfolders,
|
setCurrentSubfolders,
|
||||||
@@ -404,20 +397,11 @@ const useDocumentsWorkspace = ({
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Reactively fetch documents when selectedFolder changes
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedFolder) {
|
if (selectedFolder) {
|
||||||
ensureFolderData(selectedFolder);
|
ensureFolderData(selectedFolder);
|
||||||
}
|
}
|
||||||
}, [selectedFolder, ensureFolderData]);
|
}, [selectedFolder, documentsSortField, documentsSortDirection, ensureFolderData]);
|
||||||
|
|
||||||
// Also re-fetch when sort changes
|
|
||||||
useEffect(() => {
|
|
||||||
if (selectedFolder && sortRefreshReadyRef?.current) {
|
|
||||||
ensureFolderData(selectedFolder);
|
|
||||||
}
|
|
||||||
}, [documentsSortField, documentsSortDirection, selectedFolder, ensureFolderData, sortRefreshReadyRef]);
|
|
||||||
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
searchQuery,
|
searchQuery,
|
||||||
@@ -690,28 +674,10 @@ const useDocumentsWorkspace = ({
|
|||||||
handleEntrySelection,
|
handleEntrySelection,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!activeSortRefreshReadyRef.current) {
|
|
||||||
activeSortRefreshReadyRef.current = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!isFilterActive && token) {
|
|
||||||
refreshCurrentFolder();
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
documentsSortField,
|
|
||||||
documentsSortDirection,
|
|
||||||
isFilterActive,
|
|
||||||
refreshCurrentFolder,
|
|
||||||
token,
|
|
||||||
activeSortRefreshReadyRef,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const resetWorkspaceState = useCallback(() => {
|
const resetWorkspaceState = useCallback(() => {
|
||||||
const rootNode = createRootNode();
|
const rootNode = createRootNode();
|
||||||
setFolderNodes(new Map([[rootNode.id, rootNode]]));
|
setFolderNodes(new Map([[rootNode.id, rootNode]]));
|
||||||
setSelectedFolder('root');
|
setSelectedFolder('root');
|
||||||
setCurrentFolder(null);
|
|
||||||
setCurrentSubfolders([]);
|
setCurrentSubfolders([]);
|
||||||
setDocuments([]);
|
setDocuments([]);
|
||||||
setSelectedEntries([]);
|
setSelectedEntries([]);
|
||||||
@@ -747,9 +713,7 @@ const useDocumentsWorkspace = ({
|
|||||||
setSelectedEntries,
|
setSelectedEntries,
|
||||||
setSelectionOrder,
|
setSelectionOrder,
|
||||||
setFolderNodes,
|
setFolderNodes,
|
||||||
// setFolderContents,
|
|
||||||
setSelectedFolder,
|
setSelectedFolder,
|
||||||
setCurrentFolder,
|
|
||||||
setCurrentSubfolders,
|
setCurrentSubfolders,
|
||||||
setDocuments,
|
setDocuments,
|
||||||
setDraggedDocumentIds,
|
setDraggedDocumentIds,
|
||||||
@@ -858,7 +822,6 @@ const useDocumentsWorkspace = ({
|
|||||||
setSelectedFolder,
|
setSelectedFolder,
|
||||||
notifyApiError,
|
notifyApiError,
|
||||||
setStatusMessage,
|
setStatusMessage,
|
||||||
setCurrentFolder,
|
|
||||||
navigate,
|
navigate,
|
||||||
handleFileDrop,
|
handleFileDrop,
|
||||||
moveDocumentsToFolder,
|
moveDocumentsToFolder,
|
||||||
@@ -1184,13 +1147,9 @@ const useDocumentsWorkspace = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const breadcrumbs = useWorkspaceBreadcrumbs({
|
const breadcrumbs = useMemo(() => {
|
||||||
selectedFolder,
|
return resolveBreadcrumbs(selectedFolder || 'root', folderNodes as any);
|
||||||
folderNodes,
|
}, [selectedFolder, folderNodes]);
|
||||||
currentFolder,
|
|
||||||
breadcrumbFetchRef,
|
|
||||||
ensureFolderData,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { handleTenantSelect } = useTenantManager({
|
const { handleTenantSelect } = useTenantManager({
|
||||||
appDispatch,
|
appDispatch,
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ interface UseFolderTreeActionsOptions {
|
|||||||
setSelectedFolder: (folderId: FolderKey) => void;
|
setSelectedFolder: (folderId: FolderKey) => void;
|
||||||
notifyApiError: (error: unknown, message?: string) => void;
|
notifyApiError: (error: unknown, message?: string) => void;
|
||||||
setStatusMessage: (message: string, level?: string) => void;
|
setStatusMessage: (message: string, level?: string) => void;
|
||||||
setCurrentFolder: (updater: (prev: any) => any) => void;
|
|
||||||
navigate?: (path: string, options?: { replace?: boolean }) => void;
|
navigate?: (path: string, options?: { replace?: boolean }) => void;
|
||||||
handleFileDrop: (dataTransfer: DataTransfer, folderId: FolderKey) => Promise<void> | void;
|
handleFileDrop: (dataTransfer: DataTransfer, folderId: FolderKey) => Promise<void> | void;
|
||||||
moveDocumentsToFolder: (docIds: FolderId[], folderId: FolderKey) => Promise<void>;
|
moveDocumentsToFolder: (docIds: FolderId[], folderId: FolderKey) => Promise<void>;
|
||||||
@@ -56,7 +55,6 @@ const useFolderTreeActions = ({
|
|||||||
setSelectedFolder,
|
setSelectedFolder,
|
||||||
notifyApiError,
|
notifyApiError,
|
||||||
setStatusMessage,
|
setStatusMessage,
|
||||||
setCurrentFolder,
|
|
||||||
navigate,
|
navigate,
|
||||||
handleFileDrop,
|
handleFileDrop,
|
||||||
moveDocumentsToFolder,
|
moveDocumentsToFolder,
|
||||||
@@ -202,7 +200,6 @@ const useFolderTreeActions = ({
|
|||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
|
|
||||||
setCurrentFolder((prev) => (prev?.id === folderId ? { ...prev, name: trimmed } : prev));
|
|
||||||
setStatusMessage('Folder renamed.', 'success');
|
setStatusMessage('Folder renamed.', 'success');
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -213,7 +210,6 @@ const useFolderTreeActions = ({
|
|||||||
},
|
},
|
||||||
[
|
[
|
||||||
notifyApiError,
|
notifyApiError,
|
||||||
setCurrentFolder,
|
|
||||||
setFolderNodes,
|
setFolderNodes,
|
||||||
setStatusMessage,
|
setStatusMessage,
|
||||||
token,
|
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 type { MutableRefObject } from 'react';
|
||||||
import { resolveDocumentAssetUrl } from '../../lib/assets/AssetManager';
|
import { resolveDocumentAssetUrl } from '../../lib/assets/AssetManager';
|
||||||
import { useDetailPanel } from '../../app/useDetailPanel';
|
import { useDetailPanel } from '../../app/useDetailPanel';
|
||||||
import { DEFAULT_FOLDER_NAME } from '../../app/workspaceUtils';
|
|
||||||
import type { DocumentInfoPanelProps } from '../components/DocumentInfoPanel';
|
import type { DocumentInfoPanelProps } from '../components/DocumentInfoPanel';
|
||||||
import type { EnsureAssetUrl, GetAsset } from '../../lib/assets/AssetManager';
|
import type { EnsureAssetUrl, GetAsset } from '../../lib/assets/AssetManager';
|
||||||
import type { Identifier } from '../../types/identifiers';
|
import type { Identifier } from '../../types/identifiers';
|
||||||
import type { Document } from '../../types/documents';
|
import type { Document } from '../../types/documents';
|
||||||
|
import { resolveBreadcrumbs } from '../../documents/logic/breadcrumbs';
|
||||||
|
|
||||||
interface FolderNode {
|
interface FolderNode {
|
||||||
id: Identifier | 'root';
|
id: Identifier | 'root';
|
||||||
@@ -139,45 +139,7 @@ const useDetailWorkspace = ({
|
|||||||
|
|
||||||
const resolveFolderPath = useCallback(
|
const resolveFolderPath = useCallback(
|
||||||
(folderId) => {
|
(folderId) => {
|
||||||
if (!folderId || folderId === 'root') {
|
return resolveBreadcrumbs(folderId || 'root', folderNodes as any);
|
||||||
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();
|
|
||||||
},
|
},
|
||||||
[folderNodes],
|
[folderNodes],
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user