changed to controlled react state and added edit feature

pull/33/head
Khwaja 3 months ago
parent a463b8eb76
commit acd3b469bf

@ -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: {
...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: {
...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: {
...prev.centers,
[newName]: oldCenter,
}
[uniqueId]: { ...center, name: newName},
},
};
delete updatedData.centers[prevName];
return updatedData;
return next;
});
};

@ -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<string, CircularProduct>;
plants: Record<string, CircularPlant>;
centers: Record<string, CircularCenter>;
}
function getLayoutedNodesAndEdges(
function getLayouted(
nodes: Node<CustomNodeData>[],
edges: Edge[]
): { nodes: Node<CustomNodeData>[]; 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<PipelineBlockProps> = props => {
const nodes: Node<CustomNodeData>[] = [];
const edges: Edge[] = [];
const mapNameToType: Record<string, 'plant' | 'product' | 'center'> = {};
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<Record<string, 'plant'|'product'|'center'>>({});
const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const rebuild = useCallback(() => {
const m: Record<string, 'plant'|'product'|'center'> = {};
const newNodes: Node<CustomNodeData>[] = [];
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'
});
pl.inputs.forEach(input => {
newEdges.push({
id: `${input}-${key}-in`,
source: input,
target: key,
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 }
});
});
});
});
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 }
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'
});
}
center.output.forEach(out => {
edges.push({
id: `${key}-${out}-out`,
source: key,
target: out,
animated: true,
style: { stroke: 'black' },
markerEnd: { type: MarkerType.ArrowClosed }
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 }
});
});
});
});
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);
mapRef.current = m;
setNodes(newNodes);
setEdges(newEdges);
}, [
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, node: Node<CustomNodeData>) => {
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);
const onNodeDragStop = (_: any, n: Node<CustomNodeData>) => {
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<CustomNodeData>[]) => {
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);
});
},
[props]
);
const handleNodesDelete = useCallback((deleted: Node<CustomNodeData>[]) => {
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);
});
const onNodeDoubleClick = (ev: React.MouseEvent, node: Node<CustomNodeData>) => {
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]);
const onNodeDoubleClick = (_: React.MouseEvent, n: Node<CustomNodeData>) => {
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 (
<>
<Section title="Pipeline" />
<Card>
<div className={styles.PipelineBlock}>
<ReactFlow
<>
<Section title="Pipeline" />
<Card>
<div className={styles.PipelineBlock}>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onNodeDoubleClick={onNodeDoubleClick}
onNodeDragStop={onNodeDragStop}
onNodesDelete={handleNodesDelete}
deleteKeyCode="delete"
deleteKeyCode="Delete"
maxZoom={1.25}
minZoom={0.5}
snapToGrid
preventScrolling
nodeTypes={{ default: CustomNode }}
>
<Background />
<Controls showInteractive={false} />
</ReactFlow>
</div>
<div style={{ textAlign: 'center', marginTop: '1rem' }}>
<button style={{ margin: '0 8px' }} onClick={props.onAddProduct}>
Add product
</button>
<button style={{ margin: '0 8px' }} onClick={props.onAddPlant}>
Add plant
</button>
<button style={{ margin: '0 8px' }} onClick={props.onAddCenter}>
Add center
</button>
<button style={{ margin: '0 8px' }} onClick={onLayout}>
Auto Layout
</button>
<button
style={{ margin: '0 8px' }}
title="Drag from one connector to another to create links. Double click to rename. Click to move. Press Delete to remove."
>
?
</button>
</div>
</Card>
</>
>
<Background />
<Controls showInteractive={false} />
</ReactFlow>
</div>
<div style={{ textAlign: 'center', marginTop: '1rem' }}>
<button style={{ margin: '0 8px' }} onClick={props.onAddProduct}>Add product</button>
<button style={{ margin: '0 8px' }} onClick={props.onAddPlant}>Add plant</button>
<button style={{ margin: '0 8px' }} onClick={props.onAddCenter}>Add center</button>
<button style={{ margin: '0 8px' }} onClick={onLayout}>Auto Layout</button>
<button title="Drag & connect. Double-click to rename. Delete to remove." style={{ margin: '0 8px' }}>?</button>
</div>
</Card>
</>
);
};
export default PipelineBlock;
Loading…
Cancel
Save