feat: added ReactFlow-based node graph #11
7
package-lock.json
generated
7
package-lock.json
generated
@@ -8,6 +8,7 @@
|
|||||||
"name": "pepperplus-ui",
|
"name": "pepperplus-ui",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@neodrag/react": "^2.3.1",
|
||||||
"@xyflow/react": "^12.8.6",
|
"@xyflow/react": "^12.8.6",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1"
|
"react-dom": "^19.1.1"
|
||||||
@@ -1006,6 +1007,12 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@neodrag/react": {
|
||||||
|
"version": "2.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@neodrag/react/-/react-2.3.1.tgz",
|
||||||
|
"integrity": "sha512-mOVefo3mFmaVLs9PB5F5wMXnnclG81qjOaPHyf8YZUnw/Ciz0pAqyJDwDJk0nPTIK5I2x1JdjXSchGNdCxZNRQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@nodelib/fs.scandir": {
|
"node_modules/@nodelib/fs.scandir": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@neodrag/react": "^2.3.1",
|
||||||
"@xyflow/react": "^12.8.6",
|
"@xyflow/react": "^12.8.6",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1"
|
"react-dom": "^19.1.1"
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
Background,
|
Background,
|
||||||
Controls,
|
Controls,
|
||||||
ReactFlow,
|
ReactFlow,
|
||||||
|
ReactFlowProvider,
|
||||||
useNodesState,
|
useNodesState,
|
||||||
useEdgesState,
|
useEdgesState,
|
||||||
reconnectEdge,
|
reconnectEdge,
|
||||||
@@ -23,6 +24,8 @@ import {
|
|||||||
PhaseNode
|
PhaseNode
|
||||||
} from "./components/NodeDefinitions.tsx";
|
} from "./components/NodeDefinitions.tsx";
|
||||||
|
|
||||||
|
import { Sidebar } from './components/DragDropSidebar.tsx';
|
||||||
|
|
||||||
const nodeTypes = {
|
const nodeTypes = {
|
||||||
start: StartNode,
|
start: StartNode,
|
||||||
end: EndNode,
|
end: EndNode,
|
||||||
@@ -59,7 +62,7 @@ const defaultEdgeOptions = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const VisualProgrammingUI = ()=> {
|
const VisProgUI = ()=> {
|
||||||
const edgeReconnectSuccessful = useRef(true);
|
const edgeReconnectSuccessful = useRef(true);
|
||||||
const [nodes, , onNodesChange] = useNodesState(initialNodes);
|
const [nodes, , onNodesChange] = useNodesState(initialNodes);
|
||||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||||
@@ -89,27 +92,41 @@ const VisualProgrammingUI = ()=> {
|
|||||||
}, [setEdges]);
|
}, [setEdges]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{outlineStyle: 'solid', borderRadius: '10pt' ,marginInline: 'auto',width: '60vw', height: '60vh' }}>
|
<div style={{marginInline: 'auto',display: 'flex',justifySelf: 'center', padding:'10px', alignItems: 'center', width: '80vw', height: '60vh'}}>
|
||||||
<ReactFlow
|
<div style={{outlineStyle: 'solid', borderRadius: '10pt', marginInline: '1em',width: '70%', height:'100%' }}>
|
||||||
nodes={nodes}
|
<ReactFlow
|
||||||
edges={edges}
|
nodes={nodes}
|
||||||
defaultEdgeOptions={defaultEdgeOptions}
|
edges={edges}
|
||||||
onNodesChange={onNodesChange}
|
defaultEdgeOptions={defaultEdgeOptions}
|
||||||
onEdgesChange={onEdgesChange}
|
onNodesChange={onNodesChange}
|
||||||
nodeTypes={nodeTypes}
|
onEdgesChange={onEdgesChange}
|
||||||
snapToGrid
|
nodeTypes={nodeTypes}
|
||||||
onReconnect={onReconnect}
|
snapToGrid
|
||||||
onReconnectStart={onReconnectStart}
|
onReconnect={onReconnect}
|
||||||
onReconnectEnd={onReconnectEnd}
|
onReconnectStart={onReconnectStart}
|
||||||
onConnect={onConnect}
|
onReconnectEnd={onReconnectEnd}
|
||||||
fitView
|
onConnect={onConnect}
|
||||||
proOptions={{hideAttribution: true }}
|
fitView
|
||||||
>
|
proOptions={{hideAttribution: true }}
|
||||||
<Controls />
|
>
|
||||||
<Background />
|
<Controls />
|
||||||
</ReactFlow>
|
<Background />
|
||||||
|
</ReactFlow>
|
||||||
|
</div>
|
||||||
|
<div style={{width: '20%', height: '100%'}}>
|
||||||
|
<Sidebar />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default VisualProgrammingUI
|
function VisualProgrammingUI(){
|
||||||
|
return (
|
||||||
|
<ReactFlowProvider>
|
||||||
|
<VisProgUI />
|
||||||
|
</ReactFlowProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default VisualProgrammingUI;
|
||||||
104
src/visualProgrammingUI/components/DragDropSidebar.tsx
Normal file
104
src/visualProgrammingUI/components/DragDropSidebar.tsx
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import { useDraggable } from '@neodrag/react';
|
||||||
|
import {
|
||||||
|
useReactFlow,
|
||||||
|
type XYPosition
|
||||||
|
} from '@xyflow/react';
|
||||||
|
import {
|
||||||
|
type ReactNode,
|
||||||
|
useCallback,
|
||||||
|
useRef,
|
||||||
|
useState
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
|
||||||
|
// improve later to create better automatic IDs
|
||||||
|
let id = 0;
|
||||||
|
const getId = () => `dndnode_${id++}`;
|
||||||
|
|
||||||
|
|
||||||
|
interface DraggableNodeProps {
|
||||||
|
className?: string;
|
||||||
|
children: ReactNode;
|
||||||
|
nodeType: string;
|
||||||
|
onDrop: (nodeType: string, position: XYPosition) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DraggableNode({ className, children, nodeType, onDrop }: DraggableNodeProps) {
|
||||||
|
const draggableRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [position, setPosition] = useState<XYPosition>({ x: 0, y: 0 });
|
||||||
|
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
useDraggable(draggableRef, {
|
||||||
|
position: position,
|
||||||
|
onDrag: ({ offsetX, offsetY }) => {
|
||||||
|
// Calculate position relative to the viewport
|
||||||
|
setPosition({
|
||||||
|
x: offsetX,
|
||||||
|
y: offsetY,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onDragEnd: ({ event }) => {
|
||||||
|
setPosition({ x: 0, y: 0 });
|
||||||
|
onDrop(nodeType, {
|
||||||
|
x: event.clientX,
|
||||||
|
y: event.clientY,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={className === "default" ? "default-node" : "default-node" + "__" + className} ref={draggableRef}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Sidebar() {
|
||||||
|
const { setNodes, screenToFlowPosition } = useReactFlow();
|
||||||
|
|
||||||
|
const handleNodeDrop = useCallback(
|
||||||
|
(nodeType: string, screenPosition: XYPosition) => {
|
||||||
|
const flow = document.querySelector('.react-flow');
|
||||||
|
const flowRect = flow?.getBoundingClientRect();
|
||||||
|
const isInFlow =
|
||||||
|
flowRect &&
|
||||||
|
screenPosition.x >= flowRect.left &&
|
||||||
|
screenPosition.x <= flowRect.right &&
|
||||||
|
screenPosition.y >= flowRect.top &&
|
||||||
|
screenPosition.y <= flowRect.bottom;
|
||||||
|
|
||||||
|
// Create a new node and add it to the flow
|
||||||
|
if (isInFlow) {
|
||||||
|
const position = screenToFlowPosition(screenPosition);
|
||||||
|
|
||||||
|
const newNode = {
|
||||||
|
id: getId(),
|
||||||
|
type: nodeType,
|
||||||
|
position,
|
||||||
|
data: { label: `${nodeType} node` },
|
||||||
|
};
|
||||||
|
|
||||||
|
setNodes((nds) => nds.concat(newNode));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setNodes, screenToFlowPosition],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<aside>
|
||||||
|
<div className="description">
|
||||||
|
You can drag these nodes to the pane to create new nodes.
|
||||||
|
</div>
|
||||||
|
<DraggableNode className="default" nodeType="start" onDrop={handleNodeDrop}>
|
||||||
|
start Node
|
||||||
|
</DraggableNode>
|
||||||
|
<DraggableNode className="default" nodeType="end" onDrop={handleNodeDrop}>
|
||||||
|
end Node
|
||||||
|
</DraggableNode>
|
||||||
|
<DraggableNode className="default" nodeType="phase" onDrop={handleNodeDrop}>
|
||||||
|
phase Node
|
||||||
|
</DraggableNode>
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user