"use client"; import { useState, useEffect, useCallback, useRef } from "react"; import { useTranslations } from "next-intl"; import { Folder, FolderOpen, ChevronRight, ChevronDown, Home, Share2, } from "lucide-react"; import { cn } from "@/lib/utils"; import { useFileStore, type FileResource } from "@/stores/file-store"; interface FolderNode { id: string; name: string; path: string; } interface FolderTreeSidebarProps { currentPath: string; onNavigate: (path: string, resourceId?: string | null) => void; listByParentId: (parentId: string | null) => Promise; width?: number; isResizing?: boolean; } export function FolderTreeSidebar({ currentPath, onNavigate, listByParentId, width = 256, isResizing }: FolderTreeSidebarProps) { const t = useTranslations("files"); const client = useFileStore(s => s.client); const sharedRoots = useFileStore(s => s.sharedRoots); const loadSharedRoots = useFileStore(s => s.loadSharedRoots); const [rootChildren, setRootChildren] = useState(null); const [expandedIds, setExpandedIds] = useState>(new Set(["root"])); const [loadingIds, setLoadingIds] = useState>(new Set()); // Cache: parentId (or "root") -> FolderNode[] const [childrenCache, setChildrenCache] = useState>(new Map()); // Map folder path -> id for reverse lookup const pathToIdRef = useRef>(new Map()); const loadChildren = useCallback(async (parentId: string | null, parentPath: string) => { const cacheKey = parentId ?? "root"; // Skip if already loading or cached if (childrenCache.has(cacheKey)) return; setLoadingIds(prev => new Set(prev).add(cacheKey)); try { const resources = await listByParentId(parentId); const folders = resources .filter(r => r.isDirectory) .sort((a, b) => a.name.localeCompare(b.name)) .map(r => { const folderPath = parentPath === "/" ? `/${r.name}` : `${parentPath}/${r.name}`; pathToIdRef.current.set(folderPath, r.id); return { id: r.id, name: r.name, path: folderPath, }; }); // Don't cache empty root results - empty root likely means client wasn't ready yet if (folders.length > 0 || parentId !== null) { setChildrenCache(prev => new Map(prev).set(cacheKey, folders)); } if (parentId === null) { setRootChildren(folders); } } catch { // Silently fail } finally { setLoadingIds(prev => { const next = new Set(prev); next.delete(cacheKey); return next; }); } }, [childrenCache, listByParentId]); // Load root folders when client is available (handles page refresh timing) useEffect(() => { if (client) { loadChildren(null, "/"); // Discover folders shared with the user by other principals. loadSharedRoots(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [client]); // Auto-expand along the current path when navigating useEffect(() => { if (currentPath === "/") return; const segments = currentPath.split("/").filter(Boolean); // Walk down the path and expand + load each ancestor let ancestorPath = ""; for (let i = 0; i < segments.length; i++) { ancestorPath = "/" + segments.slice(0, i + 1).join("/"); const folderId = pathToIdRef.current.get(ancestorPath); if (folderId) { setExpandedIds(prev => { if (prev.has(folderId)) return prev; return new Set(prev).add(folderId); }); if (!childrenCache.has(folderId)) { loadChildren(folderId, ancestorPath); } } } }, [currentPath, childrenCache, loadChildren]); const handleToggleExpand = useCallback(async (folderId: string, folderPath: string) => { setExpandedIds(prev => { const next = new Set(prev); if (next.has(folderId)) { next.delete(folderId); } else { next.add(folderId); } return next; }); if (!childrenCache.has(folderId)) { await loadChildren(folderId, folderPath); } }, [childrenCache, loadChildren]); const handleFolderClick = useCallback((path: string, id: string | null) => { onNavigate(path, id); }, [onNavigate]); return (
{/* Root / Home entry */}
{/* Folder tree */} {rootChildren === null && loadingIds.has("root") ? (
) : ( rootChildren?.map(folder => ( )) )} {/* Shared with me: folders another principal has shared with the user */} {sharedRoots.filter(r => r.isDirectory).length > 0 && (
{t("shared_with_me")}
{sharedRoots.filter(r => r.isDirectory).map(r => { const path = `/${r.name}`; const isSelected = currentPath === path; return (
); })}
)}
); } function FolderTreeItem({ node, depth, currentPath, expandedIds, loadingIds, childrenCache, onToggleExpand, onFolderClick, onLoadChildren, }: { node: FolderNode; depth: number; currentPath: string; expandedIds: Set; loadingIds: Set; childrenCache: Map; onToggleExpand: (folderId: string, folderPath: string) => void; onFolderClick: (path: string, id: string | null) => void; onLoadChildren: (parentId: string, parentPath: string) => Promise; }) { const isExpanded = expandedIds.has(node.id); const isSelected = currentPath === node.path; const _isLoading = loadingIds.has(node.id); const children = childrenCache.get(node.id); const hasChildren = children !== undefined && children.length > 0; const indentPx = depth * 16; const Icon = isExpanded && hasChildren ? FolderOpen : Folder; // Eagerly load children on mount to know if subfolders exist useEffect(() => { if (children === undefined && !loadingIds.has(node.id)) { onLoadChildren(node.id, node.path); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return ( <>
{/* Expand/collapse chevron */} {hasChildren ? ( ) : null} {/* Folder name */}
{/* Children */} {isExpanded && children && (
{children.map(child => ( ))}
)} ); }