/** * T077: Directory tree component with collapsible folders */ import React, { useState, useMemo, useEffect } from 'react'; import { ChevronRight, ChevronDown, Folder, File } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { ScrollArea } from '@/components/ui/scroll-area'; import type { NoteSummary } from '@/types/note'; import { cn } from '@/lib/utils'; interface TreeNode { name: string; path: string; type: 'file' | 'folder'; children?: TreeNode[]; note?: NoteSummary; } interface DirectoryTreeProps { notes: NoteSummary[]; selectedPath?: string; onSelectNote: (path: string) => void; onMoveNote?: (oldPath: string, newFolderPath: string) => void; } /** * Build a tree structure from flat list of note paths */ function buildTree(notes: NoteSummary[]): TreeNode[] { const root: TreeNode = { name: '', path: '', type: 'folder', children: [] }; for (const note of notes) { const parts = note.note_path.split('/'); let current = root; // Navigate/create folders for (let i = 0; i < parts.length - 1; i++) { const folderName = parts[i]; const folderPath = parts.slice(0, i + 1).join('/'); let folder = current.children?.find( (child) => child.name === folderName && child.type === 'folder' ); if (!folder) { folder = { name: folderName, path: folderPath, type: 'folder', children: [], }; current.children = current.children || []; current.children.push(folder); } current = folder; } // Add file const fileName = parts[parts.length - 1]; current.children = current.children || []; current.children.push({ name: fileName, path: note.note_path, type: 'file', note, }); } // Sort children: folders first, then files, alphabetically const sortChildren = (node: TreeNode) => { if (node.children) { node.children.sort((a, b) => { if (a.type !== b.type) { return a.type === 'folder' ? -1 : 1; } return a.name.localeCompare(b.name); }); node.children.forEach(sortChildren); } }; sortChildren(root); return root.children || []; } interface TreeNodeItemProps { node: TreeNode; depth: number; selectedPath?: string; onSelectNote: (path: string) => void; onMoveNote?: (oldPath: string, newFolderPath: string) => void; forceExpandState?: boolean; } function TreeNodeItem({ node, depth, selectedPath, onSelectNote, onMoveNote, forceExpandState }: TreeNodeItemProps) { const [isOpen, setIsOpen] = useState(depth < 2); // Auto-expand first 2 levels const [isDragOver, setIsDragOver] = useState(false); // T014: Use forceExpandState if provided, otherwise use local isOpen state // Sync local state with force state when it changes const effectiveIsOpen = forceExpandState ?? isOpen; // Update local state when force state is applied useEffect(() => { if (forceExpandState !== undefined) { setIsOpen(forceExpandState); } }, [forceExpandState]); const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); if (node.type === 'folder') { setIsDragOver(true); } }; const handleDragLeave = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragOver(false); }; const handleDrop = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragOver(false); if (node.type === 'folder') { const draggedPath = e.dataTransfer.getData('application/note-path'); if (draggedPath && onMoveNote) { onMoveNote(draggedPath, node.path); } } }; const handleDragStart = (e: React.DragEvent) => { if (node.type === 'file') { e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('application/note-path', node.path); } }; if (node.type === 'folder') { return (