feat: Introduce FoldersManager and useFolderData to manage and display folder contents, including documents and subfolders.
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||||
import type { ReactNode, ComponentProps } from 'react';
|
import type { ReactNode, ComponentProps } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
DocumentsFilterProvider,
|
DocumentsFilterProvider,
|
||||||
} from '../documents/context/DocumentsFilterContext';
|
} from '../documents/context/DocumentsFilterContext';
|
||||||
@@ -12,12 +12,14 @@ 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<{
|
||||||
sidebarProps: ComponentProps<typeof Sidebar> | null;
|
sidebarProps: ComponentProps<typeof Sidebar> | null;
|
||||||
surfaceConfig: any;
|
surfaceConfig: any;
|
||||||
|
folderData: any;
|
||||||
onNavigate: (documentId: string) => void;
|
onNavigate: (documentId: string) => void;
|
||||||
}> = ({ sidebarProps, surfaceConfig, onNavigate }) => {
|
}> = ({ sidebarProps, surfaceConfig, folderData, onNavigate }) => {
|
||||||
const { openPreview } = usePreviewContext();
|
const { openPreview } = usePreviewContext();
|
||||||
const { collapsed: sidebarCollapsed } = useSidebarContext();
|
const { collapsed: sidebarCollapsed } = useSidebarContext();
|
||||||
const {
|
const {
|
||||||
@@ -46,9 +48,16 @@ const DocumentsInner: React.FC<{
|
|||||||
|
|
||||||
const documentsTablePropsWithNav = useMemo(() => (
|
const documentsTablePropsWithNav = useMemo(() => (
|
||||||
surfaceConfig.documentsTableProps
|
surfaceConfig.documentsTableProps
|
||||||
? { ...surfaceConfig.documentsTableProps, onBreadcrumbNavigate: handleBreadcrumbNavigate }
|
? {
|
||||||
|
...surfaceConfig.documentsTableProps,
|
||||||
|
onBreadcrumbNavigate: handleBreadcrumbNavigate,
|
||||||
|
documents: folderData.documents,
|
||||||
|
subfolders: folderData.subfolders,
|
||||||
|
currentFolderName: folderData.folder?.name,
|
||||||
|
isSearchLoading: folderData.loading,
|
||||||
|
}
|
||||||
: null
|
: null
|
||||||
), [surfaceConfig.documentsTableProps, handleBreadcrumbNavigate]);
|
), [surfaceConfig.documentsTableProps, handleBreadcrumbNavigate, folderData]);
|
||||||
|
|
||||||
const { surface } = useWorkspaceSurface({
|
const { surface } = useWorkspaceSurface({
|
||||||
sidebarHidden,
|
sidebarHidden,
|
||||||
@@ -98,8 +107,13 @@ 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,6 +127,7 @@ const DocumentsRouteContent: React.FC = () => {
|
|||||||
<DocumentsInner
|
<DocumentsInner
|
||||||
sidebarProps={sidebarProps}
|
sidebarProps={sidebarProps}
|
||||||
surfaceConfig={surfaceConfig}
|
surfaceConfig={surfaceConfig}
|
||||||
|
folderData={folderData}
|
||||||
onNavigate={handleDocumentNavigate}
|
onNavigate={handleDocumentNavigate}
|
||||||
/>
|
/>
|
||||||
</PreviewProvider>
|
</PreviewProvider>
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ interface DocumentsShellView {
|
|||||||
sidebarProps: ComponentProps<typeof Sidebar> | null;
|
sidebarProps: ComponentProps<typeof Sidebar> | null;
|
||||||
surfaceConfig: WorkspaceSurfaceConfig;
|
surfaceConfig: WorkspaceSurfaceConfig;
|
||||||
documentsFilter: DocumentsFilterValue;
|
documentsFilter: DocumentsFilterValue;
|
||||||
|
documentsManager: any;
|
||||||
|
foldersManager: any; // Using any to avoid circular dependency
|
||||||
}
|
}
|
||||||
|
|
||||||
const useDocumentsShell = (): DocumentsShellView => {
|
const useDocumentsShell = (): DocumentsShellView => {
|
||||||
@@ -49,6 +51,8 @@ const useDocumentsShell = (): DocumentsShellView => {
|
|||||||
sidebarProps: sidebarWithActions,
|
sidebarProps: sidebarWithActions,
|
||||||
surfaceConfig,
|
surfaceConfig,
|
||||||
documentsFilter: shell.documentsFilter as DocumentsFilterValue,
|
documentsFilter: shell.documentsFilter as DocumentsFilterValue,
|
||||||
|
documentsManager: shell.documentsManager,
|
||||||
|
foldersManager: shell.foldersManager,
|
||||||
};
|
};
|
||||||
}, [shell]);
|
}, [shell]);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,177 @@
|
|||||||
|
import { shallowEqual } from 'react-redux';
|
||||||
|
import type { FolderNodeId } from '../types/identifiers';
|
||||||
|
import type { Folder } from '../types/documents';
|
||||||
|
|
||||||
|
type ManagedFolder = Folder;
|
||||||
|
|
||||||
|
type FetchFolder = (id: FolderNodeId) => Promise<unknown>;
|
||||||
|
|
||||||
|
class FoldersManager<T extends ManagedFolder = ManagedFolder> {
|
||||||
|
private byId: Map<FolderNodeId, T>;
|
||||||
|
private fetcher?: FetchFolder;
|
||||||
|
private inflight: Map<FolderNodeId, Promise<T | null>>;
|
||||||
|
private listeners: Set<() => void>;
|
||||||
|
private emitScheduled: boolean;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
fetchFolder?: FetchFolder,
|
||||||
|
) {
|
||||||
|
this.byId = new Map();
|
||||||
|
this.fetcher = fetchFolder;
|
||||||
|
this.inflight = new Map();
|
||||||
|
this.listeners = new Set();
|
||||||
|
this.emitScheduled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private emit() {
|
||||||
|
if (this.emitScheduled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.emitScheduled = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
this.emitScheduled = false;
|
||||||
|
this.listeners.forEach((fn) => fn());
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribe(listener: () => void) {
|
||||||
|
this.listeners.add(listener);
|
||||||
|
return () => this.listeners.delete(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
setFetcher(fetchFolder?: FetchFolder) {
|
||||||
|
this.fetcher = fetchFolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
ingest(rawFolders: unknown[] = []): { canonical: T[]; changed: boolean } {
|
||||||
|
const folders = rawFolders.map((f) => f as T).filter(Boolean);
|
||||||
|
let changed = false;
|
||||||
|
let nextById = this.byId;
|
||||||
|
const canonical: T[] = [];
|
||||||
|
|
||||||
|
folders.forEach((folder) => {
|
||||||
|
const id = folder?.id;
|
||||||
|
if (id == null) {
|
||||||
|
canonical.push(folder);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = nextById.get(id as FolderNodeId);
|
||||||
|
const merged = existing ? ({ ...existing, ...folder } as T) : ({ ...(folder as T) } as T);
|
||||||
|
const useExisting = existing && shallowEqual(existing, merged);
|
||||||
|
const nextFolder = useExisting ? (existing as T) : merged;
|
||||||
|
|
||||||
|
if (!useExisting) {
|
||||||
|
if (!changed) {
|
||||||
|
nextById = new Map(this.byId);
|
||||||
|
}
|
||||||
|
nextById.set(id as FolderNodeId, nextFolder);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
canonical.push(nextFolder);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
this.byId = nextById;
|
||||||
|
this.emit();
|
||||||
|
}
|
||||||
|
|
||||||
|
return { canonical, changed };
|
||||||
|
}
|
||||||
|
|
||||||
|
async ensure(id: FolderNodeId, fetcherOverride?: FetchFolder): Promise<T | null> {
|
||||||
|
if (id == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cached = this.byId.get(id);
|
||||||
|
if (cached) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetcher = fetcherOverride || this.fetcher;
|
||||||
|
if (!fetcher) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const inflight = this.inflight.get(id);
|
||||||
|
if (inflight) {
|
||||||
|
return inflight;
|
||||||
|
}
|
||||||
|
|
||||||
|
const request = (async () => {
|
||||||
|
try {
|
||||||
|
const fetched = await fetcher(id);
|
||||||
|
const { canonical } = this.ingest([fetched as unknown]);
|
||||||
|
return canonical[0] ?? null;
|
||||||
|
} finally {
|
||||||
|
this.inflight.delete(id);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
this.inflight.set(id, request);
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
map(mapper: (folder: T) => T | undefined): boolean {
|
||||||
|
if (!this.byId.size) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let changed = false;
|
||||||
|
const next = new Map<FolderNodeId, T>();
|
||||||
|
this.byId.forEach((folder, key) => {
|
||||||
|
const updated = mapper(folder);
|
||||||
|
const nextFolder = updated === undefined ? folder : updated;
|
||||||
|
if (nextFolder !== folder) {
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
next.set(key, nextFolder ?? folder);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
this.byId = next;
|
||||||
|
this.emit();
|
||||||
|
}
|
||||||
|
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(ids: Array<FolderNodeId>): boolean {
|
||||||
|
if (!Array.isArray(ids) || ids.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let changed = false;
|
||||||
|
let next = this.byId;
|
||||||
|
ids.forEach((id) => {
|
||||||
|
if (next.has(id)) {
|
||||||
|
if (!changed) {
|
||||||
|
next = new Map(this.byId);
|
||||||
|
}
|
||||||
|
next.delete(id);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (changed) {
|
||||||
|
this.byId = next;
|
||||||
|
this.emit();
|
||||||
|
}
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
getById(id: FolderNodeId): T | null {
|
||||||
|
return this.byId.get(id) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getMany(ids: Array<FolderNodeId> = []): T[] {
|
||||||
|
return ids
|
||||||
|
.map((id) => this.byId.get(id) || null)
|
||||||
|
.filter((folder): folder is T => Boolean(folder));
|
||||||
|
}
|
||||||
|
|
||||||
|
getSnapshot(): Map<FolderNodeId, T> {
|
||||||
|
return this.byId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FoldersManager;
|
||||||
@@ -40,6 +40,7 @@ import { useStatusToast, type ToastVariant } from '../../lib/context/StatusToast
|
|||||||
import useAuthManager from './useAuthManager';
|
import useAuthManager from './useAuthManager';
|
||||||
import useTenantManager from './useTenantManager';
|
import useTenantManager from './useTenantManager';
|
||||||
import useDocuments from './useDocuments';
|
import useDocuments from './useDocuments';
|
||||||
|
import FoldersManager from '../FoldersManager';
|
||||||
import { fetchDocument } from '../../lib/api/apiClient';
|
import { fetchDocument } from '../../lib/api/apiClient';
|
||||||
import useFolderTree from '../features/folders/useFolderTree';
|
import useFolderTree from '../features/folders/useFolderTree';
|
||||||
import useFolderTreeActions from '../features/folders/useFolderTreeActions';
|
import useFolderTreeActions from '../features/folders/useFolderTreeActions';
|
||||||
@@ -315,6 +316,12 @@ const useDocumentsWorkspace = ({
|
|||||||
fetchDocumentById,
|
fetchDocumentById,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const foldersManagerRef = useRef<FoldersManager | null>(null);
|
||||||
|
if (!foldersManagerRef.current) {
|
||||||
|
foldersManagerRef.current = new FoldersManager();
|
||||||
|
}
|
||||||
|
const foldersManager = foldersManagerRef.current;
|
||||||
|
|
||||||
const documentLookup = useSyncExternalStore(
|
const documentLookup = useSyncExternalStore(
|
||||||
(onStoreChange) => documentsManager.subscribe(onStoreChange),
|
(onStoreChange) => documentsManager.subscribe(onStoreChange),
|
||||||
() => documentsManager.getSnapshot(),
|
() => documentsManager.getSnapshot(),
|
||||||
@@ -1301,6 +1308,8 @@ const useDocumentsWorkspace = ({
|
|||||||
uploadQueue,
|
uploadQueue,
|
||||||
clearUploadQueue,
|
clearUploadQueue,
|
||||||
documentsFilter,
|
documentsFilter,
|
||||||
|
documentsManager,
|
||||||
|
foldersManager,
|
||||||
};
|
};
|
||||||
|
|
||||||
// hook callers handle rendering / routing
|
// hook callers handle rendering / routing
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
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>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export 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;
|
||||||
@@ -9,37 +9,23 @@ import {
|
|||||||
createDocumentEntryKey,
|
createDocumentEntryKey,
|
||||||
createFolderEntryKey,
|
createFolderEntryKey,
|
||||||
} from '../../../app/entryKey';
|
} from '../../../app/entryKey';
|
||||||
import type { FolderId, FolderNodeId, Identifier } from '../../../types/identifiers';
|
import type { FolderNodeId, Identifier, FolderId } from '../../../types/identifiers';
|
||||||
import type { Document } from '../../../types/documents';
|
import type { Document, Folder, FolderContents } from '../../../types/documents';
|
||||||
|
|
||||||
interface FolderSummary {
|
interface FolderContentsEntry extends FolderContents {
|
||||||
id?: FolderNodeId;
|
|
||||||
name?: string;
|
|
||||||
parent_id?: FolderNodeId | null;
|
|
||||||
parentId?: FolderNodeId | null;
|
|
||||||
children?: FolderNodeId[];
|
|
||||||
subfolders?: FolderSummary[];
|
|
||||||
has_children?: boolean;
|
|
||||||
hasChildren?: boolean;
|
|
||||||
[key: string]: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FolderContentsEntry {
|
|
||||||
folder?: FolderSummary | null;
|
|
||||||
documents?: Document[];
|
|
||||||
subfolders?: FolderSummary[];
|
|
||||||
__includesDocuments?: boolean;
|
__includesDocuments?: boolean;
|
||||||
__sortField?: string | null;
|
__sortField?: string | null;
|
||||||
__sortDirection?: string | null;
|
__sortDirection?: string | null;
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FolderTreeNode extends FolderSummary {
|
interface FolderTreeNode extends Folder {
|
||||||
id: FolderNodeId;
|
id: FolderNodeId;
|
||||||
children: FolderNodeId[];
|
children: FolderNodeId[];
|
||||||
expanded?: boolean;
|
expanded?: boolean;
|
||||||
loaded?: boolean;
|
loaded?: boolean;
|
||||||
hasChildren?: boolean;
|
hasChildren?: boolean;
|
||||||
|
parentId?: FolderNodeId;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SelectionHelpers {
|
interface SelectionHelpers {
|
||||||
@@ -129,8 +115,8 @@ const useFolderTree = ({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const [selectedFolder, setSelectedFolder] = useState<FolderNodeId>(initialSelectedFolder || 'root');
|
const [selectedFolder, setSelectedFolder] = useState<FolderNodeId>(initialSelectedFolder || 'root');
|
||||||
const [currentFolder, setCurrentFolder] = useState<FolderSummary | null>(null);
|
const [currentFolder, setCurrentFolder] = useState<Folder | null>(null);
|
||||||
const [currentSubfolders, setCurrentSubfolders] = useState<FolderSummary[]>([]);
|
const [currentSubfolders, setCurrentSubfolders] = useState<Folder[]>([]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
focusedDocumentId,
|
focusedDocumentId,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import SelectionSummary from './SelectionSummary';
|
|||||||
import { useAppState } from '../../../lib/store/appState';
|
import { useAppState } from '../../../lib/store/appState';
|
||||||
import { useWorkspaceSelectionContext } from '../../../app/WorkspaceSelectionContext';
|
import { useWorkspaceSelectionContext } from '../../../app/WorkspaceSelectionContext';
|
||||||
import type { DocumentId } from '../../../types/identifiers';
|
import type { DocumentId } from '../../../types/identifiers';
|
||||||
import type { FolderTreeNode } from '../../../lib/api/apiTypes';
|
import type { FolderTreeNode, FolderTreeResponseItem } from '../../../lib/api/apiTypes';
|
||||||
|
|
||||||
type NullableDocumentId = DocumentId | null;
|
type NullableDocumentId = DocumentId | null;
|
||||||
|
|
||||||
@@ -251,31 +251,14 @@ const SelectionFloatingActions: React.FC<SelectionFloatingActionsProps> = ({
|
|||||||
try {
|
try {
|
||||||
const data = await getFolderTree();
|
const data = await getFolderTree();
|
||||||
|
|
||||||
// Convert flat list to tree
|
const traverse = (items: FolderTreeResponseItem[]): FolderTreeNode[] => {
|
||||||
const nodeMap = new Map<string, FolderTreeNode>();
|
return items.map(item => ({
|
||||||
data.forEach((item) => {
|
|
||||||
nodeMap.set(item.id, {
|
|
||||||
...item,
|
...item,
|
||||||
children: [],
|
children: item.children ? traverse(item.children) : [],
|
||||||
} as FolderTreeNode);
|
}));
|
||||||
});
|
};
|
||||||
|
|
||||||
const roots: FolderTreeNode[] = [];
|
|
||||||
data.forEach((item) => {
|
|
||||||
const node = nodeMap.get(item.id);
|
|
||||||
if (!node) return;
|
|
||||||
|
|
||||||
if (item.children && item.children.length > 0) {
|
|
||||||
node.children = item.children
|
|
||||||
.map((id) => nodeMap.get(id))
|
|
||||||
.filter((n): n is FolderTreeNode => Boolean(n));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!item.parent_id) {
|
|
||||||
roots.push(node);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
const roots = traverse(data);
|
||||||
setRemoteFolderTree(roots);
|
setRemoteFolderTree(roots);
|
||||||
return roots;
|
return roots;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -14,6 +14,13 @@ export interface DocumentCorrespondent {
|
|||||||
count?: number;
|
count?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FolderContents {
|
||||||
|
folder: Folder | null;
|
||||||
|
documents: Document[];
|
||||||
|
subfolders: Folder[];
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A tag entity as returned by the API (includes usage_count).
|
* A tag entity as returned by the API (includes usage_count).
|
||||||
* Use DocumentTag for the embedded version on documents.
|
* Use DocumentTag for the embedded version on documents.
|
||||||
@@ -76,8 +83,12 @@ export interface Document {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface Folder {
|
export interface Folder {
|
||||||
id?: Identifier | 'root';
|
id: Identifier | 'root';
|
||||||
name?: string;
|
name: string;
|
||||||
|
parent_id?: Identifier | 'root' | null;
|
||||||
|
created_at?: string;
|
||||||
|
updated_at?: string;
|
||||||
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
type FolderEntry = {
|
type FolderEntry = {
|
||||||
|
|||||||
Reference in New Issue
Block a user