feat: Refactor folder tree management to use FoldersManager and remove onToggle functionality
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import React, { useCallback, useEffect } from 'react';
|
import React, { useCallback, useEffect } from 'react';
|
||||||
import type { ReactNode, ComponentProps } from 'react';
|
import type { ReactNode, ComponentProps } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
DocumentsFilterProvider,
|
DocumentsFilterProvider,
|
||||||
} from '../documents/context/DocumentsFilterContext';
|
} from '../documents/context/DocumentsFilterContext';
|
||||||
@@ -11,14 +11,12 @@ import { SidebarProvider, useSidebarContext } from '../sidebar/SidebarContext';
|
|||||||
import { PanelManagerProvider, usePanelManager } from './PanelManagerContext';
|
import { PanelManagerProvider, usePanelManager } from './PanelManagerContext';
|
||||||
import Sidebar from '../sidebar/Sidebar';
|
import Sidebar from '../sidebar/Sidebar';
|
||||||
import useDocumentsShell from './useDocumentsShell';
|
import useDocumentsShell from './useDocumentsShell';
|
||||||
import useFolderData from '../documents/data/useFolderData';
|
|
||||||
|
|
||||||
const DocumentsInner: React.FC<{
|
const DocumentsInner: React.FC<{
|
||||||
surfaceConfig: any;
|
surfaceConfig: any;
|
||||||
folderData: any;
|
|
||||||
onNavigate: (documentId: string) => void;
|
onNavigate: (documentId: string) => void;
|
||||||
sidebarProps: ComponentProps<typeof Sidebar> | null;
|
sidebarProps: ComponentProps<typeof Sidebar> | null;
|
||||||
}> = ({ surfaceConfig, folderData, onNavigate, sidebarProps }) => {
|
}> = ({ surfaceConfig, onNavigate, sidebarProps }) => {
|
||||||
const { openPreview } = usePreviewContext();
|
const { openPreview } = usePreviewContext();
|
||||||
const { collapsed: sidebarCollapsed } = useSidebarContext();
|
const { collapsed: sidebarCollapsed } = useSidebarContext();
|
||||||
const {
|
const {
|
||||||
@@ -39,10 +37,6 @@ const DocumentsInner: React.FC<{
|
|||||||
? {
|
? {
|
||||||
...surfaceConfig.documentsTableProps,
|
...surfaceConfig.documentsTableProps,
|
||||||
onBreadcrumbNavigate: surfaceConfig.handleBreadcrumbNavigate,
|
onBreadcrumbNavigate: surfaceConfig.handleBreadcrumbNavigate,
|
||||||
documents: folderData.documents,
|
|
||||||
subfolders: folderData.subfolders,
|
|
||||||
currentFolderName: folderData.folder?.name,
|
|
||||||
isSearchLoading: folderData.loading,
|
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
@@ -94,13 +88,8 @@ const DocumentsRouteContent: React.FC = () => {
|
|||||||
sidebarProps,
|
sidebarProps,
|
||||||
surfaceConfig,
|
surfaceConfig,
|
||||||
documentsFilter,
|
documentsFilter,
|
||||||
documentsManager,
|
|
||||||
foldersManager,
|
|
||||||
} = useDocumentsShell();
|
} = useDocumentsShell();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const params = useParams();
|
|
||||||
const folderId = params.folderId || 'root';
|
|
||||||
const folderData = useFolderData(folderId, documentsManager, foldersManager);
|
|
||||||
|
|
||||||
const handleDocumentNavigate = useCallback((documentId: string) => {
|
const handleDocumentNavigate = useCallback((documentId: string) => {
|
||||||
navigate(`/documents/${documentId}`);
|
navigate(`/documents/${documentId}`);
|
||||||
@@ -113,7 +102,6 @@ const DocumentsRouteContent: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<DocumentsInner
|
<DocumentsInner
|
||||||
surfaceConfig={surfaceConfig}
|
surfaceConfig={surfaceConfig}
|
||||||
folderData={folderData}
|
|
||||||
onNavigate={handleDocumentNavigate}
|
onNavigate={handleDocumentNavigate}
|
||||||
sidebarProps={sidebarProps}
|
sidebarProps={sidebarProps}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { shallowEqual } from 'react-redux';
|
import { shallowEqual } from 'react-redux';
|
||||||
import type { FolderNodeId } from '../types/identifiers';
|
import type { FolderNodeId } from '../types/identifiers';
|
||||||
import type { Folder } from '../types/documents';
|
import type { Folder } from '../types/documents';
|
||||||
|
import { createRootNode } from '../app/workspaceUtils';
|
||||||
|
|
||||||
import type { FolderTreeNode } from '../lib/api/apiTypes';
|
import type { FolderTreeNode } from '../lib/api/apiTypes';
|
||||||
import { getFolderTree } from '../lib/api/apiClient';
|
import { getFolderTree } from '../lib/api/apiClient';
|
||||||
@@ -52,6 +53,14 @@ class FoldersManager<T extends ManagedFolder = ManagedFolder> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ingest(rawFolders: unknown[] = []): { canonical: T[]; changed: boolean } {
|
ingest(rawFolders: unknown[] = []): { canonical: T[]; changed: boolean } {
|
||||||
|
const result = this.ingestInternal(rawFolders);
|
||||||
|
if (result.changed) {
|
||||||
|
this.emit();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ingestInternal(rawFolders: unknown[] = []): { canonical: T[]; changed: boolean } {
|
||||||
const folders = rawFolders.map((f) => f as T).filter(Boolean);
|
const folders = rawFolders.map((f) => f as T).filter(Boolean);
|
||||||
let changed = false;
|
let changed = false;
|
||||||
let nextById = this.byId;
|
let nextById = this.byId;
|
||||||
@@ -81,7 +90,6 @@ class FoldersManager<T extends ManagedFolder = ManagedFolder> {
|
|||||||
|
|
||||||
if (changed) {
|
if (changed) {
|
||||||
this.byId = nextById;
|
this.byId = nextById;
|
||||||
this.emit();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { canonical, changed };
|
return { canonical, changed };
|
||||||
@@ -199,10 +207,16 @@ class FoldersManager<T extends ManagedFolder = ManagedFolder> {
|
|||||||
const raw = await getFolderTree();
|
const raw = await getFolderTree();
|
||||||
const flattened = flattenFolderTree(raw);
|
const flattened = flattenFolderTree(raw);
|
||||||
this.ingest(flattened);
|
this.ingest(flattened);
|
||||||
const roots = raw as FolderTreeNode[];
|
const rootsPromises = raw as FolderTreeNode[];
|
||||||
this.treeSnapshot = roots;
|
const rootNode = createRootNode() as FolderTreeNode;
|
||||||
|
|
||||||
|
rootNode.children = rootsPromises;
|
||||||
|
rootNode.hasChildren = rootsPromises.length > 0;
|
||||||
|
rootNode.loaded = true;
|
||||||
|
|
||||||
|
this.treeSnapshot = [rootNode];
|
||||||
this.emit();
|
this.emit();
|
||||||
return roots;
|
return [rootNode];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to fetch folder tree', error);
|
console.warn('Failed to fetch folder tree', error);
|
||||||
return [];
|
return [];
|
||||||
@@ -215,5 +229,4 @@ class FoldersManager<T extends ManagedFolder = ManagedFolder> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export default FoldersManager;
|
export default FoldersManager;
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ type UpdateDocumentCaches = (
|
|||||||
|
|
||||||
type EnsureFolderData = (
|
type EnsureFolderData = (
|
||||||
folderId: FolderId,
|
folderId: FolderId,
|
||||||
options?: { force?: boolean; includeDocuments?: boolean; prefetchDepth?: number },
|
options?: { includeDocuments?: boolean },
|
||||||
) => Promise<FolderContents>;
|
) => Promise<FolderContents>;
|
||||||
|
|
||||||
type ApplySelectedFolder = (folderId: FolderId, contents?: FolderContents | null) => void;
|
type ApplySelectedFolder = (folderId: FolderId, contents?: FolderContents | null) => void;
|
||||||
@@ -336,7 +336,7 @@ const useDocumentMutations = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (targetFolderId && targetFolderId !== selectedFolder) {
|
if (targetFolderId && targetFolderId !== selectedFolder) {
|
||||||
await ensureFolderData(targetFolderId as FolderId, { force: true, prefetchDepth: 1 });
|
await ensureFolderData(targetFolderId as FolderId);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = (error as Record<string, any>)?.response?.data?.error || 'Failed to move documents.';
|
const message = (error as Record<string, any>)?.response?.data?.error || 'Failed to move documents.';
|
||||||
@@ -668,10 +668,7 @@ const useDocumentMutations = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const contents = await ensureFolderData(folderId, {
|
const contents = await ensureFolderData(folderId);
|
||||||
force: true,
|
|
||||||
prefetchDepth: 1,
|
|
||||||
});
|
|
||||||
const hasChildren = (contents.subfolders || []).length > 0;
|
const hasChildren = (contents.subfolders || []).length > 0;
|
||||||
const hasDocs = (contents.documents || []).length > 0;
|
const hasDocs = (contents.documents || []).length > 0;
|
||||||
if (hasChildren || hasDocs) {
|
if (hasChildren || hasDocs) {
|
||||||
@@ -712,13 +709,10 @@ const useDocumentMutations = ({
|
|||||||
const node = folderNodes.get(folderId);
|
const node = folderNodes.get(folderId);
|
||||||
const parentId = node?.parentId || 'root';
|
const parentId = node?.parentId || 'root';
|
||||||
setSelectedFolder(parentId);
|
setSelectedFolder(parentId);
|
||||||
const parentContents = await ensureFolderData(parentId, {
|
const parentContents = await ensureFolderData(parentId);
|
||||||
force: true,
|
|
||||||
prefetchDepth: 1,
|
|
||||||
});
|
|
||||||
applySelectedFolder(parentId, parentContents);
|
applySelectedFolder(parentId, parentContents);
|
||||||
} else if (selectedFolder !== 'root') {
|
} else if (selectedFolder !== 'root') {
|
||||||
await ensureFolderData(selectedFolder, { force: true, prefetchDepth: 1 });
|
await ensureFolderData(selectedFolder);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showMessage) {
|
if (showMessage) {
|
||||||
|
|||||||
@@ -348,18 +348,16 @@ const useDocumentsWorkspace = ({
|
|||||||
folderLabelMap,
|
folderLabelMap,
|
||||||
applySelectedFolder,
|
applySelectedFolder,
|
||||||
ensureFolderData,
|
ensureFolderData,
|
||||||
ensureFolderAncestorsLoaded,
|
|
||||||
expandFolderAncestors,
|
|
||||||
isInvalidFolderDrop,
|
isInvalidFolderDrop,
|
||||||
} = useFolderTree({
|
} = useFolderTree({
|
||||||
initialSelectedFolder: routeFolderId || 'root',
|
initialSelectedFolder: routeFolderId || 'root',
|
||||||
tenantIdRef,
|
|
||||||
documentsSortFieldRef: activeSortFieldRef,
|
documentsSortFieldRef: activeSortFieldRef,
|
||||||
documentsSortDirectionRef: activeSortDirectionRef,
|
documentsSortDirectionRef: activeSortDirectionRef,
|
||||||
selectionHelpers,
|
selectionHelpers,
|
||||||
setDocuments,
|
setDocuments,
|
||||||
setFolderContents,
|
setFolderContents,
|
||||||
folderContentsRef,
|
folderContentsRef,
|
||||||
|
foldersManager,
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -579,10 +577,7 @@ const useDocumentsWorkspace = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const refreshCurrentFolder = useCallback(async () => {
|
const refreshCurrentFolder = useCallback(async () => {
|
||||||
const contents = await ensureFolderData(selectedFolder, {
|
const contents = await ensureFolderData(selectedFolder);
|
||||||
force: true,
|
|
||||||
prefetchDepth: 1,
|
|
||||||
});
|
|
||||||
applySelectedFolder(selectedFolder, contents);
|
applySelectedFolder(selectedFolder, contents);
|
||||||
}, [selectedFolder, ensureFolderData, applySelectedFolder]);
|
}, [selectedFolder, ensureFolderData, applySelectedFolder]);
|
||||||
|
|
||||||
@@ -828,8 +823,6 @@ const useDocumentsWorkspace = ({
|
|||||||
selectedFolder,
|
selectedFolder,
|
||||||
setSelectedFolder,
|
setSelectedFolder,
|
||||||
ensureFolderData,
|
ensureFolderData,
|
||||||
ensureFolderAncestorsLoaded,
|
|
||||||
expandFolderAncestors,
|
|
||||||
applySelectedFolder,
|
applySelectedFolder,
|
||||||
notifyApiError,
|
notifyApiError,
|
||||||
setStatusMessage,
|
setStatusMessage,
|
||||||
@@ -867,16 +860,20 @@ const useDocumentsWorkspace = ({
|
|||||||
focusedEntryKey,
|
focusedEntryKey,
|
||||||
});
|
});
|
||||||
const initializeAfterLogin = useCallback(async () => {
|
const initializeAfterLogin = useCallback(async () => {
|
||||||
await Promise.all([refreshTags(), refreshCorrespondents()]);
|
await Promise.all([
|
||||||
|
refreshTags(),
|
||||||
|
refreshCorrespondents(),
|
||||||
|
foldersManager.ensureTree(),
|
||||||
|
]);
|
||||||
const initialFolder = routeFolderId && routeFolderId !== 'root' ? routeFolderId : 'root';
|
const initialFolder = routeFolderId && routeFolderId !== 'root' ? routeFolderId : 'root';
|
||||||
await loadFolder(initialFolder, {});
|
await loadFolder(initialFolder, {});
|
||||||
}, [refreshTags, refreshCorrespondents, routeFolderId, loadFolder]);
|
}, [refreshTags, refreshCorrespondents, routeFolderId, loadFolder, foldersManager]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (appStatus !== 'ready' && appStatus !== 'bootstrapping') {
|
if (appStatus !== 'ready') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -901,7 +898,12 @@ const useDocumentsWorkspace = ({
|
|||||||
selectFolder,
|
selectFolder,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const mountedRef = useRef(true);
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
mountedRef.current = false;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (appStatus !== 'authenticated') {
|
if (appStatus !== 'authenticated') {
|
||||||
@@ -911,17 +913,16 @@ const useDocumentsWorkspace = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let cancelled = false;
|
|
||||||
const bootstrap = async () => {
|
const bootstrap = async () => {
|
||||||
bootstrapInitializedRef.current = true;
|
bootstrapInitializedRef.current = true;
|
||||||
appDispatch({ type: 'BOOTSTRAP_START' });
|
appDispatch({ type: 'BOOTSTRAP_START' });
|
||||||
try {
|
try {
|
||||||
await initializeAfterLogin();
|
await initializeAfterLogin();
|
||||||
if (!cancelled) {
|
if (mountedRef.current) {
|
||||||
appDispatch({ type: 'BOOTSTRAP_SUCCESS' });
|
appDispatch({ type: 'BOOTSTRAP_SUCCESS' });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!cancelled) {
|
if (mountedRef.current) {
|
||||||
appDispatch({
|
appDispatch({
|
||||||
type: 'BOOTSTRAP_FAILURE',
|
type: 'BOOTSTRAP_FAILURE',
|
||||||
error: error?.message || 'Failed to initialize data.',
|
error: error?.message || 'Failed to initialize data.',
|
||||||
@@ -932,10 +933,6 @@ const useDocumentsWorkspace = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
bootstrap();
|
bootstrap();
|
||||||
|
|
||||||
return () => {
|
|
||||||
cancelled = true;
|
|
||||||
};
|
|
||||||
}, [appStatus, appDispatch, initializeAfterLogin]);
|
}, [appStatus, appDispatch, initializeAfterLogin]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -1,127 +0,0 @@
|
|||||||
import { useState, useEffect, useCallback, useMemo, useSyncExternalStore } from 'react';
|
|
||||||
import { listFolderContents } from '../../lib/api/apiClient';
|
|
||||||
import type { Document, Folder, FolderContents } from '../../types/documents';
|
|
||||||
import type { Identifier, DocumentId, FolderNodeId } from '../../types/identifiers';
|
|
||||||
import type DocumentsManager from '../DocumentsManager';
|
|
||||||
import type FoldersManager from '../FoldersManager';
|
|
||||||
|
|
||||||
interface UseFolderDataResult {
|
|
||||||
folder: Folder | null;
|
|
||||||
documents: Document[];
|
|
||||||
subfolders: Folder[];
|
|
||||||
loading: boolean;
|
|
||||||
error: Error | null;
|
|
||||||
refresh: () => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const useFolderData = (
|
|
||||||
folderId: Identifier | 'root' = 'root',
|
|
||||||
documentsManager?: DocumentsManager<Document>,
|
|
||||||
foldersManager?: FoldersManager<Folder>
|
|
||||||
): UseFolderDataResult => {
|
|
||||||
const [data, setData] = useState<FolderContents>({
|
|
||||||
folder: null,
|
|
||||||
documents: [],
|
|
||||||
subfolders: [],
|
|
||||||
});
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [error, setError] = useState<Error | null>(null);
|
|
||||||
|
|
||||||
const fetchData = useCallback(async () => {
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
try {
|
|
||||||
const path = folderId === 'root' ? 'root' : folderId;
|
|
||||||
const result = await listFolderContents<FolderContents>(path, {
|
|
||||||
include_documents: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (documentsManager && result.documents) {
|
|
||||||
documentsManager.ingest(result.documents);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (foldersManager) {
|
|
||||||
const foldersToIngest: Folder[] = [];
|
|
||||||
if (result.folder) foldersToIngest.push(result.folder);
|
|
||||||
if (result.subfolders) foldersToIngest.push(...result.subfolders);
|
|
||||||
if (foldersToIngest.length > 0) {
|
|
||||||
foldersManager.ingest(foldersToIngest);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setData({
|
|
||||||
folder: result.folder || null,
|
|
||||||
documents: result.documents || [],
|
|
||||||
subfolders: result.subfolders || [],
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to fetch folder data:', err);
|
|
||||||
setError(err instanceof Error ? err : new Error('Unknown error'));
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, [folderId, documentsManager, foldersManager]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchData();
|
|
||||||
}, [fetchData]);
|
|
||||||
|
|
||||||
const subscribeToDocs = useCallback((onStoreChange: () => void) => {
|
|
||||||
if (!documentsManager) return () => { };
|
|
||||||
return documentsManager.subscribe(onStoreChange);
|
|
||||||
}, [documentsManager]);
|
|
||||||
|
|
||||||
const getDocSnapshot = useCallback(() => {
|
|
||||||
if (!documentsManager) return null;
|
|
||||||
return documentsManager.getSnapshot();
|
|
||||||
}, [documentsManager]);
|
|
||||||
|
|
||||||
const docSnapshot = useSyncExternalStore(subscribeToDocs, getDocSnapshot, getDocSnapshot);
|
|
||||||
|
|
||||||
const reactiveDocuments = useMemo(() => {
|
|
||||||
if (!documentsManager || !docSnapshot) {
|
|
||||||
return data.documents;
|
|
||||||
}
|
|
||||||
const ids = data.documents.map(d => d.id as DocumentId).filter(Boolean);
|
|
||||||
return documentsManager.getMany(ids);
|
|
||||||
}, [data.documents, documentsManager, docSnapshot]);
|
|
||||||
|
|
||||||
const subscribeToFolders = useCallback((onStoreChange: () => void) => {
|
|
||||||
if (!foldersManager) return () => { };
|
|
||||||
return foldersManager.subscribe(onStoreChange);
|
|
||||||
}, [foldersManager]);
|
|
||||||
|
|
||||||
const getFolderSnapshot = useCallback(() => {
|
|
||||||
if (!foldersManager) return null;
|
|
||||||
return foldersManager.getSnapshot();
|
|
||||||
}, [foldersManager]);
|
|
||||||
|
|
||||||
const folderSnapshot = useSyncExternalStore(subscribeToFolders, getFolderSnapshot, getFolderSnapshot);
|
|
||||||
|
|
||||||
const reactiveFolder = useMemo(() => {
|
|
||||||
if (!foldersManager || !folderSnapshot || !data.folder) {
|
|
||||||
return data.folder;
|
|
||||||
}
|
|
||||||
return foldersManager.getById(data.folder.id as FolderNodeId) || data.folder;
|
|
||||||
}, [data.folder, foldersManager, folderSnapshot]);
|
|
||||||
|
|
||||||
const reactiveSubfolders = useMemo(() => {
|
|
||||||
if (!foldersManager || !folderSnapshot) {
|
|
||||||
return data.subfolders;
|
|
||||||
}
|
|
||||||
const ids = data.subfolders.map(f => f.id as FolderNodeId).filter(Boolean);
|
|
||||||
return foldersManager.getMany(ids);
|
|
||||||
}, [data.subfolders, foldersManager, folderSnapshot]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...data,
|
|
||||||
folder: reactiveFolder,
|
|
||||||
documents: reactiveDocuments,
|
|
||||||
subfolders: reactiveSubfolders,
|
|
||||||
loading,
|
|
||||||
error,
|
|
||||||
refresh: fetchData,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useFolderData;
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { getFolderTree, listFolderContents } from '../../../lib/api/apiClient';
|
|
||||||
import type { Dispatch, MutableRefObject, SetStateAction } from 'react';
|
import type { Dispatch, MutableRefObject, SetStateAction } from 'react';
|
||||||
import { createRootNode, DEFAULT_FOLDER_NAME, flattenFolderTree } from '../../../app/workspaceUtils';
|
import { createRootNode, DEFAULT_FOLDER_NAME, flattenFolderTree } from '../../../app/workspaceUtils';
|
||||||
|
import { listFolderContents } from '../../../lib/api/apiClient';
|
||||||
import {
|
import {
|
||||||
getEntryId,
|
getEntryId,
|
||||||
isDocumentEntry,
|
isDocumentEntry,
|
||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
} from '../../../app/entryKey';
|
} from '../../../app/entryKey';
|
||||||
import type { FolderNodeId, Identifier, FolderId } from '../../../types/identifiers';
|
import type { FolderNodeId, Identifier, FolderId } from '../../../types/identifiers';
|
||||||
import type { Document, Folder, FolderContents } from '../../../types/documents';
|
import type { Document, Folder, FolderContents } from '../../../types/documents';
|
||||||
|
import type FoldersManager from '../../FoldersManager';
|
||||||
|
|
||||||
interface FolderContentsEntry extends FolderContents {
|
interface FolderContentsEntry extends FolderContents {
|
||||||
__includesDocuments?: boolean;
|
__includesDocuments?: boolean;
|
||||||
@@ -39,13 +40,13 @@ interface SelectionHelpers {
|
|||||||
|
|
||||||
interface UseFolderTreeOptions {
|
interface UseFolderTreeOptions {
|
||||||
initialSelectedFolder?: FolderNodeId;
|
initialSelectedFolder?: FolderNodeId;
|
||||||
tenantIdRef: MutableRefObject<Identifier | null>;
|
|
||||||
documentsSortFieldRef: MutableRefObject<string>;
|
documentsSortFieldRef: MutableRefObject<string>;
|
||||||
documentsSortDirectionRef: MutableRefObject<string>;
|
documentsSortDirectionRef: MutableRefObject<string>;
|
||||||
selectionHelpers: SelectionHelpers;
|
selectionHelpers: SelectionHelpers;
|
||||||
setDocuments: Dispatch<SetStateAction<Document[]>>;
|
setDocuments: Dispatch<SetStateAction<Document[]>>;
|
||||||
setFolderContents: Dispatch<SetStateAction<Map<FolderNodeId, FolderContentsEntry>>>;
|
setFolderContents: Dispatch<SetStateAction<Map<FolderNodeId, FolderContentsEntry>>>;
|
||||||
folderContentsRef: MutableRefObject<Map<FolderNodeId, FolderContentsEntry>>;
|
folderContentsRef: MutableRefObject<Map<FolderNodeId, FolderContentsEntry>>;
|
||||||
|
foldersManager?: FoldersManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FolderOption {
|
interface FolderOption {
|
||||||
@@ -55,13 +56,13 @@ interface FolderOption {
|
|||||||
|
|
||||||
const useFolderTree = ({
|
const useFolderTree = ({
|
||||||
initialSelectedFolder = 'root',
|
initialSelectedFolder = 'root',
|
||||||
tenantIdRef,
|
|
||||||
documentsSortFieldRef,
|
documentsSortFieldRef,
|
||||||
documentsSortDirectionRef,
|
documentsSortDirectionRef,
|
||||||
selectionHelpers,
|
selectionHelpers,
|
||||||
setDocuments,
|
setDocuments,
|
||||||
setFolderContents,
|
setFolderContents,
|
||||||
folderContentsRef,
|
folderContentsRef,
|
||||||
|
foldersManager,
|
||||||
}: UseFolderTreeOptions) => {
|
}: UseFolderTreeOptions) => {
|
||||||
const [folderNodes, setFolderNodes] = useState<Map<FolderNodeId, FolderTreeNode>>(() => {
|
const [folderNodes, setFolderNodes] = useState<Map<FolderNodeId, FolderTreeNode>>(() => {
|
||||||
const rootNode = createRootNode() as FolderTreeNode;
|
const rootNode = createRootNode() as FolderTreeNode;
|
||||||
@@ -71,12 +72,16 @@ const useFolderTree = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchTree = async () => {
|
const fetchTree = async () => {
|
||||||
try {
|
try {
|
||||||
const data = await getFolderTree();
|
if (!foldersManager) return;
|
||||||
|
|
||||||
|
// Use FoldersManager as the single source of truth for fetching the tree
|
||||||
|
const roots = await foldersManager.ensureTree();
|
||||||
|
|
||||||
setFolderNodes((prev) => {
|
setFolderNodes((prev) => {
|
||||||
const next = new Map(prev);
|
const next = new Map(prev);
|
||||||
const rootChildren: FolderNodeId[] = [];
|
const rootChildren: FolderNodeId[] = [];
|
||||||
|
|
||||||
const flatData = flattenFolderTree(data);
|
const flatData = flattenFolderTree(roots);
|
||||||
|
|
||||||
flatData.forEach((item) => {
|
flatData.forEach((item) => {
|
||||||
const id = item.id as FolderNodeId;
|
const id = item.id as FolderNodeId;
|
||||||
@@ -84,12 +89,15 @@ const useFolderTree = ({
|
|||||||
// item.children is now FolderTreeNode[], so map to IDs
|
// item.children is now FolderTreeNode[], so map to IDs
|
||||||
const children = (item.children || []).map((c) => c.id as FolderNodeId);
|
const children = (item.children || []).map((c) => c.id as FolderNodeId);
|
||||||
|
|
||||||
|
// Preserve existing UI state (expanded, loaded)
|
||||||
|
const existing = prev.get(id);
|
||||||
|
|
||||||
next.set(id, {
|
next.set(id, {
|
||||||
id,
|
id,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
parentId,
|
parentId,
|
||||||
children,
|
children,
|
||||||
expanded: false,
|
expanded: existing?.expanded ?? false,
|
||||||
loaded: true,
|
loaded: true,
|
||||||
hasChildren: children.length > 0,
|
hasChildren: children.length > 0,
|
||||||
});
|
});
|
||||||
@@ -115,7 +123,7 @@ const useFolderTree = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
fetchTree();
|
fetchTree();
|
||||||
}, []);
|
}, [foldersManager]);
|
||||||
|
|
||||||
const [selectedFolder, setSelectedFolder] = useState<FolderNodeId>(initialSelectedFolder || 'root');
|
const [selectedFolder, setSelectedFolder] = useState<FolderNodeId>(initialSelectedFolder || 'root');
|
||||||
const [currentFolder, setCurrentFolder] = useState<Folder | null>(null);
|
const [currentFolder, setCurrentFolder] = useState<Folder | null>(null);
|
||||||
@@ -199,72 +207,32 @@ const useFolderTree = ({
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const expandFolderAncestors = useCallback((targetId: FolderNodeId | null) => {
|
|
||||||
if (!targetId || targetId === 'root') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setFolderNodes((prev: Map<FolderNodeId, FolderTreeNode>) => {
|
|
||||||
const next = new Map<FolderNodeId, FolderTreeNode>(prev);
|
|
||||||
const node = next.get(targetId);
|
|
||||||
let currentId = node?.parentId ?? 'root';
|
|
||||||
let guard = 0;
|
|
||||||
|
|
||||||
while (currentId && guard < 32) {
|
|
||||||
guard += 1;
|
|
||||||
const currentNode = next.get(currentId);
|
|
||||||
if (!currentNode) break;
|
|
||||||
if (!currentNode.expanded) {
|
|
||||||
next.set(currentId, { ...currentNode, expanded: true });
|
|
||||||
}
|
|
||||||
currentId = currentNode.parentId ?? 'root';
|
|
||||||
}
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const ensureFolderData = useCallback(
|
const ensureFolderData = useCallback(
|
||||||
async (
|
async (
|
||||||
folderId: FolderNodeId,
|
folderId: FolderNodeId,
|
||||||
{
|
{
|
||||||
includeDocuments = true,
|
includeDocuments = true,
|
||||||
prefetchDepth = 0,
|
|
||||||
force = false,
|
|
||||||
sortField = documentsSortFieldRef.current,
|
sortField = documentsSortFieldRef.current,
|
||||||
sortDirection = documentsSortDirectionRef.current,
|
sortDirection = documentsSortDirectionRef.current,
|
||||||
}: {
|
}: {
|
||||||
includeDocuments?: boolean;
|
includeDocuments?: boolean;
|
||||||
prefetchDepth?: number;
|
|
||||||
force?: boolean;
|
|
||||||
sortField?: string;
|
sortField?: string;
|
||||||
sortDirection?: string;
|
sortDirection?: string;
|
||||||
} = {},
|
} = {},
|
||||||
): Promise<FolderContentsEntry> => {
|
): Promise<FolderContentsEntry> => {
|
||||||
const requestTenantId = tenantIdRef.current;
|
|
||||||
const cached = folderContentsRef.current.get(folderId);
|
const cached = folderContentsRef.current.get(folderId);
|
||||||
const cachedSortField = cached?.__sortField || documentsSortFieldRef.current;
|
const cachedSortField = cached?.__sortField || documentsSortFieldRef.current;
|
||||||
const cachedSortDirection = cached?.__sortDirection || documentsSortDirectionRef.current;
|
const cachedSortDirection = cached?.__sortDirection || documentsSortDirectionRef.current;
|
||||||
const cachedSortMatches = cachedSortField === sortField && cachedSortDirection === sortDirection;
|
const cachedSortMatches = cachedSortField === sortField && cachedSortDirection === sortDirection;
|
||||||
|
|
||||||
if (!force && cached) {
|
if (cached) {
|
||||||
const includesDocuments = Boolean(cached.__includesDocuments);
|
const includesDocuments = Boolean(cached.__includesDocuments);
|
||||||
if (!includeDocuments || (includesDocuments && cachedSortMatches)) {
|
if (!includeDocuments || (includesDocuments && cachedSortMatches)) {
|
||||||
if (prefetchDepth > 0) {
|
|
||||||
const subfolders = Array.isArray(cached.subfolders) ? cached.subfolders : [];
|
|
||||||
await Promise.allSettled(
|
|
||||||
subfolders.map((entry) =>
|
|
||||||
ensureFolderData(entry.id, {
|
|
||||||
includeDocuments: false,
|
|
||||||
prefetchDepth: prefetchDepth - 1,
|
|
||||||
force: false,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We use listFolderContents purely for Documents now, assuming Tree is authoritative for subfolders.
|
||||||
const path = folderId === 'root' ? 'root' : folderId;
|
const path = folderId === 'root' ? 'root' : folderId;
|
||||||
const params: Record<string, unknown> = {};
|
const params: Record<string, unknown> = {};
|
||||||
if (!includeDocuments) {
|
if (!includeDocuments) {
|
||||||
@@ -273,113 +241,30 @@ const useFolderTree = ({
|
|||||||
params.sort = sortField;
|
params.sort = sortField;
|
||||||
params.dir = sortDirection;
|
params.dir = sortDirection;
|
||||||
}
|
}
|
||||||
const data = await listFolderContents<FolderContentsEntry>(path, params);
|
|
||||||
const childFolders = Array.isArray(data.subfolders) ? data.subfolders : [];
|
const data = await listFolderContents(path, params);
|
||||||
const childIds = childFolders
|
|
||||||
.map((child) => (child?.id ?? null) as FolderNodeId | null)
|
|
||||||
.filter((id): id is FolderNodeId => Boolean(id));
|
|
||||||
|
|
||||||
const enriched = {
|
const enriched = {
|
||||||
...data,
|
...data,
|
||||||
|
documents: (data.documents || []) as Document[],
|
||||||
__includesDocuments: includeDocuments,
|
__includesDocuments: includeDocuments,
|
||||||
__sortField: includeDocuments ? sortField : cachedSortField,
|
__sortField: includeDocuments ? sortField : cachedSortField,
|
||||||
__sortDirection: includeDocuments ? sortDirection : cachedSortDirection,
|
__sortDirection: includeDocuments ? sortDirection : cachedSortDirection,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (tenantIdRef.current !== requestTenantId) {
|
|
||||||
return enriched;
|
|
||||||
}
|
|
||||||
|
|
||||||
setFolderNodes((prev: Map<FolderNodeId, FolderTreeNode>) => {
|
|
||||||
const next = new Map<FolderNodeId, FolderTreeNode>(prev);
|
|
||||||
const existingNode = next.get(folderId) || {
|
|
||||||
id: folderId,
|
|
||||||
name: folderId === 'root' ? DEFAULT_FOLDER_NAME : data.folder?.name || 'Folder',
|
|
||||||
parentId: data.folder?.parent_id || 'root',
|
|
||||||
children: [],
|
|
||||||
expanded: folderId === 'root',
|
|
||||||
loaded: false,
|
|
||||||
hasChildren: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
next.set(folderId, {
|
|
||||||
...existingNode,
|
|
||||||
name: folderId === 'root' ? DEFAULT_FOLDER_NAME : data.folder?.name || existingNode.name,
|
|
||||||
parentId: data.folder?.parent_id ?? existingNode.parentId ?? 'root',
|
|
||||||
children: childIds,
|
|
||||||
expanded: folderId === 'root' ? true : existingNode.expanded,
|
|
||||||
loaded: true,
|
|
||||||
hasChildren: childIds.length > 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
childFolders.forEach((child) => {
|
|
||||||
const childId = (child?.id ?? null) as FolderNodeId | null;
|
|
||||||
if (!childId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const childNode = next.get(childId);
|
|
||||||
const previousChildren = Array.isArray(childNode?.children) ? childNode.children : [];
|
|
||||||
const childHasChildren = (() => {
|
|
||||||
if (childNode?.loaded) {
|
|
||||||
return previousChildren.length > 0;
|
|
||||||
}
|
|
||||||
if (Array.isArray(child?.subfolders)) {
|
|
||||||
return child.subfolders.length > 0;
|
|
||||||
}
|
|
||||||
const flag = [child?.has_children, child?.hasChildren, childNode?.hasChildren]
|
|
||||||
.find((value) => value != null);
|
|
||||||
return Boolean(flag);
|
|
||||||
})();
|
|
||||||
next.set(childId, {
|
|
||||||
id: childId,
|
|
||||||
name: child.name,
|
|
||||||
parentId: (child.parent_id ?? 'root') as FolderNodeId,
|
|
||||||
children: previousChildren,
|
|
||||||
expanded: childNode?.expanded ?? false,
|
|
||||||
loaded: childNode?.loaded ?? false,
|
|
||||||
hasChildren: childHasChildren,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (prefetchDepth > 0 && childIds.length > 0 && tenantIdRef.current === requestTenantId) {
|
|
||||||
await Promise.allSettled(
|
|
||||||
childIds.map((childId) =>
|
|
||||||
ensureFolderData(childId, {
|
|
||||||
includeDocuments: false,
|
|
||||||
force: false,
|
|
||||||
prefetchDepth: prefetchDepth - 1,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
setFolderContents((prev: Map<FolderNodeId, FolderContentsEntry>) => {
|
setFolderContents((prev: Map<FolderNodeId, FolderContentsEntry>) => {
|
||||||
if (tenantIdRef.current !== requestTenantId) {
|
|
||||||
return prev;
|
|
||||||
}
|
|
||||||
const next = new Map<FolderNodeId, FolderContentsEntry>(prev);
|
const next = new Map<FolderNodeId, FolderContentsEntry>(prev);
|
||||||
if (includeDocuments) {
|
if (includeDocuments) {
|
||||||
next.set(folderId, enriched);
|
next.set(folderId, enriched);
|
||||||
} else {
|
} else {
|
||||||
const existingEntry = next.get(folderId);
|
const existing = next.get(folderId);
|
||||||
if (existingEntry) {
|
next.set(folderId, {
|
||||||
next.set(folderId, {
|
...enriched,
|
||||||
...existingEntry,
|
documents: existing?.__includesDocuments ? existing.documents : (enriched.documents as Document[]),
|
||||||
...data,
|
__includesDocuments: existing?.__includesDocuments || false,
|
||||||
documents: existingEntry.__includesDocuments
|
});
|
||||||
? existingEntry.documents
|
|
||||||
: data.documents,
|
|
||||||
__includesDocuments: existingEntry.__includesDocuments || false,
|
|
||||||
__sortField: existingEntry.__sortField ?? enriched.__sortField,
|
|
||||||
__sortDirection: existingEntry.__sortDirection ?? enriched.__sortDirection,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
next.set(folderId, enriched);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
folderContentsRef.current = next;
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -388,33 +273,12 @@ const useFolderTree = ({
|
|||||||
[
|
[
|
||||||
documentsSortDirectionRef,
|
documentsSortDirectionRef,
|
||||||
documentsSortFieldRef,
|
documentsSortFieldRef,
|
||||||
tenantIdRef,
|
|
||||||
setFolderContents,
|
setFolderContents,
|
||||||
folderContentsRef,
|
folderContentsRef,
|
||||||
|
// Removed foldersManager dependency as we don't call it here anymore
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const ensureFolderAncestorsLoaded = useCallback(
|
|
||||||
async (targetId: FolderNodeId | null) => {
|
|
||||||
if (!targetId || targetId === 'root') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let current = targetId;
|
|
||||||
let guard = 0;
|
|
||||||
while (current && current !== 'root' && guard < 32) {
|
|
||||||
guard += 1;
|
|
||||||
const node = folderNodes.get(current);
|
|
||||||
if (node?.loaded) {
|
|
||||||
current = node.parentId ?? 'root';
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
await ensureFolderData(current, { includeDocuments: false, prefetchDepth: 0 });
|
|
||||||
current = folderNodes.get(current)?.parentId ?? 'root';
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[folderNodes, ensureFolderData],
|
|
||||||
);
|
|
||||||
|
|
||||||
const isInvalidFolderDrop = useCallback(
|
const isInvalidFolderDrop = useCallback(
|
||||||
(sourceId: FolderNodeId | null, targetId: FolderNodeId | null) => {
|
(sourceId: FolderNodeId | null, targetId: FolderNodeId | null) => {
|
||||||
if (!sourceId) return false;
|
if (!sourceId) return false;
|
||||||
@@ -492,6 +356,13 @@ const useFolderTree = ({
|
|||||||
return entries;
|
return entries;
|
||||||
}, [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) => {
|
||||||
@@ -514,8 +385,6 @@ const useFolderTree = ({
|
|||||||
folderLabelMap,
|
folderLabelMap,
|
||||||
applySelectedFolder,
|
applySelectedFolder,
|
||||||
ensureFolderData,
|
ensureFolderData,
|
||||||
ensureFolderAncestorsLoaded,
|
|
||||||
expandFolderAncestors,
|
|
||||||
isInvalidFolderDrop,
|
isInvalidFolderDrop,
|
||||||
resetFolderTreeState,
|
resetFolderTreeState,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -30,9 +30,7 @@ interface FolderContentsState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface EnsureFolderOptions {
|
interface EnsureFolderOptions {
|
||||||
force?: boolean;
|
|
||||||
includeDocuments?: boolean;
|
includeDocuments?: boolean;
|
||||||
prefetchDepth?: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LoadFolderOptions {
|
interface LoadFolderOptions {
|
||||||
@@ -45,7 +43,6 @@ interface SelectFolderOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface FolderClickHandlers {
|
interface FolderClickHandlers {
|
||||||
onToggle: (folderId: FolderKey) => Promise<void>;
|
|
||||||
onSelect: (folderId: FolderKey, options?: SelectFolderOptions) => Promise<void>;
|
onSelect: (folderId: FolderKey, options?: SelectFolderOptions) => Promise<void>;
|
||||||
onDrop: (event: DragEvent<HTMLElement>, folderId: FolderKey) => Promise<void>;
|
onDrop: (event: DragEvent<HTMLElement>, folderId: FolderKey) => Promise<void>;
|
||||||
onDragOver: (event: DragEvent<HTMLElement>, folderId: FolderKey) => void;
|
onDragOver: (event: DragEvent<HTMLElement>, folderId: FolderKey) => void;
|
||||||
@@ -59,8 +56,6 @@ interface UseFolderTreeActionsOptions {
|
|||||||
selectedFolder: FolderKey;
|
selectedFolder: FolderKey;
|
||||||
setSelectedFolder: (folderId: FolderKey) => void;
|
setSelectedFolder: (folderId: FolderKey) => void;
|
||||||
ensureFolderData: (folderId: FolderKey, options?: EnsureFolderOptions) => Promise<any>;
|
ensureFolderData: (folderId: FolderKey, options?: EnsureFolderOptions) => Promise<any>;
|
||||||
ensureFolderAncestorsLoaded: (folderId: FolderKey) => Promise<void>;
|
|
||||||
expandFolderAncestors: (folderId: FolderKey, options?: { includeSelf?: boolean }) => void;
|
|
||||||
applySelectedFolder: (folderId: FolderKey, contents: any) => void;
|
applySelectedFolder: (folderId: FolderKey, contents: any) => 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;
|
||||||
@@ -88,8 +83,6 @@ const useFolderTreeActions = ({
|
|||||||
selectedFolder,
|
selectedFolder,
|
||||||
setSelectedFolder,
|
setSelectedFolder,
|
||||||
ensureFolderData,
|
ensureFolderData,
|
||||||
ensureFolderAncestorsLoaded,
|
|
||||||
expandFolderAncestors,
|
|
||||||
applySelectedFolder,
|
applySelectedFolder,
|
||||||
notifyApiError,
|
notifyApiError,
|
||||||
setStatusMessage,
|
setStatusMessage,
|
||||||
@@ -175,12 +168,12 @@ const useFolderTreeActions = ({
|
|||||||
const refreshTargets = new Set([previousParentKey, targetKey]);
|
const refreshTargets = new Set([previousParentKey, targetKey]);
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
Array.from(refreshTargets).map((key) =>
|
Array.from(refreshTargets).map((key) =>
|
||||||
ensureFolderData(key === 'root' ? 'root' : key, { force: true, prefetchDepth: 1 }),
|
ensureFolderData(key),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (selectedFolder === folderId) {
|
if (selectedFolder === folderId) {
|
||||||
await ensureFolderData(folderId, { force: true, prefetchDepth: 1 });
|
await ensureFolderData(folderId);
|
||||||
setSelectedFolder(folderId);
|
setSelectedFolder(folderId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,7 +185,7 @@ const useFolderTreeActions = ({
|
|||||||
const refreshTargets = new Set([previousParentKey, targetKey]);
|
const refreshTargets = new Set([previousParentKey, targetKey]);
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
Array.from(refreshTargets).map((key) =>
|
Array.from(refreshTargets).map((key) =>
|
||||||
ensureFolderData(key === 'root' ? 'root' : key, { force: true, prefetchDepth: 1 }),
|
ensureFolderData(key),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -212,16 +205,12 @@ const useFolderTreeActions = ({
|
|||||||
async (folderId: FolderKey | null, { preserveSearch = false }: LoadFolderOptions = {}) => {
|
async (folderId: FolderKey | null, { preserveSearch = false }: LoadFolderOptions = {}) => {
|
||||||
const targetId = folderId || 'root';
|
const targetId = folderId || 'root';
|
||||||
setSelectedFolder(targetId);
|
setSelectedFolder(targetId);
|
||||||
await ensureFolderAncestorsLoaded(targetId);
|
|
||||||
expandFolderAncestors(targetId);
|
|
||||||
try {
|
try {
|
||||||
const contents = await ensureFolderData(targetId, { force: true, prefetchDepth: 1 });
|
const contents = await ensureFolderData(targetId);
|
||||||
if (targetId !== 'root') {
|
if (targetId !== 'root') {
|
||||||
try {
|
try {
|
||||||
await ensureFolderData('root', {
|
await ensureFolderData('root', {
|
||||||
force: false,
|
|
||||||
includeDocuments: false,
|
includeDocuments: false,
|
||||||
prefetchDepth: 1,
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to refresh root folder tree', error);
|
console.warn('Failed to refresh root folder tree', error);
|
||||||
@@ -237,9 +226,7 @@ const useFolderTreeActions = ({
|
|||||||
},
|
},
|
||||||
[
|
[
|
||||||
applySelectedFolder,
|
applySelectedFolder,
|
||||||
ensureFolderAncestorsLoaded,
|
|
||||||
ensureFolderData,
|
ensureFolderData,
|
||||||
expandFolderAncestors,
|
|
||||||
notifyApiError,
|
notifyApiError,
|
||||||
setSearchResultIds,
|
setSearchResultIds,
|
||||||
setSelectedFolder,
|
setSelectedFolder,
|
||||||
@@ -250,9 +237,6 @@ const useFolderTreeActions = ({
|
|||||||
async (folderId: FolderKey | null, { replace = false, immediate = false }: SelectFolderOptions = {}) => {
|
async (folderId: FolderKey | null, { replace = false, immediate = false }: SelectFolderOptions = {}) => {
|
||||||
const targetId = folderId && folderId !== 'root' ? folderId : 'root';
|
const targetId = folderId && folderId !== 'root' ? folderId : 'root';
|
||||||
|
|
||||||
await ensureFolderAncestorsLoaded(targetId);
|
|
||||||
expandFolderAncestors(targetId);
|
|
||||||
|
|
||||||
if (!navigate || immediate) {
|
if (!navigate || immediate) {
|
||||||
await loadFolder(targetId, { preserveSearch: isFilterActive });
|
await loadFolder(targetId, { preserveSearch: isFilterActive });
|
||||||
setSelectedFolder(targetId);
|
setSelectedFolder(targetId);
|
||||||
@@ -263,8 +247,6 @@ const useFolderTreeActions = ({
|
|||||||
navigate(path, { replace });
|
navigate(path, { replace });
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
ensureFolderAncestorsLoaded,
|
|
||||||
expandFolderAncestors,
|
|
||||||
isFilterActive,
|
isFilterActive,
|
||||||
loadFolder,
|
loadFolder,
|
||||||
navigate,
|
navigate,
|
||||||
@@ -381,7 +363,7 @@ const useFolderTreeActions = ({
|
|||||||
|
|
||||||
// Refresh the parent folder to ensure consistency
|
// Refresh the parent folder to ensure consistency
|
||||||
const refreshTarget = targetParentId || 'root';
|
const refreshTarget = targetParentId || 'root';
|
||||||
await ensureFolderData(refreshTarget, { force: true, prefetchDepth: 1 });
|
await ensureFolderData(refreshTarget);
|
||||||
|
|
||||||
await selectFolder(folderData.id, { immediate: true });
|
await selectFolder(folderData.id, { immediate: true });
|
||||||
succeeded = true;
|
succeeded = true;
|
||||||
@@ -425,10 +407,7 @@ const useFolderTreeActions = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const contents = await ensureFolderData(folderId, {
|
const contents = await ensureFolderData(folderId);
|
||||||
force: true,
|
|
||||||
prefetchDepth: 1,
|
|
||||||
});
|
|
||||||
const hasChildren = (contents.subfolders || []).length > 0;
|
const hasChildren = (contents.subfolders || []).length > 0;
|
||||||
const hasDocs = (contents.documents || []).length > 0;
|
const hasDocs = (contents.documents || []).length > 0;
|
||||||
if (hasChildren || hasDocs) {
|
if (hasChildren || hasDocs) {
|
||||||
@@ -469,13 +448,10 @@ const useFolderTreeActions = ({
|
|||||||
const node = folderNodes.get(folderId);
|
const node = folderNodes.get(folderId);
|
||||||
const parentId = node?.parentId || 'root';
|
const parentId = node?.parentId || 'root';
|
||||||
setSelectedFolder(parentId);
|
setSelectedFolder(parentId);
|
||||||
const parentContents = await ensureFolderData(parentId, {
|
const parentContents = await ensureFolderData(parentId);
|
||||||
force: true,
|
|
||||||
prefetchDepth: 1,
|
|
||||||
});
|
|
||||||
applySelectedFolder(parentId, parentContents);
|
applySelectedFolder(parentId, parentContents);
|
||||||
} else if (selectedFolder !== 'root') {
|
} else if (selectedFolder !== 'root') {
|
||||||
await ensureFolderData(selectedFolder, { force: true, prefetchDepth: 1 });
|
await ensureFolderData(selectedFolder);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showMessage) {
|
if (showMessage) {
|
||||||
@@ -507,36 +483,6 @@ const useFolderTreeActions = ({
|
|||||||
|
|
||||||
const folderClickHandlers: FolderClickHandlers = useMemo(
|
const folderClickHandlers: FolderClickHandlers = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
onToggle: async (folderId: FolderKey) => {
|
|
||||||
const node = folderNodes.get(folderId);
|
|
||||||
const nextExpanded = !(node?.expanded ?? false);
|
|
||||||
if (nextExpanded) {
|
|
||||||
try {
|
|
||||||
await ensureFolderData(folderId, {
|
|
||||||
includeDocuments: false,
|
|
||||||
prefetchDepth: 1,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
notifyApiError(error, 'Failed to load folder.');
|
|
||||||
}
|
|
||||||
} else if (node && !node.loaded) {
|
|
||||||
try {
|
|
||||||
await ensureFolderData(folderId, {
|
|
||||||
includeDocuments: false,
|
|
||||||
prefetchDepth: 1,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
notifyApiError(error, 'Failed to load folder.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setFolderNodes((prev) => {
|
|
||||||
const next = new Map(prev);
|
|
||||||
const current = next.get(folderId);
|
|
||||||
if (!current) return prev;
|
|
||||||
next.set(folderId, { ...current, expanded: nextExpanded });
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onSelect: selectFolder,
|
onSelect: selectFolder,
|
||||||
onDrop: async (event: DragEvent<HTMLElement>, folderId: FolderKey) => {
|
onDrop: async (event: DragEvent<HTMLElement>, folderId: FolderKey) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -659,18 +605,14 @@ const useFolderTreeActions = ({
|
|||||||
[
|
[
|
||||||
draggedDocumentIds,
|
draggedDocumentIds,
|
||||||
draggedFolderId,
|
draggedFolderId,
|
||||||
ensureFolderData,
|
|
||||||
folderNodes,
|
|
||||||
handleFileDrop,
|
handleFileDrop,
|
||||||
isInvalidFolderDrop,
|
isInvalidFolderDrop,
|
||||||
moveDocumentsToFolder,
|
moveDocumentsToFolder,
|
||||||
moveFolder,
|
moveFolder,
|
||||||
notifyApiError,
|
|
||||||
selectFolder,
|
selectFolder,
|
||||||
selectedFolder,
|
selectedFolder,
|
||||||
setDraggedDocumentIds,
|
setDraggedDocumentIds,
|
||||||
setDraggedFolderId,
|
setDraggedFolderId,
|
||||||
setFolderNodes,
|
|
||||||
setStatusMessage,
|
setStatusMessage,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ interface UseDocumentUploadsArgs {
|
|||||||
token?: string | null;
|
token?: string | null;
|
||||||
selectedFolder?: FolderId;
|
selectedFolder?: FolderId;
|
||||||
currentFolderName?: string | null;
|
currentFolderName?: string | null;
|
||||||
ensureFolderData: (folderId: FolderId, options?: { force?: boolean; prefetchDepth?: number }) => Promise<void>;
|
ensureFolderData: (folderId: FolderId, options?: { [key: string]: unknown }) => Promise<void>;
|
||||||
refreshCurrentFolder: () => Promise<void>;
|
refreshCurrentFolder: () => Promise<void>;
|
||||||
shellRef: MutableRefObject<HTMLElement | null>;
|
shellRef: MutableRefObject<HTMLElement | null>;
|
||||||
notifyApiError?: NotifyApiError;
|
notifyApiError?: NotifyApiError;
|
||||||
@@ -441,7 +441,7 @@ const useDocumentUploads = ({
|
|||||||
targetFolderId !== 'root' &&
|
targetFolderId !== 'root' &&
|
||||||
targetFolderId !== selectedFolder
|
targetFolderId !== selectedFolder
|
||||||
) {
|
) {
|
||||||
await ensureFolderData(targetFolderId, { force: true, prefetchDepth: 1 });
|
await ensureFolderData(targetFolderId);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
const message = error.message || 'Failed to upload files.';
|
const message = error.message || 'Failed to upload files.';
|
||||||
|
|||||||
+10
-2
@@ -14,7 +14,7 @@ import DocumentsRoute from './app/DocumentsRoute';
|
|||||||
import DropOverlay from './app/DropOverlay';
|
import DropOverlay from './app/DropOverlay';
|
||||||
import LoginRoute from './app/LoginRoute';
|
import LoginRoute from './app/LoginRoute';
|
||||||
import SettingsRoute from './app/SettingsRoute';
|
import SettingsRoute from './app/SettingsRoute';
|
||||||
import { AppStateProvider } from './lib/store/appState';
|
import { AppStateProvider, useAppState } from './lib/store/appState';
|
||||||
import { useDocumentsPreferences } from './app/useDocumentsPreferences';
|
import { useDocumentsPreferences } from './app/useDocumentsPreferences';
|
||||||
import { AppShellContext } from './lib/context/AppShellContext';
|
import { AppShellContext } from './lib/context/AppShellContext';
|
||||||
import useDocumentsWorkspace from './documents/data/useDocumentsWorkspace';
|
import useDocumentsWorkspace from './documents/data/useDocumentsWorkspace';
|
||||||
@@ -80,10 +80,18 @@ const AppLayout: React.FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const TenantAwareLayout: React.FC = () => {
|
||||||
|
const { tenant } = useAppState();
|
||||||
|
// Force remount when tenant changes to ensure clean state (folders, selection, etc.)
|
||||||
|
const key = tenant?.id ? String(tenant.id) : undefined;
|
||||||
|
|
||||||
|
return <AppLayout key={key} />;
|
||||||
|
};
|
||||||
|
|
||||||
const AppRouter: React.FC = () => (
|
const AppRouter: React.FC = () => (
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/account/login" element={<LoginRoute />} />
|
<Route path="/account/login" element={<LoginRoute />} />
|
||||||
<Route element={<AppLayout />}>
|
<Route element={<TenantAwareLayout />}>
|
||||||
<Route path="/" element={<Navigate to="/documents" replace />} />
|
<Route path="/" element={<Navigate to="/documents" replace />} />
|
||||||
<Route path="/documents" element={<DocumentsRoute />} />
|
<Route path="/documents" element={<DocumentsRoute />} />
|
||||||
<Route path="/documents/folder/:folderId" element={<DocumentsRoute />} />
|
<Route path="/documents/folder/:folderId" element={<DocumentsRoute />} />
|
||||||
|
|||||||
@@ -67,6 +67,8 @@ interface FolderInfo {
|
|||||||
|
|
||||||
export interface FolderTreeNode extends FolderInfo {
|
export interface FolderTreeNode extends FolderInfo {
|
||||||
children?: FolderTreeNode[];
|
children?: FolderTreeNode[];
|
||||||
|
hasChildren?: boolean;
|
||||||
|
loaded?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CapabilitySetResponse {
|
export interface CapabilitySetResponse {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import React, { useRef, useSyncExternalStore, useCallback } from 'react';
|
import React, { useRef } from 'react';
|
||||||
import { useAppShell } from '../lib/context/AppShellContext';
|
import { useAppShell } from '../lib/context/AppShellContext';
|
||||||
import FoldersManager from '../documents/FoldersManager';
|
|
||||||
import { usePanelResizeBindings } from '../app/PanelManagerContext';
|
import { usePanelResizeBindings } from '../app/PanelManagerContext';
|
||||||
import type { Identifier } from '../types/identifiers';
|
import type { Identifier } from '../types/identifiers';
|
||||||
import SidebarFolderList from './components/SidebarFolderList';
|
import SidebarFolderList from './components/SidebarFolderList';
|
||||||
@@ -11,10 +10,7 @@ import { TenantOption } from './components/SidebarMenu';
|
|||||||
import SidebarHeader from './components/SidebarHeader';
|
import SidebarHeader from './components/SidebarHeader';
|
||||||
import SidebarSearch from './components/SidebarSearch';
|
import SidebarSearch from './components/SidebarSearch';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
interface SidebarProps {
|
interface SidebarProps {
|
||||||
onToggle: (folderId: FolderIdentifier) => void;
|
|
||||||
onSelect: (folderId: FolderIdentifier) => void;
|
onSelect: (folderId: FolderIdentifier) => void;
|
||||||
onDrop: (event: React.DragEvent<HTMLDivElement>, folderId: FolderIdentifier) => void;
|
onDrop: (event: React.DragEvent<HTMLDivElement>, folderId: FolderIdentifier) => void;
|
||||||
onDragOver: (event: React.DragEvent<HTMLDivElement>, folderId: FolderIdentifier) => void;
|
onDragOver: (event: React.DragEvent<HTMLDivElement>, folderId: FolderIdentifier) => void;
|
||||||
@@ -30,7 +26,6 @@ interface SidebarProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Sidebar: React.FC<SidebarProps> = ({
|
const Sidebar: React.FC<SidebarProps> = ({
|
||||||
onToggle,
|
|
||||||
onSelect,
|
onSelect,
|
||||||
onDrop,
|
onDrop,
|
||||||
onDragOver,
|
onDragOver,
|
||||||
@@ -51,7 +46,6 @@ const Sidebar: React.FC<SidebarProps> = ({
|
|||||||
handleLogout,
|
handleLogout,
|
||||||
tags = [],
|
tags = [],
|
||||||
correspondents = [],
|
correspondents = [],
|
||||||
foldersManager,
|
|
||||||
tenant,
|
tenant,
|
||||||
tenants,
|
tenants,
|
||||||
tenantOptions,
|
tenantOptions,
|
||||||
@@ -60,19 +54,6 @@ const Sidebar: React.FC<SidebarProps> = ({
|
|||||||
handleFileSelection, // Used for upload
|
handleFileSelection, // Used for upload
|
||||||
} = useAppShell();
|
} = useAppShell();
|
||||||
|
|
||||||
// Subscribe to folder tree updates
|
|
||||||
const subscribeToTree = useCallback((callback: () => void) => {
|
|
||||||
if (!foldersManager) return () => { };
|
|
||||||
return (foldersManager as FoldersManager).subscribe(callback);
|
|
||||||
}, [foldersManager]);
|
|
||||||
|
|
||||||
const getTreeSnapshot = useCallback(() => {
|
|
||||||
if (!foldersManager) return [];
|
|
||||||
return (foldersManager as FoldersManager).getTreeSnapshot();
|
|
||||||
}, [foldersManager]);
|
|
||||||
|
|
||||||
const roots = useSyncExternalStore(subscribeToTree, getTreeSnapshot);
|
|
||||||
|
|
||||||
const onManageTags = openTagsModal;
|
const onManageTags = openTagsModal;
|
||||||
const onManageCorrespondents = openCorrespondentsModal;
|
const onManageCorrespondents = openCorrespondentsModal;
|
||||||
|
|
||||||
@@ -81,7 +62,6 @@ const Sidebar: React.FC<SidebarProps> = ({
|
|||||||
const effectiveTenants = (tenants || tenantOptions || []);
|
const effectiveTenants = (tenants || tenantOptions || []);
|
||||||
const activeTenantId = (tenant as TenantOption)?.id;
|
const activeTenantId = (tenant as TenantOption)?.id;
|
||||||
|
|
||||||
// Upload handler wrapper (if needed, or pass handleFileSelection directly if signature matches)
|
|
||||||
const onUploadFiles = handleFileSelection;
|
const onUploadFiles = handleFileSelection;
|
||||||
|
|
||||||
const sidebarRef = useRef<HTMLDivElement | null>(null);
|
const sidebarRef = useRef<HTMLDivElement | null>(null);
|
||||||
@@ -125,9 +105,7 @@ const Sidebar: React.FC<SidebarProps> = ({
|
|||||||
<SidebarSearch />
|
<SidebarSearch />
|
||||||
|
|
||||||
<SidebarFolderList
|
<SidebarFolderList
|
||||||
roots={roots}
|
|
||||||
selectedFolder={selectedFolder}
|
selectedFolder={selectedFolder}
|
||||||
onToggle={onToggle}
|
|
||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
onDrop={onDrop}
|
onDrop={onDrop}
|
||||||
onDragOver={onDragOver}
|
onDragOver={onDragOver}
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ import type { Identifier } from '../../types/identifiers';
|
|||||||
import { FolderPlusIcon } from '../../components/icons';
|
import { FolderPlusIcon } from '../../components/icons';
|
||||||
import FolderNode, { FolderIdentifier } from './SidebarFolderNode';
|
import FolderNode, { FolderIdentifier } from './SidebarFolderNode';
|
||||||
import type { FolderTreeNode } from '../../lib/api/apiTypes';
|
import type { FolderTreeNode } from '../../lib/api/apiTypes';
|
||||||
|
import { useAppShell } from '../../lib/context/AppShellContext';
|
||||||
|
import FoldersManager from '../../documents/FoldersManager';
|
||||||
|
import { useSyncExternalStore } from 'react';
|
||||||
|
|
||||||
interface SidebarFolderListProps {
|
interface SidebarFolderListProps {
|
||||||
roots: FolderTreeNode[];
|
|
||||||
selectedFolder: FolderIdentifier | null;
|
selectedFolder: FolderIdentifier | null;
|
||||||
onToggle: (folderId: FolderIdentifier) => void;
|
|
||||||
onSelect: (folderId: FolderIdentifier) => void;
|
onSelect: (folderId: FolderIdentifier) => void;
|
||||||
onDrop: (event: React.DragEvent<HTMLDivElement>, folderId: FolderIdentifier) => void;
|
onDrop: (event: React.DragEvent<HTMLDivElement>, folderId: FolderIdentifier) => void;
|
||||||
onDragOver: (event: React.DragEvent<HTMLDivElement>, folderId: FolderIdentifier) => void;
|
onDragOver: (event: React.DragEvent<HTMLDivElement>, folderId: FolderIdentifier) => void;
|
||||||
@@ -23,9 +24,7 @@ interface SidebarFolderListProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const SidebarFolderList: React.FC<SidebarFolderListProps> = ({
|
const SidebarFolderList: React.FC<SidebarFolderListProps> = ({
|
||||||
roots,
|
|
||||||
selectedFolder,
|
selectedFolder,
|
||||||
onToggle,
|
|
||||||
onSelect,
|
onSelect,
|
||||||
onDrop,
|
onDrop,
|
||||||
onDragOver,
|
onDragOver,
|
||||||
@@ -38,17 +37,56 @@ const SidebarFolderList: React.FC<SidebarFolderListProps> = ({
|
|||||||
onCreateFolder,
|
onCreateFolder,
|
||||||
creatingFolder,
|
creatingFolder,
|
||||||
}) => {
|
}) => {
|
||||||
// Local expansion state for the UI
|
const { foldersManager } = useAppShell();
|
||||||
|
|
||||||
|
const getTreeSnapshot = useCallback(() => {
|
||||||
|
if (!foldersManager) return [];
|
||||||
|
return (foldersManager as FoldersManager).getTreeSnapshot();
|
||||||
|
}, [foldersManager]);
|
||||||
|
|
||||||
|
const getFolderMap = useCallback(() => {
|
||||||
|
if (!foldersManager) return new Map();
|
||||||
|
return (foldersManager as FoldersManager).getSnapshot();
|
||||||
|
}, [foldersManager]);
|
||||||
|
|
||||||
|
const subscribeToTree = useCallback((callback: () => void) => {
|
||||||
|
if (!foldersManager) return () => { };
|
||||||
|
return (foldersManager as FoldersManager).subscribe(callback);
|
||||||
|
}, [foldersManager]);
|
||||||
|
|
||||||
|
const roots = useSyncExternalStore(subscribeToTree, getTreeSnapshot);
|
||||||
|
const folderMap = useSyncExternalStore(subscribeToTree, getFolderMap);
|
||||||
const [expandedIds, setExpandedIds] = React.useState<Set<string>>(new Set(['root']));
|
const [expandedIds, setExpandedIds] = React.useState<Set<string>>(new Set(['root']));
|
||||||
|
|
||||||
// Override validation or external toggle if needed
|
// Auto-expand ancestors when selected folder changes
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (selectedFolder) {
|
if (!selectedFolder || !folderMap) return;
|
||||||
// ensure parent paths are expanded?
|
|
||||||
// Without a parent map, we can't easily auto-expand up.
|
const ancestors = new Set<string>();
|
||||||
// Rely on user or initial state for now, or add parent pointers.
|
let current = folderMap.get(String(selectedFolder));
|
||||||
|
|
||||||
|
while (current) {
|
||||||
|
const parentId = current.parentId || current.parent_id;
|
||||||
|
if (!parentId || parentId === 'root') break;
|
||||||
|
|
||||||
|
ancestors.add(String(parentId));
|
||||||
|
current = folderMap.get(String(parentId));
|
||||||
}
|
}
|
||||||
}, [selectedFolder]);
|
|
||||||
|
if (ancestors.size > 0) {
|
||||||
|
setExpandedIds((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
let changed = false;
|
||||||
|
ancestors.forEach(id => {
|
||||||
|
if (!next.has(id)) {
|
||||||
|
next.add(id);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return changed ? next : prev;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [selectedFolder, folderMap]);
|
||||||
|
|
||||||
const handleToggle = useCallback((folderId: FolderIdentifier) => {
|
const handleToggle = useCallback((folderId: FolderIdentifier) => {
|
||||||
setExpandedIds((prev) => {
|
setExpandedIds((prev) => {
|
||||||
@@ -61,8 +99,7 @@ const SidebarFolderList: React.FC<SidebarFolderListProps> = ({
|
|||||||
}
|
}
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
onToggle(folderId); // propagate event just in case parent needs it
|
}, []);
|
||||||
}, [onToggle]);
|
|
||||||
|
|
||||||
const renderNodes = useCallback(
|
const renderNodes = useCallback(
|
||||||
(nodes: FolderTreeNode[], depth: number): ReactNode =>
|
(nodes: FolderTreeNode[], depth: number): ReactNode =>
|
||||||
@@ -126,7 +163,7 @@ const SidebarFolderList: React.FC<SidebarFolderListProps> = ({
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<ul className="folder-tree">
|
<ul className="folder-tree">
|
||||||
{renderNodes(roots, 0)}
|
{renderNodes(roots as FolderTreeNode[], 0)}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,14 +2,12 @@ import { useMemo } from 'react';
|
|||||||
import type { DragEvent } from 'react';
|
import type { DragEvent } from 'react';
|
||||||
import type { Identifier } from '../types/identifiers';
|
import type { Identifier } from '../types/identifiers';
|
||||||
|
|
||||||
type FolderToggleHandler = (folderId: Identifier) => void | Promise<void>;
|
|
||||||
type FolderDropHandler = (
|
type FolderDropHandler = (
|
||||||
event: DragEvent<HTMLElement>,
|
event: DragEvent<HTMLElement>,
|
||||||
folderId: Identifier,
|
folderId: Identifier,
|
||||||
) => void | Promise<void>;
|
) => void | Promise<void>;
|
||||||
|
|
||||||
interface FolderClickHandlers {
|
interface FolderClickHandlers {
|
||||||
onToggle: FolderToggleHandler;
|
|
||||||
onSelect: (folderId: Identifier | null) => void;
|
onSelect: (folderId: Identifier | null) => void;
|
||||||
onDrop: FolderDropHandler;
|
onDrop: FolderDropHandler;
|
||||||
onDragOver: (event: DragEvent<HTMLElement>, folderId: Identifier) => void;
|
onDragOver: (event: DragEvent<HTMLElement>, folderId: Identifier) => void;
|
||||||
@@ -30,7 +28,6 @@ interface UseSidebarPropsArgs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface SidebarHookResult {
|
interface SidebarHookResult {
|
||||||
onToggle: FolderClickHandlers['onToggle'];
|
|
||||||
onSelect: FolderClickHandlers['onSelect'];
|
onSelect: FolderClickHandlers['onSelect'];
|
||||||
onDrop: FolderClickHandlers['onDrop'];
|
onDrop: FolderClickHandlers['onDrop'];
|
||||||
onDragOver: FolderClickHandlers['onDragOver'];
|
onDragOver: FolderClickHandlers['onDragOver'];
|
||||||
@@ -45,44 +42,23 @@ interface SidebarHookResult {
|
|||||||
creatingFolder: boolean;
|
creatingFolder: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useSidebarProps = ({
|
const useSidebarProps = (args: UseSidebarPropsArgs): SidebarHookResult =>
|
||||||
folderClickHandlers,
|
|
||||||
handleFolderDelete,
|
|
||||||
handleFolderRename,
|
|
||||||
selectedFolder,
|
|
||||||
handleFolderDragStart,
|
|
||||||
handleFolderDragEnd,
|
|
||||||
draggedFolderId,
|
|
||||||
handlePromptCreateFolder,
|
|
||||||
creatingFolder,
|
|
||||||
}: UseSidebarPropsArgs): SidebarHookResult =>
|
|
||||||
useMemo(
|
useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
onToggle: folderClickHandlers.onToggle,
|
onSelect: args.folderClickHandlers.onSelect,
|
||||||
onSelect: folderClickHandlers.onSelect,
|
onDrop: args.folderClickHandlers.onDrop,
|
||||||
onDrop: folderClickHandlers.onDrop,
|
onDragOver: args.folderClickHandlers.onDragOver,
|
||||||
onDragOver: folderClickHandlers.onDragOver,
|
onDragLeave: args.folderClickHandlers.onDragLeave,
|
||||||
onDragLeave: folderClickHandlers.onDragLeave,
|
onDeleteFolder: args.handleFolderDelete,
|
||||||
onDeleteFolder: handleFolderDelete,
|
onRenameFolder: args.handleFolderRename,
|
||||||
onRenameFolder: handleFolderRename,
|
selectedFolder: args.selectedFolder,
|
||||||
selectedFolder,
|
onFolderDragStart: args.handleFolderDragStart,
|
||||||
onFolderDragStart: handleFolderDragStart,
|
onFolderDragEnd: args.handleFolderDragEnd,
|
||||||
onFolderDragEnd: handleFolderDragEnd,
|
draggedFolderId: args.draggedFolderId,
|
||||||
draggedFolderId,
|
onCreateFolder: (parentId) => args.handlePromptCreateFolder?.(parentId),
|
||||||
onCreateFolder: (parentId) => handlePromptCreateFolder?.(parentId),
|
creatingFolder: args.creatingFolder,
|
||||||
creatingFolder,
|
|
||||||
}),
|
}),
|
||||||
[
|
[args],
|
||||||
creatingFolder,
|
|
||||||
folderClickHandlers,
|
|
||||||
handleFolderDragEnd,
|
|
||||||
handleFolderDragStart,
|
|
||||||
handleFolderDelete,
|
|
||||||
handleFolderRename,
|
|
||||||
handlePromptCreateFolder,
|
|
||||||
draggedFolderId,
|
|
||||||
selectedFolder,
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
export default useSidebarProps;
|
export default useSidebarProps;
|
||||||
|
|||||||
Reference in New Issue
Block a user