feat: refine frontend folder management and display by merging folder tree data.
This commit is contained in:
@@ -227,6 +227,12 @@ class FoldersManager<T extends ManagedFolder = ManagedFolder> {
|
|||||||
|
|
||||||
return this.treePromise;
|
return this.treePromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
invalidateTree() {
|
||||||
|
this.treeSnapshot = [];
|
||||||
|
this.treePromise = null;
|
||||||
|
this.emit();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default FoldersManager;
|
export default FoldersManager;
|
||||||
|
|||||||
@@ -57,7 +57,6 @@ import { listFolderContents } from '../../lib/api/apiClient';
|
|||||||
import { useApi } from '../../lib/context/ApiContext';
|
import { useApi } from '../../lib/context/ApiContext';
|
||||||
import { useWorkspaceSelection } from '../../app/useWorkspaceSelection';
|
import { useWorkspaceSelection } from '../../app/useWorkspaceSelection';
|
||||||
import useDocumentPreview from '../../app/useDocumentPreview';
|
import useDocumentPreview from '../../app/useDocumentPreview';
|
||||||
import { createRootNode } from '../../app/workspaceUtils';
|
|
||||||
import type { DocumentId, FolderNodeId, Identifier } from '../../types/identifiers';
|
import type { DocumentId, FolderNodeId, Identifier } from '../../types/identifiers';
|
||||||
|
|
||||||
const EntryType = Object.freeze({
|
const EntryType = Object.freeze({
|
||||||
@@ -250,7 +249,6 @@ const useDocumentsWorkspace = ({
|
|||||||
});
|
});
|
||||||
const {
|
const {
|
||||||
folderNodes,
|
folderNodes,
|
||||||
setFolderNodes,
|
|
||||||
selectedFolder,
|
selectedFolder,
|
||||||
setSelectedFolder,
|
setSelectedFolder,
|
||||||
currentFolderName,
|
currentFolderName,
|
||||||
@@ -261,6 +259,7 @@ const useDocumentsWorkspace = ({
|
|||||||
const folderState = {
|
const folderState = {
|
||||||
...folderStateRaw,
|
...folderStateRaw,
|
||||||
setCreatingFolder,
|
setCreatingFolder,
|
||||||
|
foldersManager,
|
||||||
};
|
};
|
||||||
|
|
||||||
const [currentSubfolders, setCurrentSubfolders] = useState<Array<{ id?: FolderNodeId; name?: string | null;[key: string]: unknown }>>([]);
|
const [currentSubfolders, setCurrentSubfolders] = useState<Array<{ id?: FolderNodeId; name?: string | null;[key: string]: unknown }>>([]);
|
||||||
@@ -299,7 +298,10 @@ const useDocumentsWorkspace = ({
|
|||||||
if (includeDocuments) {
|
if (includeDocuments) {
|
||||||
setDocuments((data.documents || []) as Document[]);
|
setDocuments((data.documents || []) as Document[]);
|
||||||
}
|
}
|
||||||
setCurrentSubfolders((data.subfolders || []) as any[]);
|
|
||||||
|
const subfolders = (data.subfolders || []) as any[];
|
||||||
|
setCurrentSubfolders(subfolders);
|
||||||
|
foldersManager.ingest(subfolders);
|
||||||
|
|
||||||
if (includeDocuments) {
|
if (includeDocuments) {
|
||||||
setSelectedEntries((prev) => reconcileSelectionWithFolderData(
|
setSelectedEntries((prev) => reconcileSelectionWithFolderData(
|
||||||
@@ -316,6 +318,7 @@ const useDocumentsWorkspace = ({
|
|||||||
setCurrentSubfolders,
|
setCurrentSubfolders,
|
||||||
reconcileSelectionWithFolderData,
|
reconcileSelectionWithFolderData,
|
||||||
selectedFolderRef,
|
selectedFolderRef,
|
||||||
|
foldersManager,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -580,8 +583,6 @@ const useDocumentsWorkspace = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const resetWorkspaceState = useCallback(() => {
|
const resetWorkspaceState = useCallback(() => {
|
||||||
const rootNode = createRootNode();
|
|
||||||
setFolderNodes(new Map([[rootNode.id, rootNode]]));
|
|
||||||
setSelectedFolder('root');
|
setSelectedFolder('root');
|
||||||
setCurrentSubfolders([]);
|
setCurrentSubfolders([]);
|
||||||
setDocuments([]);
|
setDocuments([]);
|
||||||
@@ -617,7 +618,6 @@ const useDocumentsWorkspace = ({
|
|||||||
setFocusedDocumentId,
|
setFocusedDocumentId,
|
||||||
setSelectedEntries,
|
setSelectedEntries,
|
||||||
setSelectionOrder,
|
setSelectionOrder,
|
||||||
setFolderNodes,
|
|
||||||
setSelectedFolder,
|
setSelectedFolder,
|
||||||
setCurrentSubfolders,
|
setCurrentSubfolders,
|
||||||
setDocuments,
|
setDocuments,
|
||||||
@@ -637,9 +637,10 @@ const useDocumentsWorkspace = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (appStatus === 'logged-out' || appStatus === 'selecting-tenant') {
|
if (appStatus === 'logged-out' || appStatus === 'selecting-tenant') {
|
||||||
|
foldersManager.invalidateTree();
|
||||||
resetWorkspaceState();
|
resetWorkspaceState();
|
||||||
}
|
}
|
||||||
}, [appStatus, resetWorkspaceState]);
|
}, [appStatus, resetWorkspaceState, foldersManager]);
|
||||||
|
|
||||||
|
|
||||||
const removeDocumentsFromCaches = useCallback(
|
const removeDocumentsFromCaches = useCallback(
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState, useSyncExternalStore } from 'react';
|
||||||
import { createRootNode, DEFAULT_FOLDER_NAME, flattenFolderTree } from '../../../app/workspaceUtils';
|
import { createRootNode, DEFAULT_FOLDER_NAME, flattenFolderTree } from '../../../app/workspaceUtils';
|
||||||
import type { FolderNodeId, FolderId } from '../../../types/identifiers';
|
import type { FolderNodeId, FolderId } from '../../../types/identifiers';
|
||||||
import type { FolderNode } from '../../../types/documents';
|
import type { FolderNode } from '../../../types/documents';
|
||||||
@@ -18,67 +18,81 @@ const useFolderTree = ({
|
|||||||
initialSelectedFolder = 'root',
|
initialSelectedFolder = 'root',
|
||||||
foldersManager,
|
foldersManager,
|
||||||
}: UseFolderTreeOptions) => {
|
}: UseFolderTreeOptions) => {
|
||||||
const [folderNodes, setFolderNodes] = useState<Map<FolderNodeId, FolderNode>>(() => {
|
// Subscribe to manager updates
|
||||||
const rootNode = createRootNode() as FolderNode;
|
const managerSnapshot = useSyncExternalStore(
|
||||||
return new Map([[rootNode.id, rootNode]]);
|
useCallback(cb => foldersManager ? foldersManager.subscribe(cb) : () => { }, [foldersManager]),
|
||||||
});
|
() => foldersManager ? foldersManager.getSnapshot() : null,
|
||||||
|
() => foldersManager ? foldersManager.getSnapshot() : null,
|
||||||
|
);
|
||||||
|
|
||||||
|
const treeSnapshot = useSyncExternalStore(
|
||||||
|
useCallback(cb => foldersManager ? foldersManager.subscribe(cb) : () => { }, [foldersManager]),
|
||||||
|
() => foldersManager ? foldersManager.getTreeSnapshot() : [],
|
||||||
|
() => foldersManager ? foldersManager.getTreeSnapshot() : [],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Track expanded state locally
|
||||||
|
const [expandedIds, setExpandedIds] = useState<Set<FolderNodeId>>(new Set(['root']));
|
||||||
|
|
||||||
|
// Fetch tree on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchTree = async () => {
|
if (foldersManager) {
|
||||||
try {
|
foldersManager.ensureTree().catch(err => console.error(err));
|
||||||
if (!foldersManager) return;
|
}
|
||||||
|
|
||||||
// Use FoldersManager as the single source of truth for fetching the tree
|
|
||||||
const roots = await foldersManager.ensureTree();
|
|
||||||
|
|
||||||
setFolderNodes((prev) => {
|
|
||||||
const next = new Map(prev);
|
|
||||||
const rootChildren: FolderNodeId[] = [];
|
|
||||||
|
|
||||||
const flatData = flattenFolderTree(roots);
|
|
||||||
|
|
||||||
flatData.forEach((item) => {
|
|
||||||
const id = item.id as FolderNodeId;
|
|
||||||
const parentId = (item.parent_id || 'root') as FolderNodeId;
|
|
||||||
// item.children is now FolderTreeNode[], so map to IDs
|
|
||||||
const children = (item.children || []).map((c) => c.id as FolderNodeId);
|
|
||||||
|
|
||||||
// Preserve existing UI state (expanded, loaded)
|
|
||||||
const existing = prev.get(id);
|
|
||||||
|
|
||||||
next.set(id, {
|
|
||||||
id,
|
|
||||||
name: item.name,
|
|
||||||
parentId,
|
|
||||||
children,
|
|
||||||
expanded: existing?.expanded ?? false,
|
|
||||||
loaded: true,
|
|
||||||
hasChildren: children.length > 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (parentId === 'root') {
|
|
||||||
rootChildren.push(id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const root = next.get('root');
|
|
||||||
if (root) {
|
|
||||||
next.set('root', {
|
|
||||||
...(root as FolderNode),
|
|
||||||
children: rootChildren,
|
|
||||||
hasChildren: rootChildren.length > 0,
|
|
||||||
loaded: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to fetch folder tree', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
fetchTree();
|
|
||||||
}, [foldersManager]);
|
}, [foldersManager]);
|
||||||
|
|
||||||
|
const folderNodes = useMemo(() => {
|
||||||
|
if (!foldersManager || !managerSnapshot) {
|
||||||
|
const rootNode = createRootNode() as FolderNode;
|
||||||
|
return new Map([[rootNode.id, rootNode]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const map = new Map<FolderNodeId, FolderNode>();
|
||||||
|
// Use the synced tree snapshot
|
||||||
|
const roots = treeSnapshot;
|
||||||
|
|
||||||
|
if (roots.length === 0) {
|
||||||
|
// Return placeholder or empty
|
||||||
|
const rootNode = createRootNode() as FolderNode;
|
||||||
|
return new Map([[rootNode.id, rootNode]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const flatStructure = flattenFolderTree(roots);
|
||||||
|
// Use the synced data snapshot
|
||||||
|
const dataSnapshot = managerSnapshot;
|
||||||
|
|
||||||
|
// Reconstruct the nodes integrating data from byId and structure from tree
|
||||||
|
// plus local UI state (expanded)
|
||||||
|
const rootChildren: FolderNodeId[] = [];
|
||||||
|
|
||||||
|
flatStructure.forEach((item) => {
|
||||||
|
const id = item.id as FolderNodeId;
|
||||||
|
const data = dataSnapshot.get(id);
|
||||||
|
|
||||||
|
// Merge: structure (children, parent) comes from flatStructure (which comes from treeSnapshot)
|
||||||
|
// Data (name) comes from dataSnapshot (byId) to ensure renames propagate instantly
|
||||||
|
const name = data?.name ?? item.name;
|
||||||
|
const parentId = (item.parent_id || 'root') as FolderNodeId;
|
||||||
|
const children = (item.children || []).map(c => c.id as FolderNodeId);
|
||||||
|
|
||||||
|
map.set(id, {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
parentId,
|
||||||
|
children,
|
||||||
|
expanded: expandedIds.has(id),
|
||||||
|
loaded: true,
|
||||||
|
hasChildren: children.length > 0
|
||||||
|
});
|
||||||
|
|
||||||
|
if (parentId === 'root') {
|
||||||
|
rootChildren.push(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}, [foldersManager, managerSnapshot, treeSnapshot, expandedIds]);
|
||||||
|
|
||||||
const [selectedFolder, setSelectedFolder] = useState<FolderNodeId>(initialSelectedFolder || 'root');
|
const [selectedFolder, setSelectedFolder] = useState<FolderNodeId>(initialSelectedFolder || 'root');
|
||||||
|
|
||||||
const isInvalidFolderDrop = useCallback(
|
const isInvalidFolderDrop = useCallback(
|
||||||
@@ -108,8 +122,7 @@ const useFolderTree = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const resetFolderTreeState = useCallback(() => {
|
const resetFolderTreeState = useCallback(() => {
|
||||||
const rootNode = createRootNode() as FolderNode;
|
setExpandedIds(new Set(['root']));
|
||||||
setFolderNodes(new Map<FolderNodeId, FolderNode>([[rootNode.id, rootNode]]));
|
|
||||||
setSelectedFolder('root');
|
setSelectedFolder('root');
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -157,12 +170,6 @@ const useFolderTree = ({
|
|||||||
}, [folderNodes]);
|
}, [folderNodes]);
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (foldersManager && folderNodes.size > 0) {
|
|
||||||
foldersManager.ingest(Array.from(folderNodes.values()));
|
|
||||||
}
|
|
||||||
}, [foldersManager, folderNodes]);
|
|
||||||
|
|
||||||
const folderLabelMap = useMemo(() => {
|
const folderLabelMap = useMemo(() => {
|
||||||
const map = new Map<FolderNodeId, string>();
|
const map = new Map<FolderNodeId, string>();
|
||||||
folderOptions.forEach((option) => {
|
folderOptions.forEach((option) => {
|
||||||
@@ -171,9 +178,18 @@ const useFolderTree = ({
|
|||||||
return map;
|
return map;
|
||||||
}, [folderOptions]);
|
}, [folderOptions]);
|
||||||
|
|
||||||
|
// Exposed helper to toggle expansion (if needed by consumers who can reach here)
|
||||||
|
const toggleFolder = useCallback((folderId: FolderNodeId) => {
|
||||||
|
setExpandedIds(prev => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (next.has(folderId)) next.delete(folderId);
|
||||||
|
else next.add(folderId);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
folderNodes,
|
folderNodes,
|
||||||
setFolderNodes,
|
|
||||||
selectedFolder,
|
selectedFolder,
|
||||||
setSelectedFolder,
|
setSelectedFolder,
|
||||||
currentFolderName,
|
currentFolderName,
|
||||||
@@ -181,6 +197,8 @@ const useFolderTree = ({
|
|||||||
folderLabelMap,
|
folderLabelMap,
|
||||||
isInvalidFolderDrop,
|
isInvalidFolderDrop,
|
||||||
resetFolderTreeState,
|
resetFolderTreeState,
|
||||||
|
toggleFolder,
|
||||||
|
setExpandedIds
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useCallback, useMemo } from 'react';
|
|||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import type { DragEvent } from 'react';
|
import type { DragEvent } from 'react';
|
||||||
import { useStatusToast } from '../../../lib/context/StatusToastContext';
|
import { useStatusToast } from '../../../lib/context/StatusToastContext';
|
||||||
import { DEFAULT_FOLDER_NAME, hasFiles } from '../../../app/workspaceUtils';
|
import { hasFiles } from '../../../app/workspaceUtils';
|
||||||
import {
|
import {
|
||||||
createFolder,
|
createFolder,
|
||||||
deleteFolder,
|
deleteFolder,
|
||||||
@@ -35,11 +35,12 @@ import useNotifyApiError from '../../../hooks/useNotifyApiError';
|
|||||||
import type {
|
import type {
|
||||||
FolderState,
|
FolderState,
|
||||||
DragState,
|
DragState,
|
||||||
FolderNode,
|
|
||||||
} from '../../types/workspaceTypes';
|
} from '../../types/workspaceTypes';
|
||||||
|
|
||||||
|
import type FoldersManager from '../../FoldersManager';
|
||||||
|
|
||||||
interface UseFolderTreeActionsOptions {
|
interface UseFolderTreeActionsOptions {
|
||||||
folderState: Pick<FolderState, 'folderNodes' | 'setFolderNodes' | 'selectedFolder' | 'setSelectedFolder' | 'setCreatingFolder'>;
|
folderState: Pick<FolderState, 'folderNodes' | 'selectedFolder' | 'setSelectedFolder' | 'setCreatingFolder'> & { foldersManager?: FoldersManager };
|
||||||
dragState: DragState;
|
dragState: DragState;
|
||||||
actions: {
|
actions: {
|
||||||
handleFileDrop: (dataTransfer: DataTransfer, folderId: FolderKey) => Promise<void> | void;
|
handleFileDrop: (dataTransfer: DataTransfer, folderId: FolderKey) => Promise<void> | void;
|
||||||
@@ -58,10 +59,10 @@ const useFolderTreeActions = ({
|
|||||||
}: UseFolderTreeActionsOptions) => {
|
}: UseFolderTreeActionsOptions) => {
|
||||||
const {
|
const {
|
||||||
folderNodes,
|
folderNodes,
|
||||||
setFolderNodes,
|
|
||||||
selectedFolder,
|
selectedFolder,
|
||||||
setSelectedFolder,
|
setSelectedFolder,
|
||||||
setCreatingFolder,
|
setCreatingFolder,
|
||||||
|
foldersManager,
|
||||||
} = folderState;
|
} = folderState;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -101,50 +102,9 @@ const useFolderTreeActions = ({
|
|||||||
try {
|
try {
|
||||||
await moveFolderRequest(folderId, parent_id);
|
await moveFolderRequest(folderId, parent_id);
|
||||||
|
|
||||||
setFolderNodes((prev: Map<FolderKey, FolderNode>) => {
|
if (foldersManager) {
|
||||||
const next = new Map(prev);
|
foldersManager.invalidateTree();
|
||||||
const currentNode = next.get(folderId);
|
}
|
||||||
if (!currentNode) {
|
|
||||||
return prev;
|
|
||||||
}
|
|
||||||
|
|
||||||
next.set(folderId, { ...currentNode, parentId: parent_id ?? null });
|
|
||||||
|
|
||||||
const previousParent = next.get(previousParentKey);
|
|
||||||
if (previousParent) {
|
|
||||||
const remainingChildren = (previousParent.children || []).filter(
|
|
||||||
(childId) => childId !== folderId,
|
|
||||||
);
|
|
||||||
next.set(previousParentKey, {
|
|
||||||
...previousParent,
|
|
||||||
children: remainingChildren,
|
|
||||||
hasChildren: remainingChildren.length > 0,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!next.has(targetKey)) {
|
|
||||||
next.set(targetKey, {
|
|
||||||
id: targetKey,
|
|
||||||
name: targetKey === 'root' ? DEFAULT_FOLDER_NAME : 'Folder',
|
|
||||||
parentId: targetKey === 'root' ? null : null,
|
|
||||||
children: [],
|
|
||||||
expanded: targetKey === 'root',
|
|
||||||
loaded: false,
|
|
||||||
hasChildren: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const targetNode = next.get(targetKey);
|
|
||||||
if (targetNode && !targetNode.children.includes(folderId)) {
|
|
||||||
next.set(targetKey, {
|
|
||||||
...targetNode,
|
|
||||||
children: [...targetNode.children, folderId],
|
|
||||||
hasChildren: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (selectedFolder === folderId) {
|
if (selectedFolder === folderId) {
|
||||||
setSelectedFolder(folderId);
|
setSelectedFolder(folderId);
|
||||||
@@ -160,7 +120,7 @@ const useFolderTreeActions = ({
|
|||||||
folderNodes,
|
folderNodes,
|
||||||
notifyApiError,
|
notifyApiError,
|
||||||
selectedFolder,
|
selectedFolder,
|
||||||
setFolderNodes,
|
foldersManager,
|
||||||
setSelectedFolder,
|
setSelectedFolder,
|
||||||
showToast,
|
showToast,
|
||||||
],
|
],
|
||||||
@@ -202,14 +162,9 @@ const useFolderTreeActions = ({
|
|||||||
try {
|
try {
|
||||||
await renameFolderRequest(folderId, trimmed);
|
await renameFolderRequest(folderId, trimmed);
|
||||||
|
|
||||||
setFolderNodes((prev: Map<FolderKey, FolderNode>) => {
|
if (foldersManager) {
|
||||||
const next = new Map(prev);
|
foldersManager.ingest([{ id: folderId, name: trimmed }]);
|
||||||
const node = next.get(folderId);
|
}
|
||||||
if (node) {
|
|
||||||
next.set(folderId, { ...node, name: trimmed });
|
|
||||||
}
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
|
|
||||||
showToast('Folder renamed.', 'success');
|
showToast('Folder renamed.', 'success');
|
||||||
return true;
|
return true;
|
||||||
@@ -221,7 +176,7 @@ const useFolderTreeActions = ({
|
|||||||
},
|
},
|
||||||
[
|
[
|
||||||
notifyApiError,
|
notifyApiError,
|
||||||
setFolderNodes,
|
foldersManager,
|
||||||
showToast,
|
showToast,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -250,29 +205,13 @@ const useFolderTreeActions = ({
|
|||||||
throw new Error('Folder creation failed.');
|
throw new Error('Folder creation failed.');
|
||||||
}
|
}
|
||||||
showToast('Folder created.', 'success');
|
showToast('Folder created.', 'success');
|
||||||
setFolderNodes((prev: Map<FolderKey, FolderNode>) => {
|
|
||||||
const next = new Map(prev);
|
if (foldersManager) {
|
||||||
const parentId = folderData.parent_id ?? payload.parent_id ?? 'root';
|
// Ingest the new folder data immediately so it's available
|
||||||
const parentNode = next.get(parentId);
|
foldersManager.ingest([folderData]);
|
||||||
if (parentNode) {
|
// Force tree refresh to update structure
|
||||||
next.set(parentId, {
|
foldersManager.invalidateTree();
|
||||||
...parentNode,
|
}
|
||||||
children: parentNode.children.concat([folderData.id]),
|
|
||||||
loaded: true,
|
|
||||||
hasChildren: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
next.set(folderData.id, {
|
|
||||||
id: folderData.id,
|
|
||||||
name: folderData.name ?? payload.name,
|
|
||||||
parentId: parentId,
|
|
||||||
children: folderData.children || [],
|
|
||||||
expanded: false,
|
|
||||||
loaded: false,
|
|
||||||
hasChildren: Array.isArray(folderData.children) ? folderData.children.length > 0 : false,
|
|
||||||
});
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
|
|
||||||
await selectFolder(folderData.id, { immediate: true });
|
await selectFolder(folderData.id, { immediate: true });
|
||||||
succeeded = true;
|
succeeded = true;
|
||||||
@@ -293,7 +232,7 @@ const useFolderTreeActions = ({
|
|||||||
selectFolder,
|
selectFolder,
|
||||||
selectedFolder,
|
selectedFolder,
|
||||||
setCreatingFolder,
|
setCreatingFolder,
|
||||||
setFolderNodes,
|
foldersManager,
|
||||||
showToast,
|
showToast,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -310,26 +249,13 @@ const useFolderTreeActions = ({
|
|||||||
try {
|
try {
|
||||||
await deleteFolder(folderId);
|
await deleteFolder(folderId);
|
||||||
|
|
||||||
setFolderNodes((prev: Map<FolderKey, FolderNode>) => {
|
if (foldersManager) {
|
||||||
const next = new Map(prev);
|
foldersManager.remove([folderId]);
|
||||||
const node = next.get(folderId);
|
foldersManager.invalidateTree();
|
||||||
next.delete(folderId);
|
}
|
||||||
if (node) {
|
|
||||||
const parentId = node.parentId || 'root';
|
|
||||||
const parentNode = next.get(parentId);
|
|
||||||
if (parentNode) {
|
|
||||||
const remaining = parentNode.children.filter((id) => id !== folderId);
|
|
||||||
next.set(parentId, {
|
|
||||||
...parentNode,
|
|
||||||
children: remaining,
|
|
||||||
hasChildren: remaining.length > 0,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (selectedFolder === folderId) {
|
if (selectedFolder === folderId) {
|
||||||
|
// Fallback selection logic
|
||||||
const node = folderNodes.get(folderId);
|
const node = folderNodes.get(folderId);
|
||||||
const parentId = node?.parentId || 'root';
|
const parentId = node?.parentId || 'root';
|
||||||
setSelectedFolder(parentId);
|
setSelectedFolder(parentId);
|
||||||
@@ -352,7 +278,7 @@ const useFolderTreeActions = ({
|
|||||||
folderNodes,
|
folderNodes,
|
||||||
notifyApiError,
|
notifyApiError,
|
||||||
selectedFolder,
|
selectedFolder,
|
||||||
setFolderNodes,
|
foldersManager,
|
||||||
setSelectedFolder,
|
setSelectedFolder,
|
||||||
showToast,
|
showToast,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -238,9 +238,30 @@ const SelectionFloatingActions: React.FC<SelectionFloatingActionsProps> = ({
|
|||||||
() => foldersManager.getTreeSnapshot(),
|
() => foldersManager.getTreeSnapshot(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const dataSnapshot = useSyncExternalStore(
|
||||||
|
useCallback(cb => foldersManager.subscribe(cb), [foldersManager]),
|
||||||
|
() => foldersManager.getSnapshot(),
|
||||||
|
() => foldersManager.getSnapshot(),
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setRemoteFolderTree(treeSnapshot);
|
if (!treeSnapshot || treeSnapshot.length === 0) {
|
||||||
}, [treeSnapshot]);
|
setRemoteFolderTree([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const map = dataSnapshot;
|
||||||
|
|
||||||
|
const mergeNode = (node: FolderTreeNode): FolderTreeNode => {
|
||||||
|
const liveData = map.get(node.id);
|
||||||
|
const name = liveData?.name ?? node.name;
|
||||||
|
const children = node.children ? node.children.map(mergeNode) : [];
|
||||||
|
return { ...node, name, children };
|
||||||
|
};
|
||||||
|
|
||||||
|
const mergedTree = treeSnapshot.map(mergeNode);
|
||||||
|
setRemoteFolderTree(mergedTree);
|
||||||
|
}, [treeSnapshot, dataSnapshot]);
|
||||||
|
|
||||||
const requestFolderTree = useCallback(() => {
|
const requestFolderTree = useCallback(() => {
|
||||||
foldersManager.ensureTree();
|
foldersManager.ensureTree();
|
||||||
|
|||||||
@@ -1,19 +1,9 @@
|
|||||||
import type { Dispatch, MutableRefObject, SetStateAction } from 'react';
|
import type { Dispatch, MutableRefObject, SetStateAction } from 'react';
|
||||||
import type { DocumentId, FolderId as FolderIdentifier } from '../../types/identifiers';
|
import type { DocumentId, FolderId as FolderIdentifier } from '../../types/identifiers';
|
||||||
import type { Document } from '../../types/documents';
|
import type { Document, FolderNode } from '../../types/documents';
|
||||||
|
|
||||||
type FolderId = FolderIdentifier | 'root';
|
type FolderId = FolderIdentifier | 'root';
|
||||||
|
|
||||||
export interface FolderNode {
|
|
||||||
id: FolderId;
|
|
||||||
parentId?: FolderId;
|
|
||||||
children: FolderId[];
|
|
||||||
hasChildren?: boolean;
|
|
||||||
name?: string;
|
|
||||||
expanded?: boolean;
|
|
||||||
loaded?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Tag {
|
export interface Tag {
|
||||||
id: DocumentId;
|
id: DocumentId;
|
||||||
label: string;
|
label: string;
|
||||||
@@ -55,7 +45,6 @@ export interface DocumentsState {
|
|||||||
|
|
||||||
export interface FolderState {
|
export interface FolderState {
|
||||||
folderNodes: Map<FolderId, FolderNode>;
|
folderNodes: Map<FolderId, FolderNode>;
|
||||||
setFolderNodes: Dispatch<SetStateAction<Map<FolderId, FolderNode>>>;
|
|
||||||
selectedFolder: FolderId;
|
selectedFolder: FolderId;
|
||||||
setSelectedFolder: Dispatch<SetStateAction<FolderId>>;
|
setSelectedFolder: Dispatch<SetStateAction<FolderId>>;
|
||||||
folderLabelMap: Map<FolderId, string>;
|
folderLabelMap: Map<FolderId, string>;
|
||||||
|
|||||||
@@ -105,10 +105,13 @@ const SidebarFolderList: React.FC<SidebarFolderListProps> = ({
|
|||||||
(nodes: FolderTreeNode[], depth: number): ReactNode =>
|
(nodes: FolderTreeNode[], depth: number): ReactNode =>
|
||||||
nodes.map((node) => {
|
nodes.map((node) => {
|
||||||
const isExpanded = expandedIds.has(String(node.id));
|
const isExpanded = expandedIds.has(String(node.id));
|
||||||
|
const liveNode = folderMap.get(String(node.id));
|
||||||
|
const displayNode = liveNode ? { ...node, name: liveNode.name ?? node.name } : node;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FolderNode
|
<FolderNode
|
||||||
key={node.id}
|
key={node.id}
|
||||||
node={node}
|
node={displayNode}
|
||||||
depth={depth}
|
depth={depth}
|
||||||
isSelected={selectedFolder === node.id}
|
isSelected={selectedFolder === node.id}
|
||||||
onToggle={handleToggle}
|
onToggle={handleToggle}
|
||||||
@@ -129,6 +132,7 @@ const SidebarFolderList: React.FC<SidebarFolderListProps> = ({
|
|||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
expandedIds,
|
expandedIds,
|
||||||
|
folderMap,
|
||||||
selectedFolder,
|
selectedFolder,
|
||||||
handleToggle,
|
handleToggle,
|
||||||
onSelect,
|
onSelect,
|
||||||
|
|||||||
Reference in New Issue
Block a user