import { useEffect, useState, useCallback } from 'react'; import ReactFlow, { addEdge, Background, Controls, useNodesState, useEdgesState, } from 'reactflow'; import 'reactflow/dist/style.css'; import { fetchDiagramByName, saveDiagramByName, listDiagrams, deleteDiagramByName, } from '../services/api'; import CustomNode from './CustomNode'; import IconSelector from './IconSelector'; import { toast } from 'react-toastify'; const nodeTypes = { custom: CustomNode, }; function Diagram() { const [nodes, setNodes, onNodesChange] = useNodesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]); const [selectedNode, setSelectedNode] = useState(null); const [showForm, setShowForm] = useState(false); const [newLabel, setNewLabel] = useState(''); const [selectedIcon, setSelectedIcon] = useState(''); const [selectedEdge, setSelectedEdge] = useState(null); const [diagramName, setDiagramName] = useState(null); const [diagramList, setDiagramList] = useState([]); const isIframe = window.self !== window.top; useEffect(() => { let isMounted = true; const load = async () => { try { const { diagrams } = await listDiagrams(); if (isMounted) { setDiagramList(diagrams); if (diagrams.length > 0) { setDiagramName((prev) => prev || diagrams[0]); } else { setDiagramName(null); } } } catch (err) { console.error(err); setDiagramList([]); setDiagramName(null); } }; load(); return () => { isMounted = false; }; }, []); useEffect(() => { let isMounted = true; const loadDiagram = async () => { if (!diagramName) { setNodes([]); setEdges([]); return; } try { const data = await fetchDiagramByName(diagramName); if (isMounted) { setNodes(data.nodes || []); const sanitizedEdges = (data.edges || []).map(({ id, source, target, animated }) => ({ id, source, target, animated: !!animated, })); setEdges(sanitizedEdges); setSelectedNode(null); setSelectedEdge(null); } } catch (err) { console.error(err); toast.error(`❌ Failed to load diagram "${diagramName}"`); setNodes([]); setEdges([]); } }; loadDiagram(); return () => { isMounted = false; }; }, [diagramName, setNodes, setEdges]); const handleSave = async () => { if (!diagramName) return; const cleanedEdges = edges.map(({ id, source, target, animated }) => ({ id, source, target, animated: !!animated, })); await saveDiagramByName(diagramName, { nodes, edges: cleanedEdges }); toast.success(`✅ Diagram "${diagramName}" saved!`); }; const handleAddNode = () => { setShowForm(true); }; const handleSubmitNode = () => { const id = (nodes.length + 1).toString(); const label = newLabel || `Node ${id}`; const icon = selectedIcon || 'https://s3.dvirlabs.com/lab-icons/default.svg'; const newNode = { id, type: 'custom', data: { label, icon }, position: { x: Math.random() * 400, y: Math.random() * 300 }, }; setNodes((nds) => [...nds, newNode]); toast.success(`🟢 Node "${label}" added`); setShowForm(false); setNewLabel(''); setSelectedIcon(''); }; const handleDeleteNode = () => { if (!selectedNode) return; setNodes((nds) => nds.filter((n) => n.id !== selectedNode.id)); setEdges((eds) => eds.filter((e) => e.source !== selectedNode.id && e.target !== selectedNode.id)); toast.error(`🗑️ Node "${selectedNode.data.label}" deleted`); setSelectedNode(null); }; const onEdgeClick = (_, edge) => { setSelectedEdge(edge); setSelectedNode(null); }; const handleDeleteEdge = () => { if (!selectedEdge) return; setEdges((eds) => eds.filter((e) => e.id !== selectedEdge.id)); toast.error(`🗑️ Edge "${selectedEdge.id}" deleted`); setSelectedEdge(null); }; const onConnect = useCallback( (params) => setEdges((eds) => addEdge({ ...params, animated: true }, eds)), [setEdges] ); const handleNodeDoubleClick = (event, node) => { const newLabel = prompt('Enter new name:', node.data.label); if (newLabel !== null) { setNodes((nds) => nds.map((n) => n.id === node.id ? { ...n, data: { ...n.data, label: newLabel } } : n ) ); toast.info(`✏️ Node renamed to "${newLabel}"`); } }; const onNodeClick = (_, node) => { setSelectedNode(node); }; const handleDeleteDiagram = async () => { if (!diagramName || diagramName === 'default') { toast.warn("❌ Cannot delete 'default' diagram or nothing selected."); return; } const confirmDelete = confirm(`Are you sure you want to delete diagram "${diagramName}"?`); if (!confirmDelete) return; try { await deleteDiagramByName(diagramName); const updatedList = diagramList.filter(name => name !== diagramName); setDiagramList(updatedList); const newSelected = updatedList[0] || null; setDiagramName(newSelected); setNodes([]); setEdges([]); toast.success(`🗑️ Diagram "${diagramName}" deleted`); } catch (err) { toast.error(`❌ Failed to delete diagram "${diagramName}"`); } }; return (