'use client'; import React, { useMemo, useCallback, useState, ComponentType } from 'react'; import * as Accordion from '@radix-ui/react-accordion'; import { ChevronDown, Package, Circle, Search, Trash2, Copy, Edit2, GripVertical } from 'lucide-react'; import { type Node as FlowNode } from '@xyflow/react'; import useFlowStore, { useFlowStoreActions } from '@/stores/flow-store'; import { useNodesStore } from '@/stores/nodes-store'; import { useEditorStore } from '@/stores/editor-store'; import { RegisteredNode } from '@/components/core/designer/nodes/types'; interface NodeWithConfig extends FlowNode { iconComponent?: ComponentType<{ size?: number; className?: string }>; iconColor?: string; } interface NodeGroup { type: string; items: NodeWithConfig[]; } const getNodeName = (node: FlowNode): string => { // Priority order for finding the node name // 1. Direct label if (node.data?.label && typeof node.data.label === 'string') { return node.data.label; } // 2. Check for nested structures based on node type const nodeType = node.type || 'default'; // Common nested patterns: node.data.{nodeType}.name if (node.data && typeof node.data === 'object' && node.data[nodeType] && typeof node.data[nodeType] === 'object' && node.data[nodeType].name && typeof node.data[nodeType].name === 'string') { return node.data[nodeType].name; } // 3. Check for common nested patterns const commonPaths = [ 'service', 'message', 'channel', 'event', 'command', 'query', 'domain' ]; for (const path of commonPaths) { if (node.data && typeof node.data === 'object' && node.data[path] && typeof node.data[path] === 'object' && node.data[path].name && typeof node.data[path].name === 'string') { return node.data[path].name; } } // 4. Fallback to direct name property if (node.data?.name && typeof node.data.name === 'string') { return node.data.name; } // 5. Final fallback return `${nodeType} ${node.id.slice(-4)}`; }; const getIconColorClass = (color: string): string => { const colorMap: Record = { 'red': 'text-red-600', 'blue': 'text-blue-600', 'green': 'text-green-600', 'yellow': 'text-yellow-600', 'purple': 'text-purple-600', 'pink': 'text-pink-600', 'indigo': 'text-indigo-600', 'orange': 'text-orange-600', 'teal': 'text-teal-600', 'cyan': 'text-cyan-600', 'gray': 'text-gray-600', 'grey': 'text-gray-600' }; return colorMap[color] || 'text-gray-600'; }; const ResourceList = () => { const { nodes, reactFlowInstance } = useFlowStore(); const { deleteNode, duplicateNode } = useFlowStoreActions(); const { nodes: registeredNodes } = useNodesStore(); const { setSelectedNode } = useEditorStore(); const [searchTerm, setSearchTerm] = useState(''); const [hoveredNodeId, setHoveredNodeId] = useState(null); const handleNodeClick = useCallback((nodeId: string) => { if (reactFlowInstance) { const node = nodes.find(n => n.id === nodeId); if (node) { reactFlowInstance.fitView({ nodes: [{ id: nodeId }], duration: 800, padding: 0.3, }); // Select the node reactFlowInstance.setNodes(prevNodes => prevNodes.map(n => ({ ...n, selected: n.id === nodeId })) ); } } }, [nodes, reactFlowInstance]); const handleDeleteNode = useCallback((nodeId: string, event: React.MouseEvent) => { event.stopPropagation(); deleteNode(nodeId); }, [deleteNode]); const handleDuplicateNode = useCallback((nodeId: string, event: React.MouseEvent) => { event.stopPropagation(); duplicateNode(nodeId); }, [duplicateNode]); const handleMouseEnter = useCallback((nodeId: string) => { setHoveredNodeId(nodeId); }, []); const handleMouseLeave = useCallback(() => { setHoveredNodeId(null); }, []); const handleEditNode = useCallback((nodeId: string, event: React.MouseEvent) => { event.stopPropagation(); const node = nodes.find(n => n.id === nodeId); if (node) { setSelectedNode(node); } }, [nodes, setSelectedNode]); const nodeGroups = useMemo((): NodeGroup[] => { if (!nodes || !Array.isArray(nodes) || !registeredNodes) { return []; } // Create a lookup map for registered node configurations const nodeConfigMap = registeredNodes.reduce((map: Record; color: string }>, registeredNode: RegisteredNode) => { map[registeredNode.type] = { icon: registeredNode.configuration?.icon || Circle, color: registeredNode.configuration?.color || 'gray' }; return map; }, {} as Record; color: string }>); const groupedNodes = nodes.reduce((groups: { [key: string]: NodeWithConfig[] }, node: FlowNode) => { const nodeType = node.type || 'default'; const config = nodeConfigMap[nodeType] || { icon: Circle, color: 'gray' }; const nodeWithConfig: NodeWithConfig = { ...node, iconComponent: config.icon, iconColor: config.color }; if (!groups[nodeType]) { groups[nodeType] = []; } groups[nodeType].push(nodeWithConfig); return groups; }, {}); return Object.entries(groupedNodes).map(([type, items]) => ({ type: type.charAt(0).toUpperCase() + type.slice(1), items: items })); }, [nodes, registeredNodes]); const filteredNodeGroups = useMemo(() => { if (!nodeGroups || !Array.isArray(nodeGroups) || nodeGroups.length === 0) return []; if (searchTerm.trim() === '') return nodeGroups; return nodeGroups .map((group: NodeGroup) => { // If group type matches search, return entire group if (group.type.toLowerCase().includes(searchTerm.toLowerCase())) { return group; } // Filter items within the group const filteredItems = group.items.filter((node: NodeWithConfig) => { const nodeName = getNodeName(node); return nodeName.toLowerCase().includes(searchTerm.toLowerCase()) || (node.type || '').toLowerCase().includes(searchTerm.toLowerCase()); }); if (filteredItems.length > 0) { return { ...group, items: filteredItems }; } return null; }) .filter((group): group is NodeGroup => group !== null); }, [searchTerm, nodeGroups]); return (
setSearchTerm(e.target.value)} value={searchTerm} />
{nodeGroups.length === 0 ? (

No nodes on canvas.

) : filteredNodeGroups.length === 0 && searchTerm.trim() !== '' ? (

No results found.

) : ( g.type)}> {filteredNodeGroups.map((group: NodeGroup) => ( {group.type}
{group.items.map((node: NodeWithConfig) => { const NodeIcon = node.iconComponent || Circle; const iconColor = node.iconColor || 'gray'; const iconClassName = getIconColorClass(iconColor); const isHovered = hoveredNodeId === node.id; return (
handleNodeClick(node.id)} onMouseEnter={() => handleMouseEnter(node.id)} onMouseLeave={handleMouseLeave} >
{getNodeName(node)}
); })}
))}
)}
); }; export default ResourceList;