From acd3b469bf5e4dff169177f7569cd1a31877d050 Mon Sep 17 00:00:00 2001 From: Khwaja Date: Wed, 9 Jul 2025 11:06:06 -0500 Subject: [PATCH] changed to controlled react state and added edit feature --- .../components/CaseBuilder/CaseBuilder.tsx | 55 ++- .../components/CaseBuilder/PipelineBlock.tsx | 351 +++++++++--------- 2 files changed, 195 insertions(+), 211 deletions(-) diff --git a/web/src/components/CaseBuilder/CaseBuilder.tsx b/web/src/components/CaseBuilder/CaseBuilder.tsx index bc8fe34..7af685b 100644 --- a/web/src/components/CaseBuilder/CaseBuilder.tsx +++ b/web/src/components/CaseBuilder/CaseBuilder.tsx @@ -287,53 +287,50 @@ const onAddCenterOutput = (centerName: string, productName: string) => { }); }; - const onRenamePlant = (prevName: string, newName: string) => { + const onRenamePlant = (uniqueId: string, newName: string) => { setCircularData(prev => { - const oldPlant = prev.plants[prevName]; - if (!oldPlant) return prev; - - const updatedData: CircularData = { + const plant = prev.plants[uniqueId]; + if (!plant) return prev; + const next = { ...prev, - plants: { + plants: { ...prev.plants, - [prevName]: {...oldPlant, name: newName} - } + [uniqueId]: { ...plant, name: newName}, + }, }; + return next; + }); -}; + }; -const onRenameProduct = (prevName: string, newName: string) => { +const onRenameProduct = (uniqueId: string, newName: string) => { setCircularData(prev => { - const oldProduct = prev.products[prevName]; - if (!oldProduct) return prev; - - const updatedData: CircularData = { + const product = prev.products[uniqueId]; + if (!product) return prev; + const next = { ...prev, - products: { + products: { ...prev.products, - [newName]: oldProduct, - } + [uniqueId]: { ...product, name: newName}, + }, }; - delete updatedData.products[prevName]; - return updatedData; + return next; }); }; -const onRenameCenter = (prevName: string, newName: string) => { +const onRenameCenter = (uniqueId: string, newName: string) => { setCircularData(prev => { - const oldCenter = prev.centers[prevName]; - if (!oldCenter) return prev; - - const updatedData: CircularData = { + const center = prev.centers[uniqueId]; + if (!center) return prev; + const next = { ...prev, - centers: { + centers: { ...prev.centers, - [newName]: oldCenter, - } + [uniqueId]: { ...center, name: newName}, + }, }; - delete updatedData.centers[prevName]; - return updatedData; + return next; }); }; diff --git a/web/src/components/CaseBuilder/PipelineBlock.tsx b/web/src/components/CaseBuilder/PipelineBlock.tsx index b156742..34b869f 100644 --- a/web/src/components/CaseBuilder/PipelineBlock.tsx +++ b/web/src/components/CaseBuilder/PipelineBlock.tsx @@ -1,21 +1,14 @@ -import React, { useEffect, useCallback } from 'react'; +import React, { useEffect, useCallback, useRef } from 'react'; import dagre from 'dagre'; import { - ReactFlow, - Background, - Controls, - Node, - Edge, - Connection, - MarkerType -} from '@xyflow/react'; + ReactFlow, useNodesState, useEdgesState, Background, + Controls, Node, Edge, Connection, MarkerType } from '@xyflow/react'; import { CircularPlant, CircularProduct, CircularCenter } from './CircularData'; import CustomNode, { CustomNodeData } from './NodesAndEdges'; import Section from '../Common/Section'; import Card from '../Common/Card'; import styles from './PipelineBlock.module.css'; - interface PipelineBlockProps { onAddPlant: () => void; onAddProduct: () => void; @@ -30,223 +23,217 @@ interface PipelineBlockProps { onRemovePlant: (id: string) => void; onRemoveProduct: (id: string) => void; onRemoveCenter: (id: string) => void; - onRenameProduct: (prevName: string, newName: string) => void; - onRenamePlant: (prevName: string, newName: string) => void; - onRenameCenter: (prevName: string, newName: string) => void; + onRenameProduct: (uid: string, newName: string) => void; + onRenamePlant: (uid: string, newName: string) => void; + onRenameCenter: (uid: string, newName: string) => void; products: Record; plants: Record; centers: Record; } - -function getLayoutedNodesAndEdges( + +function getLayouted( nodes: Node[], edges: Edge[] ): { nodes: Node[]; edges: Edge[] } { - const NODE_WIDTH = 125; - const NODE_HEIGHT = 45; + const W = 125, H = 45; const g = new dagre.graphlib.Graph(); g.setDefaultEdgeLabel(() => ({})); g.setGraph({ rankdir: 'LR' }); - nodes.forEach(n => g.setNode(n.id, { width: NODE_WIDTH, height: NODE_HEIGHT })); + nodes.forEach(n => g.setNode(n.id, { width: W, height: H })); edges.forEach(e => g.setEdge(e.source, e.target)); dagre.layout(g); - const layouted = nodes.map(n => { - const d = g.node(n.id)!; - return { - ...n, - position: { - x: d.x - NODE_WIDTH / 2, - y: d.y - NODE_HEIGHT / 2 - } - }; - }); - return { nodes: layouted, edges }; + return { + nodes: nodes.map(n => { + const d = g.node(n.id)!; + return { ...n, position: { x: d.x - W/2, y: d.y - H/2 } }; + }), + edges + }; } - - const PipelineBlock: React.FC = props => { - const nodes: Node[] = []; - const edges: Edge[] = []; - const mapNameToType: Record = {}; - let hasNullPositions = false; - Object.entries(props.products).forEach(([key, product]) => { - if (!product.x || !product.y) hasNullPositions = true; - mapNameToType[key] = 'product'; - nodes.push({ - id: product.uid, - type: 'default', - data: { label: product.name, type: 'product' }, - position: { x: product.x, y: product.y }, - className: 'ProductNode' - }); - }); - Object.entries(props.plants).forEach(([key, plant]) => { - if (!plant.x || !plant.y) hasNullPositions = true; - mapNameToType[key] = 'plant'; - nodes.push({ - id: plant.uid, - type: 'default', - data: { label: plant.name, type: 'plant' }, - position: { x: plant.x, y: plant.y }, - className: 'PlantNode' - }); - plant.inputs.forEach(input => { - edges.push({ - id: `${input}-${key}-in`, - source: input, - target: key, - animated: true, - style: { stroke: 'black' }, - markerEnd: { type: MarkerType.ArrowClosed } + const mapRef = useRef>({}); + const [nodes, setNodes, onNodesChange] = useNodesState([]); + const [edges, setEdges, onEdgesChange] = useEdgesState([]); + + const rebuild = useCallback(() => { + const m: Record = {}; + const newNodes: Node[] = []; + const newEdges: Edge[] = []; + Object.entries(props.products).forEach(([key, p]) => { + m[key] = 'product'; + newNodes.push({ + id: p.uid, + type: 'default', + data: { label: p.name, type: 'product' }, + position: { x: p.x, y: p.y }, + className: 'ProductNode' }); }); - plant.outputs.forEach(output => { - edges.push({ - id: `${key}-${output}-out`, - source: key, - target: output, - animated: true, - style: { stroke: 'black' }, - markerEnd: { type: MarkerType.ArrowClosed } + + Object.entries(props.plants).forEach(([key, pl]) => { + m[key] = 'plant'; + newNodes.push({ + id: pl.uid, + type: 'default', + data: { label: pl.name, type: 'plant' }, + position: { x: pl.x, y: pl.y }, + className: 'PlantNode' }); - }); - }); - Object.entries(props.centers).forEach(([key, center]) => { - if (!center.x || !center.y) hasNullPositions = true; - mapNameToType[key] = 'center'; - nodes.push({ - id: center.uid, - type: 'default', - data: { label: center.name, type: 'center' }, - position: { x: center.x, y: center.y }, - className: 'CenterNode' - }); - if (center.input) { - edges.push({ - id: `${center.input}-${key}-in`, - source: center.input, - target: key, - animated: true, - style: { stroke: 'black' }, - markerEnd: { type: MarkerType.ArrowClosed } + + pl.inputs.forEach(input => { + newEdges.push({ + id: `${input}-${key}-in`, + source: input, + target: key, + animated: true, + style: { stroke: 'black' }, + markerEnd: { type: MarkerType.ArrowClosed } + }); }); - } - center.output.forEach(out => { - edges.push({ - id: `${key}-${out}-out`, - source: key, - target: out, - animated: true, - style: { stroke: 'black' }, - markerEnd: { type: MarkerType.ArrowClosed } + + pl.outputs.forEach(output => { + newEdges.push({ + id: `${key}-${output}-out`, + source: key, + target: output, + animated: true, + style: { stroke: 'black' }, + markerEnd: { type: MarkerType.ArrowClosed } + }); }); }); - }); - const onConnect = (params: Connection) => { - const { source, target } = params; - if (!source || !target) return; - const sourceType = mapNameToType[source]; - const targetType = mapNameToType[target]; - if (sourceType === 'product' && targetType === 'plant') props.onSetPlantInput(target, source); - else if (sourceType === 'plant' && targetType === 'product') props.onAddPlantOutput(source, target); - else if (sourceType === 'product' && targetType === 'center') props.onAddCenterInput(target, source); - else if (sourceType === 'center' && targetType === 'product') props.onAddCenterOutput(source, target); - }; - const onNodeDragStop = (_: any, node: Node) => { - const { id, position, data } = node; - if (data.type === 'plant') props.onMovePlant(id, position.x, position.y); - if (data.type === 'product') props.onMoveProduct(id, position.x, position.y); - if (data.type === 'center') props.onMoveCenter(id, position.x, position.y); - }; + Object.entries(props.centers).forEach(([key, c]) => { + m[key] = 'center'; + newNodes.push({ + id: c.uid, + type: 'default', + data: { label: c.name, type: 'center' }, + position: { x: c.x, y: c.y }, + className: 'CenterNode' + }); - const handleNodesDelete = useCallback( - (deleted: Node[]) => { - deleted.forEach(n => { - const type = mapNameToType[n.id]; - if (type === 'plant') props.onRemovePlant(n.id); - else if (type === 'product') props.onRemoveProduct(n.id); - else if (type === 'center') props.onRemoveCenter(n.id); + if (c.input) { + newEdges.push({ + id: `${c.input}-${key}-in`, + source: c.input, + target: key, + animated: true, + style: { stroke: 'black' }, + markerEnd: { type: MarkerType.ArrowClosed } + }); + + } + + c.output.forEach(o => { + newEdges.push({ + id: `${key}-${o}-out`, + source: key, + target: o, + animated: true, + style: { stroke: 'black' }, + markerEnd: { type: MarkerType.ArrowClosed } + }); }); - }, - [props] - ); + }); + mapRef.current = m; + setNodes(newNodes); + setEdges(newEdges); + }, [ - const onNodeDoubleClick = (ev: React.MouseEvent, node: Node) => { - const oldName = node.data.label; - const newName = window.prompt("Enter new name", oldName); - if (!newName|| newName.trim().length === 0) return; - if (newName in mapNameToType) return; - if (node.data.type === "plant") { - props.onRenamePlant(oldName, newName); - } else if (node.data.type === "product") { - props.onRenameProduct(oldName, newName); - } - else if (node.data.type === "center") { - props.onRenameCenter(oldName, newName); - } + props.products, + props.plants, + props.centers, + setNodes, + setEdges + ]); + + useEffect(() => { rebuild(); }, [rebuild]); + const onConnect = (c: Connection) => { + const s = c.source!, t = c.target!; + const st = mapRef.current[s], tt = mapRef.current[t]; + if (st==='product' && tt==='plant') props.onSetPlantInput(t, s); + else if (st==='plant' && tt==='product') props.onAddPlantOutput(s, t); + else if (st==='product' && tt==='center') props.onAddCenterInput(t, s); + else if (st==='center' && tt==='product') props.onAddCenterOutput(s, t); }; + + const onNodeDragStop = (_: any, n: Node) => { + const { id, position, data } = n; + if (data.type==='plant') props.onMovePlant(id, position.x, position.y); + if (data.type==='product') props.onMoveProduct(id, position.x, position.y); + if (data.type==='center') props.onMoveCenter(id, position.x, position.y); + }; + + const handleNodesDelete = useCallback((deleted: Node[]) => { + deleted.forEach(n => { + const t = mapRef.current[n.id]; + if (t==='plant') props.onRemovePlant(n.id); + if (t==='product') props.onRemoveProduct(n.id); + if (t==='center') props.onRemoveCenter(n.id); + }); + }, [props]); + + const onNodeDoubleClick = (_: React.MouseEvent, n: Node) => { + const oldName = n.data.label; + const newName = window.prompt('Enter new name', oldName); + console.log('after rename', newName); + const uniqueId = n.id; + if (!newName || newName===oldName) return; + if (n.data.type==='plant') props.onRenamePlant(uniqueId, newName); + if (n.data.type==='product') props.onRenameProduct(uniqueId, newName); + if (n.data.type==='center') props.onRenameCenter(uniqueId, newName); + }; + const onLayout = () => { - const { nodes: ln, edges: le } = getLayoutedNodesAndEdges(nodes, edges); + const { nodes: ln, edges: le } = getLayouted(nodes, edges); ln.forEach(n => { const { id, position, data } = n; - if (data.type === 'plant') props.onMovePlant(id, position.x, position.y); - else if (data.type === 'product') props.onMoveProduct(id, position.x, position.y); + if (data.type==='plant') props.onMovePlant(id, position.x, position.y); + else if (data.type==='product') props.onMoveProduct(id, position.x, position.y); else props.onMoveCenter(id, position.x, position.y); }); }; - - useEffect(() => { - if (hasNullPositions) onLayout(); - }, [hasNullPositions]); - + return ( - <> -
- -
- +
+ +
+ - - - -
-
- - - - - -
-
- +> + + + +
+
+ + + + + +
+
+ + ); }; + export default PipelineBlock; + \ No newline at end of file