// //////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: /Data/WebUI/src/Flow_Editor.jsx // Import Node Configuration Sidebar import NodeConfigurationSidebar from "./Node_Configuration_Sidebar"; import React, { useState, useEffect, useCallback, useRef } from "react"; import ReactFlow, { Background, addEdge, applyNodeChanges, applyEdgeChanges, useReactFlow } from "reactflow"; import { Menu, MenuItem, MenuList, Slider, Box } from "@mui/material"; import { Polyline as PolylineIcon, DeleteForever as DeleteForeverIcon, Edit as EditIcon } from "@mui/icons-material"; import { SketchPicker } from "react-color"; import "reactflow/dist/style.css"; export default function FlowEditor({ flowId, nodes, edges, setNodes, setEdges, nodeTypes, categorizedNodes }) { // Node Configuration Sidebar State const [drawerOpen, setDrawerOpen] = useState(false); const [selectedNodeId, setSelectedNodeId] = useState(null); useEffect(() => { window.BorealisOpenDrawer = (id) => { setSelectedNodeId(id); setDrawerOpen(true); }; return () => { delete window.BorealisOpenDrawer; }; }, []); const selectedNode = nodes.find((n) => n.id === selectedNodeId); const wrapperRef = useRef(null); const { project } = useReactFlow(); const [contextMenu, setContextMenu] = useState(null); const [edgeContextMenu, setEdgeContextMenu] = useState(null); const [selectedEdgeId, setSelectedEdgeId] = useState(null); const [showColorPicker, setShowColorPicker] = useState(false); const [colorPickerMode, setColorPickerMode] = useState(null); const [labelPadding, setLabelPadding] = useState([8, 4]); const [labelBorderRadius, setLabelBorderRadius] = useState(4); const [labelOpacity, setLabelOpacity] = useState(0.8); const [tempColor, setTempColor] = useState({ hex: "#58a6ff" }); const [pickerPos, setPickerPos] = useState({ x: 0, y: 0 }); // helper-line state // guides: array of { xFlow, xPx } or { yFlow, yPx } for stationary nodes const [guides, setGuides] = useState([]); // activeGuides: array of { xPx } or { yPx } to draw const [activeGuides, setActiveGuides] = useState([]); // store moving node flow-size on drag start const movingFlowSize = useRef({ width: 0, height: 0 }); const edgeStyles = { step: "step", curved: "bezier", straight: "straight", smoothstep: "smoothstep" }; const animationStyles = { dashes: { animated: true, style: { strokeDasharray: "6 3" } }, dots: { animated: true, style: { strokeDasharray: "2 4" } }, none: { animated: false, style: {} } }; // Compute edge-only guides and capture moving node flow-size const computeGuides = useCallback((dragNode) => { if (!wrapperRef.current) return; const parentRect = wrapperRef.current.getBoundingClientRect(); // measure moving node in pixel space const dragEl = wrapperRef.current.querySelector( `.react-flow__node[data-id="${dragNode.id}"]` ); if (dragEl) { const dr = dragEl.getBoundingClientRect(); const relLeft = dr.left - parentRect.left; const relTop = dr.top - parentRect.top; const relRight = relLeft + dr.width; const relBottom = relTop + dr.height; // project pixel corners to flow coords const pTL = project({ x: relLeft, y: relTop }); const pTR = project({ x: relRight, y: relTop }); const pBL = project({ x: relLeft, y: relBottom }); movingFlowSize.current = { width: pTR.x - pTL.x, height: pBL.y - pTL.y }; } const lines = []; nodes.forEach((n) => { if (n.id === dragNode.id) return; const el = wrapperRef.current.querySelector( `.react-flow__node[data-id="${n.id}"]` ); if (!el) return; const r = el.getBoundingClientRect(); const relLeft = r.left - parentRect.left; const relTop = r.top - parentRect.top; const relRight = relLeft + r.width; const relBottom = relTop + r.height; // project pixel to flow coords const pTL = project({ x: relLeft, y: relTop }); const pTR = project({ x: relRight, y: relTop }); const pBL = project({ x: relLeft, y: relBottom }); // vertical guides: left edge, right edge lines.push({ xFlow: pTL.x, xPx: relLeft }); lines.push({ xFlow: pTR.x, xPx: relRight }); // horizontal guides: top edge, bottom edge lines.push({ yFlow: pTL.y, yPx: relTop }); lines.push({ yFlow: pBL.y, yPx: relBottom }); }); setGuides(lines); }, [nodes, project]); // Snap & show only matching guides within threshold during drag const onNodeDrag = useCallback((_, node) => { const threshold = 5; let snapX = null, snapY = null; const show = []; const { width: fw, height: fh } = movingFlowSize.current; guides.forEach((ln) => { if (ln.xFlow != null) { // moving left edge to stationary edges if (Math.abs(node.position.x - ln.xFlow) < threshold) { snapX = ln.xFlow; show.push({ xPx: ln.xPx }); } // moving right edge to stationary edges else if (Math.abs(node.position.x + fw - ln.xFlow) < threshold) { snapX = ln.xFlow - fw; show.push({ xPx: ln.xPx }); } } if (ln.yFlow != null) { // moving top edge if (Math.abs(node.position.y - ln.yFlow) < threshold) { snapY = ln.yFlow; show.push({ yPx: ln.yPx }); } // moving bottom edge else if (Math.abs(node.position.y + fh - ln.yFlow) < threshold) { snapY = ln.yFlow - fh; show.push({ yPx: ln.yPx }); } } }); if (snapX !== null || snapY !== null) { setNodes((nds) => applyNodeChanges( [{ id: node.id, type: "position", position: { x: snapX !== null ? snapX : node.position.x, y: snapY !== null ? snapY : node.position.y } }], nds ) ); setActiveGuides(show); } else { setActiveGuides([]); } }, [guides, setNodes]); const onDrop = useCallback((event) => { event.preventDefault(); const type = event.dataTransfer.getData("application/reactflow"); if (!type) return; const bounds = wrapperRef.current.getBoundingClientRect(); const position = project({ x: event.clientX - bounds.left, y: event.clientY - bounds.top }); const id = "node-" + Date.now(); const nodeMeta = Object.values(categorizedNodes).flat().find((n) => n.type === type); const newNode = { id, type, position, data: { label: nodeMeta?.label || type, content: nodeMeta?.content }, dragHandle: ".borealis-node-header" }; setNodes((nds) => [...nds, newNode]); }, [project, setNodes, categorizedNodes]); const onDragOver = useCallback((event) => { event.preventDefault(); event.dataTransfer.dropEffect = "move"; }, []); const onConnect = useCallback((params) => { setEdges((eds) => addEdge({ ...params, type: "bezier", animated: true, style: { strokeDasharray: "6 3", stroke: "#58a6ff" } }, eds) ); }, [setEdges]); const onNodesChange = useCallback((changes) => { setNodes((nds) => applyNodeChanges(changes, nds)); }, [setNodes]); const onEdgesChange = useCallback((changes) => { setEdges((eds) => applyEdgeChanges(changes, eds)); }, [setEdges]); const handleRightClick = (e, node) => { e.preventDefault(); setContextMenu({ mouseX: e.clientX + 2, mouseY: e.clientY - 6, nodeId: node.id }); }; const handleEdgeRightClick = (e, edge) => { e.preventDefault(); setEdgeContextMenu({ edgeId: edge.id, mouseX: e.clientX + 2, mouseY: e.clientY - 6 }); setSelectedEdgeId(edge.id); }; const changeEdgeType = (newType) => { setEdges((eds) => eds.map((e) => e.id === selectedEdgeId ? { ...e, type: edgeStyles[newType] } : e )); setEdgeContextMenu(null); }; const changeEdgeAnimation = (newAnim) => { setEdges((eds) => eds.map((e) => { if (e.id !== selectedEdgeId) return e; const strokeColor = e.style?.stroke || "#58a6ff"; const anim = animationStyles[newAnim] || {}; return { ...e, animated: anim.animated, style: { ...anim.style, stroke: strokeColor }, markerEnd: e.markerEnd ? { ...e.markerEnd, color: strokeColor } : undefined }; })); setEdgeContextMenu(null); }; const handleColorChange = (color) => { setEdges((eds) => eds.map((e) => { if (e.id !== selectedEdgeId) return e; const updated = { ...e }; if (colorPickerMode === "stroke") { updated.style = { ...e.style, stroke: color.hex }; if (e.markerEnd) updated.markerEnd = { ...e.markerEnd, color: color.hex }; } else if (colorPickerMode === "labelText") { updated.labelStyle = { ...e.labelStyle, fill: color.hex }; } else if (colorPickerMode === "labelBg") { updated.labelBgStyle = { ...e.labelBgStyle, fill: color.hex, fillOpacity: labelOpacity }; } return updated; })); }; const handleAddLabel = () => { setEdges((eds) => eds.map((e) => e.id === selectedEdgeId ? { ...e, label: "New Label" } : e )); setEdgeContextMenu(null); }; const handleEditLabel = () => { const newText = prompt("Enter label text:"); if (newText !== null) { setEdges((eds) => eds.map((e) => e.id === selectedEdgeId ? { ...e, label: newText } : e )); } setEdgeContextMenu(null); }; const handleRemoveLabel = () => { setEdges((eds) => eds.map((e) => e.id === selectedEdgeId ? { ...e, label: undefined } : e )); setEdgeContextMenu(null); }; const handlePickColor = (mode) => { setColorPickerMode(mode); setTempColor({ hex: "#58a6ff" }); setPickerPos({ x: edgeContextMenu?.mouseX || 0, y: edgeContextMenu?.mouseY || 0 }); setShowColorPicker(true); }; const applyLabelStyleExtras = () => { setEdges((eds) => eds.map((e) => e.id === selectedEdgeId ? { ...e, labelBgPadding: labelPadding, labelBgStyle: { ...e.labelBgStyle, fillOpacity: labelOpacity, rx: labelBorderRadius, ry: labelBorderRadius } } : e )); setEdgeContextMenu(null); }; useEffect(() => { const nodeCountEl = document.getElementById("nodeCount"); if (nodeCountEl) nodeCountEl.innerText = nodes.length; }, [nodes]); const nodeDef = selectedNode ? Object.values(categorizedNodes).flat().find((def) => def.type === selectedNode.type) : null; return (
computeGuides(node)} onNodeDrag={onNodeDrag} onNodeDragStop={() => { setGuides([]); setActiveGuides([]); }} > {/* helper lines */} {activeGuides.map((ln, i) => ln.xPx != null ? (
) : (
) )} setContextMenu(null)} anchorReference="anchorPosition" anchorPosition={contextMenu ? { top: contextMenu.mouseY, left: contextMenu.mouseX } : undefined} PaperProps={{ sx: { bgcolor: "#1e1e1e", color: "#fff", fontSize: "13px" } }} > { if (contextMenu?.nodeId) { setEdges((eds) => eds.filter((e) => e.source !== contextMenu.nodeId && e.target !== contextMenu.nodeId )); } setContextMenu(null); }}> Disconnect All Edges { if (contextMenu?.nodeId) { setNodes((nds) => nds.filter((n) => n.id !== contextMenu.nodeId)); setEdges((eds) => eds.filter((e) => e.source !== contextMenu.nodeId && e.target !== contextMenu.nodeId )); } setContextMenu(null); }}> Remove Node setEdgeContextMenu(null)} anchorReference="anchorPosition" anchorPosition={edgeContextMenu ? { top: edgeContextMenu.mouseY, left: edgeContextMenu.mouseX } : undefined} PaperProps={{ sx: { bgcolor: "#1e1e1e", color: "#fff", fontSize: "13px" } }} > setEdges((eds) => eds.filter((e) => e.id !== selectedEdgeId))}> Unlink Edge Edge Styles changeEdgeType("step")}>Step changeEdgeType("curved")}>Curved changeEdgeType("straight")}>Straight changeEdgeType("smoothstep")}>Smoothstep Animations changeEdgeAnimation("dashes")}>Dashes changeEdgeAnimation("dots")}>Dots changeEdgeAnimation("none")}>Solid Line Label Add Remove Edit handlePickColor("labelText")}>Text Color handlePickColor("labelBg")}>Background Color Padding: { const parts = e.target.value.split(",").map((v) => parseInt(v.trim())); if (parts.length === 2 && parts.every(Number.isFinite)) setLabelPadding(parts); }} /> Radius: { const val = parseInt(e.target.value); if (!isNaN(val)) setLabelBorderRadius(val); }} /> Opacity: setLabelOpacity(v)} step={0.05} min={0} max={1} style={{ width: 100 }} /> { const v = parseFloat(e.target.value); if (!isNaN(v)) setLabelOpacity(v); }} /> Apply Label Style Changes handlePickColor("stroke")}>Color {showColorPicker && (
setTempColor(c)} />
)}
); }