import { useState, useEffect, useCallback } from 'react' import { Folder, ChevronDown, ChevronRight, Plus, MoreVertical, Pencil, Trash2, FolderPlus, X } from 'lucide-react' import { foldersApi } from '@/api/folders' import type { FolderListItem, FolderTreeItem } from '@/types' import { cn } from '@/lib/utils' interface FolderSidebarProps { selectedFolderId: string | null onFolderSelect: (folderId: string | null) => void onCreateFolder: (parentId?: string | null) => void onEditFolder: (folder: FolderListItem) => void mobileOpen?: boolean onMobileClose?: () => void } // Build tree structure from flat folder list function buildFolderTree(folders: FolderListItem[]): FolderTreeItem[] { const folderMap = new Map() const rootFolders: FolderTreeItem[] = [] // First pass: create all folder items folders.forEach((folder) => { folderMap.set(folder.id, { ...folder, children: [], isExpanded: true, // Default expanded }) }) // Second pass: build parent-child relationships folders.forEach((folder) => { const treeItem = folderMap.get(folder.id)! if (folder.parent_id && folderMap.has(folder.parent_id)) { const parent = folderMap.get(folder.parent_id)! parent.children.push(treeItem) } else { rootFolders.push(treeItem) } }) // Sort children by display_order const sortChildren = (items: FolderTreeItem[]) => { items.sort((a, b) => a.display_order - b.display_order) items.forEach((item) => sortChildren(item.children)) } sortChildren(rootFolders) return rootFolders } // Get all descendant IDs (for cascade delete warning) function getDescendantIds(folders: FolderListItem[], folderId: string): string[] { const children = folders.filter((f) => f.parent_id === folderId) const descendantIds: string[] = [] children.forEach((child) => { descendantIds.push(child.id) descendantIds.push(...getDescendantIds(folders, child.id)) }) return descendantIds } interface ContextMenuState { x: number y: number folder: FolderTreeItem canAddSubfolder: boolean } interface FolderItemProps { folder: FolderTreeItem depth: number selectedFolderId: string | null expandedIds: Set menuOpenId: string | null onFolderSelect: (folderId: string) => void onToggleExpand: (folderId: string) => void onMenuToggle: (folderId: string | null) => void onEditFolder: (folder: FolderListItem) => void onAddSubfolder: (parentId: string) => void onDeleteFolder: (folderId: string, hasChildren: boolean) => void onContextMenu: (e: React.MouseEvent, folder: FolderTreeItem, canAddSubfolder: boolean) => void canAddSubfolder: boolean } function FolderItem({ folder, depth, selectedFolderId, expandedIds, menuOpenId, onFolderSelect, onToggleExpand, onMenuToggle, onEditFolder, onAddSubfolder, onDeleteFolder, onContextMenu, canAddSubfolder, }: FolderItemProps) { const isExpanded = expandedIds.has(folder.id) const hasSubfolders = folder.children.length > 0 const handleContextMenu = (e: React.MouseEvent) => { e.preventDefault() e.stopPropagation() onContextMenu(e, folder, canAddSubfolder) } return (
) : ( // Spacer for alignment )} {folder.name} {folder.tree_count} {/* Folder menu button - replaces tree count on hover */} {/* Dropdown menu */} {menuOpenId === folder.id && (
{canAddSubfolder && ( )}
)}
{/* Children */} {isExpanded && hasSubfolders && (
{folder.children.map((child) => ( ))}
)}
) } export function FolderSidebar({ selectedFolderId, onFolderSelect, onCreateFolder, onEditFolder, mobileOpen = false, onMobileClose, }: FolderSidebarProps) { const [folders, setFolders] = useState([]) const [folderTree, setFolderTree] = useState([]) const [isExpanded, setIsExpanded] = useState(true) const [isLoading, setIsLoading] = useState(true) const [menuOpenId, setMenuOpenId] = useState(null) const [expandedIds, setExpandedIds] = useState>(new Set()) const [contextMenu, setContextMenu] = useState(null) const loadFolders = useCallback(async () => { setIsLoading(true) try { const data = await foldersApi.list() setFolders(data) setFolderTree(buildFolderTree(data)) // Expand all by default setExpandedIds(new Set(data.map((f) => f.id))) } catch (err) { console.error('Failed to load folders:', err) } finally { setIsLoading(false) } }, []) useEffect(() => { loadFolders() }, [loadFolders]) const handleToggleExpand = (folderId: string) => { setExpandedIds((prev) => { const next = new Set(prev) if (next.has(folderId)) { next.delete(folderId) } else { next.add(folderId) } return next }) } const handleDeleteFolder = async (folderId: string, folderHasChildren: boolean) => { const descendantCount = getDescendantIds(folders, folderId).length const message = folderHasChildren ? `Are you sure you want to delete this folder and its ${descendantCount} subfolder(s)? The trees in them will not be deleted.` : 'Are you sure you want to delete this folder? The trees in it will not be deleted.' if (!confirm(message)) { return } try { await foldersApi.delete(folderId) // Remove folder and all descendants from local state const idsToRemove = new Set([folderId, ...getDescendantIds(folders, folderId)]) const updatedFolders = folders.filter((f) => !idsToRemove.has(f.id)) setFolders(updatedFolders) setFolderTree(buildFolderTree(updatedFolders)) if (selectedFolderId && idsToRemove.has(selectedFolderId)) { onFolderSelect(null) } } catch (err) { console.error('Failed to delete folder:', err) } } const handleAddSubfolder = (parentId: string) => { onCreateFolder(parentId) } const handleContextMenu = (e: React.MouseEvent, folder: FolderTreeItem, canAddSubfolder: boolean) => { setContextMenu({ x: e.clientX, y: e.clientY, folder, canAddSubfolder, }) setMenuOpenId(null) // Close any open hover menu } const closeContextMenu = () => { setContextMenu(null) } // Refresh folders when a folder is edited or created useEffect(() => { const handleFolderChange = () => loadFolders() window.addEventListener('folder-changed', handleFolderChange) return () => window.removeEventListener('folder-changed', handleFolderChange) }, [loadFolders]) // Close hover menu when clicking outside useEffect(() => { if (!menuOpenId) return const handleClickOutside = () => setMenuOpenId(null) document.addEventListener('click', handleClickOutside) return () => document.removeEventListener('click', handleClickOutside) }, [menuOpenId]) // Close context menu on click outside, right-click elsewhere, or Escape useEffect(() => { if (!contextMenu) return const close = () => setContextMenu(null) const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') close() } document.addEventListener('click', close) document.addEventListener('contextmenu', close) document.addEventListener('keydown', handleKeyDown) return () => { document.removeEventListener('click', close) document.removeEventListener('contextmenu', close) document.removeEventListener('keydown', handleKeyDown) } }, [contextMenu]) return ( <> {/* Mobile backdrop */} {mobileOpen && (