feat: added ReactFlow-based node graph #11
92
src/visualProgrammingUI/VisProgStores.tsx
Normal file
92
src/visualProgrammingUI/VisProgStores.tsx
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
import {
|
||||||
|
applyNodeChanges,
|
||||||
|
applyEdgeChanges,
|
||||||
|
addEdge,
|
||||||
|
reconnectEdge, type Edge, type Connection
|
||||||
|
} from '@xyflow/react';
|
||||||
|
|
||||||
|
import { type FlowState } from './VisProgTypes.tsx';
|
||||||
|
|
||||||
|
const initialNodes = [
|
||||||
|
{
|
||||||
|
id: 'start',
|
||||||
|
type: 'start',
|
||||||
|
position: {x: 0, y: 0},
|
||||||
|
data: {label: 'start'}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'genericPhase',
|
||||||
|
type: 'phase',
|
||||||
|
position: {x: 0, y: 150},
|
||||||
|
data: {label: 'Generic Phase', number: 1},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'end',
|
||||||
|
type: 'end',
|
||||||
|
position: {x: 0, y: 300},
|
||||||
|
data: {label: 'End'}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const initialEdges = [
|
||||||
|
{
|
||||||
|
id: 'start-end',
|
||||||
|
source: 'start',
|
||||||
|
target: 'end'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// this is our useStore hook that we can use in our components to get parts of the store and call actions
|
||||||
|
const useStore = create<FlowState>((set, get) => ({
|
||||||
|
nodes: initialNodes,
|
||||||
|
edges: initialEdges,
|
||||||
|
edgeReconnectSuccessful: true,
|
||||||
|
onNodesChange: (changes) => {
|
||||||
|
set({
|
||||||
|
nodes: applyNodeChanges(changes, get().nodes)
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onEdgesChange: (changes) => {
|
||||||
|
set({
|
||||||
|
edges: applyEdgeChanges(changes, get().edges)
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onConnect: (connection) => {
|
||||||
|
set({
|
||||||
|
edges: addEdge(connection, get().edges)
|
||||||
|
});
|
||||||
|
},
|
||||||
|
// handles attempted reconnections of a previously disconnected edge
|
||||||
|
onReconnect: (oldEdge: Edge, newConnection: Connection) => {
|
||||||
|
get().edgeReconnectSuccessful = true;
|
||||||
|
set({
|
||||||
|
edges: reconnectEdge(oldEdge, newConnection, get().edges)
|
||||||
|
});
|
||||||
|
},
|
||||||
|
// Handles initiation of reconnection of edges that are manually disconnected from a node
|
||||||
|
onReconnectStart: () => {
|
||||||
|
set({
|
||||||
|
edgeReconnectSuccessful: false
|
||||||
|
});
|
||||||
|
},
|
||||||
|
// Drops the edge from the set of edges, removing it from the flow, if no successful reconnection occurred
|
||||||
|
onReconnectEnd: (_: unknown, edge: { id: string; }) => {
|
||||||
|
if (!get().edgeReconnectSuccessful) {
|
||||||
|
set({
|
||||||
|
edges: get().edges.filter((e) => e.id !== edge.id),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
set({
|
||||||
|
edgeReconnectSuccessful: true
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setNodes: (nodes) => {
|
||||||
|
set({ nodes });
|
||||||
|
},
|
||||||
|
setEdges: (edges) => {
|
||||||
|
set({ edges });
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default useStore;
|
||||||
25
src/visualProgrammingUI/VisProgTypes.tsx
Normal file
25
src/visualProgrammingUI/VisProgTypes.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import {
|
||||||
|
type Edge,
|
||||||
|
type Node,
|
||||||
|
type OnNodesChange,
|
||||||
|
type OnEdgesChange,
|
||||||
|
type OnConnect,
|
||||||
|
type OnReconnect,
|
||||||
|
} from '@xyflow/react';
|
||||||
|
|
||||||
|
|
||||||
|
export type AppNode = Node;
|
||||||
|
|
||||||
|
export type FlowState = {
|
||||||
|
nodes: Node[];
|
||||||
|
edges: Edge[];
|
||||||
|
edgeReconnectSuccessful: boolean;
|
||||||
|
onNodesChange: OnNodesChange;
|
||||||
|
onEdgesChange: OnEdgesChange;
|
||||||
|
onConnect: OnConnect;
|
||||||
|
onReconnect: OnReconnect;
|
||||||
|
onReconnectStart: () => void;
|
||||||
|
onReconnectEnd: (_: unknown, edge: { id: string }) => void;
|
||||||
|
setNodes: (nodes: AppNode[]) => void;
|
||||||
|
setEdges: (edges: Edge[]) => void;
|
||||||
|
};
|
||||||
@@ -1,21 +1,13 @@
|
|||||||
import './VisProgUI.css'
|
import './VisProgUI.css'
|
||||||
|
|
||||||
import {
|
|
||||||
useCallback,
|
|
||||||
useRef
|
|
||||||
} from 'react';
|
|
||||||
import {
|
import {
|
||||||
Background,
|
Background,
|
||||||
Controls,
|
Controls,
|
||||||
ReactFlow,
|
ReactFlow,
|
||||||
ReactFlowProvider,
|
ReactFlowProvider,
|
||||||
useNodesState,
|
|
||||||
useEdgesState,
|
|
||||||
reconnectEdge,
|
|
||||||
addEdge,
|
|
||||||
MarkerType,
|
MarkerType,
|
||||||
type Edge,
|
Panel,
|
||||||
type Connection, Panel,
|
|
||||||
} from '@xyflow/react';
|
} from '@xyflow/react';
|
||||||
import '@xyflow/react/dist/style.css';
|
import '@xyflow/react/dist/style.css';
|
||||||
import {
|
import {
|
||||||
@@ -27,6 +19,10 @@ import {
|
|||||||
|
|
||||||
import { Sidebar } from './components/DragDropSidebar.tsx';
|
import { Sidebar } from './components/DragDropSidebar.tsx';
|
||||||
|
|
||||||
|
import useStore from "./VisProgStores.tsx";
|
||||||
|
import {useShallow} from "zustand/react/shallow";
|
||||||
|
import type {FlowState} from "./VisProgTypes.tsx";
|
||||||
|
|
||||||
// --| config starting params for flow |--
|
// --| config starting params for flow |--
|
||||||
|
|
||||||
|
|
||||||
@@ -39,28 +35,7 @@ const nodeTypes = {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
const initialNodes = [
|
|
||||||
{
|
|
||||||
id: 'start',
|
|
||||||
type: 'start',
|
|
||||||
position: {x: 0, y: 0},
|
|
||||||
data: {label: 'start'}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'genericPhase',
|
|
||||||
type: 'phase',
|
|
||||||
position: {x: 0, y: 150},
|
|
||||||
data: {label: 'Generic Phase', number: 1},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'end',
|
|
||||||
type: 'end',
|
|
||||||
position: {x: 0, y: 300},
|
|
||||||
data: {label: 'End'}
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const initialEdges = [{id: 'start-end', source: 'start', target: 'end'}];
|
|
||||||
|
|
||||||
const defaultEdgeOptions = {
|
const defaultEdgeOptions = {
|
||||||
type: 'default',
|
type: 'default',
|
||||||
@@ -70,38 +45,27 @@ const defaultEdgeOptions = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const selector = (state: FlowState) => ({
|
||||||
|
nodes: state.nodes,
|
||||||
|
edges: state.edges,
|
||||||
|
onNodesChange: state.onNodesChange,
|
||||||
|
onEdgesChange: state.onEdgesChange,
|
||||||
|
onConnect: state.onConnect,
|
||||||
|
onReconnectStart: state.onReconnectStart,
|
||||||
|
onReconnectEnd: state.onReconnectEnd,
|
||||||
|
onReconnect: state.onReconnect
|
||||||
|
});
|
||||||
|
|
||||||
// --| define ReactFlow editor |--
|
// --| define ReactFlow editor |--
|
||||||
|
|
||||||
const VisProgUI = ()=> {
|
const VisProgUI = ()=> {
|
||||||
const edgeReconnectSuccessful = useRef(true);
|
|
||||||
const [nodes, , onNodesChange] = useNodesState(initialNodes);
|
|
||||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
|
||||||
|
|
||||||
// handles connection of newly created edges
|
const { nodes, edges, onNodesChange, onEdgesChange, onConnect, onReconnect, onReconnectStart, onReconnectEnd } = useStore(
|
||||||
const onConnect = useCallback(
|
useShallow(selector),
|
||||||
(params: Edge | Connection) => setEdges((els) => addEdge(params, els)),
|
|
||||||
[setEdges],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// handles connection of newly created edges
|
||||||
|
|
||||||
// Handles initiation of reconnection of edges that are manually disconnected from a node
|
|
||||||
const onReconnectStart = useCallback(() => {
|
|
||||||
edgeReconnectSuccessful.current = false;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// handles attempted reconnections of a previously disconnected edge
|
|
||||||
const onReconnect = useCallback((oldEdge: Edge, newConnection: Connection) => {
|
|
||||||
edgeReconnectSuccessful.current = true;
|
|
||||||
setEdges((els) => reconnectEdge(oldEdge, newConnection, els));
|
|
||||||
}, [setEdges]);
|
|
||||||
|
|
||||||
// Drops the edge from the set of edges, removing it from the flow, if no successful reconnection occurred
|
|
||||||
const onReconnectEnd = useCallback((_: unknown, edge: { id: string; }) => {
|
|
||||||
if (!edgeReconnectSuccessful.current) {
|
|
||||||
setEdges((eds) => eds.filter((e) => e.id !== edge.id));
|
|
||||||
}
|
|
||||||
edgeReconnectSuccessful.current = true;
|
|
||||||
}, [setEdges]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{marginInline: 'auto',display: 'flex',justifySelf: 'center', padding:'10px', alignItems: 'center', width: '80vw', height: '80vh'}}>
|
<div style={{marginInline: 'auto',display: 'flex',justifySelf: 'center', padding:'10px', alignItems: 'center', width: '80vw', height: '80vh'}}>
|
||||||
@@ -113,11 +77,11 @@ const VisProgUI = ()=> {
|
|||||||
onNodesChange={onNodesChange}
|
onNodesChange={onNodesChange}
|
||||||
onEdgesChange={onEdgesChange}
|
onEdgesChange={onEdgesChange}
|
||||||
nodeTypes={nodeTypes}
|
nodeTypes={nodeTypes}
|
||||||
snapToGrid
|
|
||||||
onReconnect={onReconnect}
|
onReconnect={onReconnect}
|
||||||
onReconnectStart={onReconnectStart}
|
onReconnectStart={onReconnectStart}
|
||||||
onReconnectEnd={onReconnectEnd}
|
onReconnectEnd={onReconnectEnd}
|
||||||
onConnect={onConnect}
|
onConnect={onConnect}
|
||||||
|
snapToGrid
|
||||||
fitView
|
fitView
|
||||||
proOptions={{hideAttribution: true }}
|
proOptions={{hideAttribution: true }}
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user