Skip to main content
Glama
FolderSelector.tsx8.4 kB
"use client" import { Button } from "@/src/components/ui/button"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, } from "@/src/components/ui/command"; import { Popover, PopoverContent, PopoverTrigger, } from "@/src/components/ui/popover"; import { cn } from "@/src/lib/general-utils"; import { Tool } from '@superglue/shared'; import { Archive, Check, ChevronRight, Folder, FolderOpen } from "lucide-react"; import { useMemo, useState } from 'react'; const FOLDER_STORAGE_KEY = 'superglue-selected-folder'; const UNCATEGORIZED = ''; interface FolderNode { name: string; fullPath: string; count: number; children: FolderNode[]; depth: number; } interface FolderSelectorProps { tools: Tool[]; selectedFolder: string; onFolderChange: (folder: string) => void; } export function FolderSelector({ tools, selectedFolder, onFolderChange }: FolderSelectorProps) { const [open, setOpen] = useState(false); const [searchValue, setSearchValue] = useState(""); const { flatFolders, totalCount, archivedCount } = useMemo(() => { const folderCounts = new Map<string, number>(); let total = 0; let archived = 0; tools.forEach(tool => { if (tool.archived) { archived++; return; } const folder = tool.folder || UNCATEGORIZED; folderCounts.set(folder, (folderCounts.get(folder) || 0) + 1); total++; }); const flatList: FolderNode[] = []; const processedPaths = new Set<string>(); const sortedFolders = Array.from(folderCounts.keys()).sort(); sortedFolders.forEach(folderPath => { const parts = folderPath.split('/'); let currentPath = ''; parts.forEach((part, index) => { currentPath = index === 0 ? part : `${currentPath}/${part}`; if (!processedPaths.has(currentPath)) { processedPaths.add(currentPath); const isLeaf = currentPath === folderPath; flatList.push({ name: part, fullPath: currentPath, count: isLeaf ? (folderCounts.get(folderPath) || 0) : 0, children: [], depth: index, }); } }); }); return { flatFolders: flatList, totalCount: total, archivedCount: archived }; }, [tools]); const displayLabel = useMemo(() => { if (selectedFolder === 'all') return `All (${totalCount})`; if (selectedFolder === 'archived') return `Archived (${archivedCount})`; const folder = flatFolders.find(f => f.fullPath === selectedFolder); if (folder) return `${folder.fullPath} (${folder.count})`; return selectedFolder; }, [selectedFolder, flatFolders, totalCount, archivedCount]); const handleSelect = (folder: string) => { onFolderChange(folder); setOpen(false); setSearchValue(""); }; const filteredFolders = useMemo(() => { if (!searchValue) return flatFolders; const lower = searchValue.toLowerCase(); return flatFolders.filter(f => f.fullPath.toLowerCase().includes(lower)); }, [flatFolders, searchValue]); return ( <Popover open={open} onOpenChange={setOpen}> <PopoverTrigger asChild> <Button variant="outline" role="combobox" aria-expanded={open} className="w-[220px] justify-between" > <div className="flex items-center gap-2 truncate"> <Folder className="h-4 w-4 flex-shrink-0" /> <span className="truncate">{displayLabel}</span> </div> <ChevronRight className={cn( "ml-2 h-4 w-4 shrink-0 opacity-50 transition-transform", open && "rotate-90" )} /> </Button> </PopoverTrigger> <PopoverContent className="w-[280px] p-0" align="start"> <Command> <CommandInput placeholder="Search folders..." value={searchValue} onValueChange={setSearchValue} /> <CommandList> <CommandEmpty>No folders found.</CommandEmpty> <CommandGroup className="p-1"> <CommandItem value="all" onSelect={() => handleSelect('all')} className="cursor-pointer px-2 py-1.5" > {selectedFolder === 'all' ? ( <Check className="mr-2 h-4 w-4 flex-shrink-0" /> ) : ( <FolderOpen className="mr-2 h-4 w-4 flex-shrink-0" /> )} <span className={selectedFolder === 'all' ? 'font-medium' : ''}>All</span> <span className="ml-auto text-xs text-muted-foreground"> {totalCount} </span> </CommandItem> </CommandGroup> <CommandSeparator /> <CommandGroup heading="Folders" className="p-1"> {filteredFolders.map((folder) => { const isSelected = selectedFolder === folder.fullPath; return ( <CommandItem key={folder.fullPath} value={folder.fullPath} onSelect={() => handleSelect(folder.fullPath)} className="cursor-pointer py-1.5" style={{ paddingLeft: `${(folder.depth * 12) + 8}px` }} > {isSelected ? ( <Check className="mr-2 h-4 w-4 flex-shrink-0" /> ) : ( <Folder className="mr-2 h-4 w-4 flex-shrink-0" /> )} <span className={cn("truncate", isSelected && "font-medium")}>{folder.name || "No Folder"}</span> {folder.count > 0 && ( <span className="ml-auto text-xs text-muted-foreground"> {folder.count} </span> )} </CommandItem> ); })} </CommandGroup> {archivedCount > 0 && ( <> <CommandSeparator /> <CommandGroup className="p-1"> <CommandItem value="archived" onSelect={() => handleSelect('archived')} className="cursor-pointer px-2 py-1.5" > {selectedFolder === 'archived' ? ( <Check className="mr-2 h-4 w-4 flex-shrink-0" /> ) : ( <Archive className="mr-2 h-4 w-4 flex-shrink-0" /> )} <span className={selectedFolder === 'archived' ? 'font-medium' : ''}>Archived</span> <span className="ml-auto text-xs text-muted-foreground"> {archivedCount} </span> </CommandItem> </CommandGroup> </> )} </CommandList> </Command> </PopoverContent> </Popover> ); } export function useFolderFilter(tools: Tool[]) { const [selectedFolder, setSelectedFolder] = useState<string>(() => { if (typeof window !== 'undefined') { return localStorage.getItem(FOLDER_STORAGE_KEY) || 'all'; } return 'all'; }); const handleFolderChange = (folder: string) => { setSelectedFolder(folder); if (typeof window !== 'undefined') { localStorage.setItem(FOLDER_STORAGE_KEY, folder); } }; const filteredByFolder = useMemo(() => { if (selectedFolder === 'archived') { return tools.filter(tool => tool.archived); } const nonArchived = tools.filter(tool => !tool.archived); if (selectedFolder === 'all') return nonArchived; return nonArchived.filter(tool => { const toolFolder = tool.folder || UNCATEGORIZED; return toolFolder === selectedFolder || toolFolder.startsWith(`${selectedFolder}/`); }); }, [tools, selectedFolder]); const allFolderPaths = useMemo(() => { const paths = new Set<string>(); tools.forEach(tool => { if (tool.folder) paths.add(tool.folder); }); return Array.from(paths).sort(); }, [tools]); return { selectedFolder, setSelectedFolder: handleFolderChange, filteredByFolder, allFolderPaths, }; } export { FOLDER_STORAGE_KEY, UNCATEGORIZED };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/superglue-ai/superglue'

If you have feedback or need assistance with the MCP directory API, please join our Discord server