'use client'; import React, { useState, useMemo, useEffect, ComponentType, useCallback } from 'react'; import * as Accordion from '@radix-ui/react-accordion'; import { Search, PlusSquare, ChevronDown, GripVertical, Package, Plus, } from 'lucide-react'; import { type Node as FlowNode } from '@xyflow/react'; import { getNodeId } from '@/utils/react-flow'; import { RegisteredNode, NodeCategory } from '@/components/core/designer/nodes/types'; import { useNodesStore } from '@/stores/nodes-store'; import { useFlowStoreActions } from '@/stores/flow-store'; import { useSidebarStore } from '@/stores/sidebar-store'; interface NodeListItemData { label: string; version: string; name: string; summary: string; mode: string; } interface NodeListItem { name: string; NodeIconComponent: ComponentType; nodeIconColor: string; nodeType: string; data: NodeListItemData; id: string; } interface NodeGroup { type: string; CategoryIconComponent: ComponentType; items: NodeListItem[]; } interface DraggableNodeItemProps { item: NodeListItem; isHovered: boolean; isSelected: boolean; onItemDragStart: (event: React.DragEvent, nodeType: string, label: string, data: NodeListItemData) => void; onItemMouseEnter: () => void; onItemMouseLeave: () => void; onItemAdd: (nodeType: string, label: string, data: NodeListItemData) => void; onItemSelect: (nodeType: string, label: string, data: NodeListItemData, iconComponent: any, iconColor: string) => void; } const DraggableNodeItem = React.memo(({ item, isHovered, isSelected, onItemDragStart, onItemMouseEnter, onItemMouseLeave, onItemAdd, onItemSelect }) => { const ItmIcon = item.NodeIconComponent as any; return (
onItemDragStart(event, item.nodeType ?? 'default', item.name, item.data)} onMouseEnter={onItemMouseEnter} onMouseLeave={onItemMouseLeave} onClick={() => onItemSelect(item.nodeType ?? 'default', item.name, item.data, item.NodeIconComponent, item.nodeIconColor)} >
{item.name}
); }); DraggableNodeItem.displayName = 'DraggableNodeItem'; const NodeList = () => { const [searchTerm, setSearchTerm] = useState(''); const [openAccordionItems, setOpenAccordionItems] = useState([]); const [hoveredItemId, setHoveredItemId] = useState(null); const { addNode } = useFlowStoreActions(); const { selectedComponent, setSelectedComponent } = useSidebarStore(); const onDragStart = useCallback((event: React.DragEvent, nodeType: string, label: string, data: NodeListItemData) => { event.dataTransfer.setData('application/reactflow', JSON.stringify({ nodeType, label, data })); event.dataTransfer.effectAllowed = 'move'; }, []); const { nodes, nodeCategories } = useNodesStore(); const handleAddNode = useCallback((nodeType: string, label: string, itemData: NodeListItemData) => { const position = { x: Math.random() * 400, y: Math.random() * 400 }; const newNode: FlowNode = { id: getNodeId(nodeType), type: nodeType, position, data: { ...itemData, label }, }; addNode(newNode); }, [addNode]); const componentGroups = useMemo((): NodeGroup[] => { if (!nodeCategories || !nodes || !Array.isArray(nodeCategories) || !Array.isArray(nodes)) { return []; } return nodeCategories.map((category: NodeCategory) => { const categoryItems: NodeListItem[] = (nodes || []) .filter((node: RegisteredNode) => node && node.category === category.type) .map((node: RegisteredNode, index: number) => { const name = node.type.charAt(0).toUpperCase() + node.type.slice(1); const IconComp = node.configuration.icon ?? Package; const iconColor = node.configuration.color ?? 'gray'; return { name, NodeIconComponent: IconComp, nodeIconColor: iconColor, nodeType: node.type, id: `${category.type}-${node.type}-${index}`, data: { ...node.configuration.defaultData, ...(node?.configuration?.editor ? { editor: node.configuration.editor } : {}), } }; }); return { type: category.label, CategoryIconComponent: category.icon, items: categoryItems, }; }); }, [nodes, nodeCategories]); const filteredAndMemoizedGroups = useMemo(() => { if (!componentGroups || !Array.isArray(componentGroups) || componentGroups.length === 0) return []; return componentGroups .map((group: NodeGroup | null) => { if (!group || !group.items || !Array.isArray(group.items)) return null; if (group.type.toLowerCase().includes(searchTerm.toLowerCase())) { return group; } const filteredItems = group.items.filter((item: NodeListItem) => item && item.name && item.name.toLowerCase().includes(searchTerm.toLowerCase()) ); if (filteredItems.length > 0) { return { ...group, items: filteredItems }; } return null; }) .filter((group): group is NodeGroup => group !== null); }, [searchTerm, componentGroups]); useEffect(() => { if (searchTerm.trim() === '') { // Open all sections by default const allSections = filteredAndMemoizedGroups.map((g: NodeGroup) => g.type); if (JSON.stringify(openAccordionItems) !== JSON.stringify(allSections)) { setOpenAccordionItems(allSections); } } else { const itemsToOpen = filteredAndMemoizedGroups.map((g: NodeGroup) => g.type); if (JSON.stringify(openAccordionItems) !== JSON.stringify(itemsToOpen)) { setOpenAccordionItems(itemsToOpen); } } }, [searchTerm, filteredAndMemoizedGroups]); const handleItemMouseEnter = useCallback((itemId: string) => { setHoveredItemId(itemId); }, []); const handleItemMouseLeave = useCallback(() => { setHoveredItemId(null); }, []); const handleItemSelect = useCallback((nodeType: string, label: string, data: NodeListItemData, iconComponent: any, iconColor: string) => { const component = { nodeType, label, data, iconComponent, iconColor }; // Toggle selection - if same component is clicked, deselect if (selectedComponent && selectedComponent.nodeType === nodeType && selectedComponent.label === label) { setSelectedComponent(null); } else { setSelectedComponent(component); } }, [selectedComponent, setSelectedComponent]); return (
setSearchTerm(e.target.value)} value={searchTerm} />
{filteredAndMemoizedGroups.length === 0 && searchTerm.trim() !== '' ? (

No results found.

) : ( filteredAndMemoizedGroups .map((group: NodeGroup) => { const GrpIcon = group.CategoryIconComponent as any; return group && ( {group.type}
{group.items.map((item: NodeListItem) => ( handleItemMouseEnter(item.id)} onItemMouseLeave={handleItemMouseLeave} onItemAdd={handleAddNode} onItemSelect={handleItemSelect} /> ))}
); }) )}
); }; export default NodeList;