import React, { useState, useEffect } from 'react'; import { Box, VStack, HStack, Text, IconButton, Collapse, Input, InputGroup, InputLeftElement, } from '@chakra-ui/react'; import { ChevronRightIcon, ChevronDownIcon } from '@chakra-ui/icons'; import { FiFolder } from 'react-icons/fi'; interface FolderNode { name: string; path: string; children: FolderNode[]; isExpanded?: boolean; } interface FolderBrowserProps { folders: string[]; selectedFolder: string; onFolderSelect: (folder: string) => void; } const FolderBrowser: React.FC = ({ folders, selectedFolder, onFolderSelect, }) => { const [searchQuery, setSearchQuery] = useState(''); const [folderTree, setFolderTree] = useState([]); const [expandedFolders, setExpandedFolders] = useState>(new Set()); // Convert flat folder list to tree structure useEffect(() => { const tree: FolderNode[] = []; const folderMap = new Map(); // Add root node const rootNode: FolderNode = { name: '(root)', path: '', children: [] }; tree.push(rootNode); folderMap.set('', rootNode); // Process each folder path folders.forEach(folder => { if (folder === '') return; // Skip root as it's already added const parts = folder.split('/'); let currentPath = ''; parts.forEach((part) => { const parentPath = currentPath; currentPath = currentPath ? `${currentPath}/${part}` : part; if (!folderMap.has(currentPath)) { const newNode: FolderNode = { name: part, path: currentPath, children: [], }; folderMap.set(currentPath, newNode); if (parentPath === '') { // Top level folder tree.push(newNode); } else { // Subfolder const parent = folderMap.get(parentPath); if (parent) { parent.children.push(newNode); } } } }); }); setFolderTree(tree); }, [folders]); const toggleFolder = (path: string) => { const newExpanded = new Set(expandedFolders); if (newExpanded.has(path)) { newExpanded.delete(path); } else { newExpanded.add(path); } setExpandedFolders(newExpanded); }; const isExpanded = (path: string) => expandedFolders.has(path); const renderFolderNode = (node: FolderNode, level: number = 0): React.ReactNode => { const hasChildren = node.children.length > 0; const expanded = isExpanded(node.path); const isSelected = selectedFolder === node.path; // Filter children based on search query const filteredChildren = node.children.filter(child => child.name.toLowerCase().includes(searchQuery.toLowerCase()) || child.path.toLowerCase().includes(searchQuery.toLowerCase()) ); // If searching and this node doesn't match, only show if it has matching children if (searchQuery && !node.name.toLowerCase().includes(searchQuery.toLowerCase()) && filteredChildren.length === 0) { return null; } return ( onFolderSelect(node.path)} minH="32px" > {hasChildren ? ( : } onClick={(e) => { e.stopPropagation(); toggleFolder(node.path); }} aria-label={expanded ? 'Collapse folder' : 'Expand folder'} color="gray.300" _hover={{ bg: 'gray.700', color: 'white' }} _active={{ bg: 'gray.600' }} /> ) : ( )} {node.name} {hasChildren && ( {filteredChildren.map(child => renderFolderNode(child, level + 1))} )} ); }; const filteredTree = searchQuery ? folderTree.filter(node => node.name.toLowerCase().includes(searchQuery.toLowerCase()) || node.path.toLowerCase().includes(searchQuery.toLowerCase()) || node.children.some(child => child.name.toLowerCase().includes(searchQuery.toLowerCase()) || child.path.toLowerCase().includes(searchQuery.toLowerCase()) ) ) : folderTree; return ( setSearchQuery(e.target.value)} bg="gray.800" borderColor="gray.700" _focus={{ borderColor: 'blue.500' }} /> {filteredTree.map(node => renderFolderNode(node))} {searchQuery && filteredTree.length === 0 && ( No folders match your search )} ); }; export default FolderBrowser;