refactor: Initial working framework of node encapsulation works- polymorphic implementation of nodes in creating and connecting calls correct functions

ref: N25B-294
This commit is contained in:
Björn Otgaar
2025-11-17 14:25:01 +01:00
parent b7eb0cb5ec
commit c5dc825ca3
13 changed files with 605 additions and 662 deletions

View File

@@ -1,142 +1,127 @@
import {create} from 'zustand';
import { create } from 'zustand';
import {
applyNodeChanges,
applyEdgeChanges,
addEdge,
reconnectEdge, type Edge, type Connection
reconnectEdge,
type Node,
type Edge,
type NodeChange,
type XYPosition,
} from '@xyflow/react';
import type { FlowState, AppNode } from './VisProgTypes';
import { NodeDefaults, NodeConnects } from './NodeRegistry';
import {type AppNode, type FlowState} from './VisProgTypes.tsx';
/**
* contains the nodes that are created when the editor is loaded,
* should contain at least a start and an end node
* Create a node given the correct data
* @param type
* @param id
* @param position
* @param data
* @constructor
*/
const initialNodes = [
{
id: 'start',
type: 'start',
position: {x: 0, y: 0},
data: {label: 'start'}
},
{
id: 'phase-1',
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'}
function createNode(id: string, type: string, position: XYPosition, data: any) {
const defaultData = Object.entries(NodeDefaults).find(([t, _]) => t == type)?.[1]
const newData = {
id: id,
type: type,
position: position,
data: data,
}
return (defaultData == undefined) ? newData : ({...defaultData, ...newData})
}
//* Initial nodes, created by using createNodeInstance. */
const initialNodes : Node[] = [
createNode('start', 'start', {x: 100, y: 100}, {label: "Start"}),
createNode('end', 'end', {x: 370, y: 100}, {label: "End"}),
createNode('phase-1', 'phase', {x:200, y:100}, {label: "Phase 1", children: ['end', 'start']}),
createNode('norms-1', 'norm', {x:-200, y:100}, {label: "Initial Norms", normList: ["Be a robot", "get good"]}),
];
/**
* contains the initial edges that are created when the editor is loaded
*/
const initialEdges = [
{
id: 'start-phase-1',
source: 'start',
target: 'phase-1',
},
{
id: 'phase-1-end',
source: 'phase-1',
target: 'end',
}
// * Initial edges * /
const initialEdges: Edge[] = [
{ id: 'start-phase-1', source: 'start', target: 'phase-1' },
{ id: 'phase-1-end', source: 'phase-1', target: 'end' },
];
/**
* The useFlowStore hook contains the implementation for editor functionality and state
* we can use this inside our editor component to access the current state
* and use any implemented functionality
*/
const useFlowStore = 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)
});
},
// handles connection of newly created edges
onNodesChange: (changes) =>
set({nodes: applyNodeChanges(changes, get().nodes)}),
onEdgesChange: (changes) => set({ edges: applyEdgeChanges(changes, get().edges) }),
// Let's make sure we tell the nodes when they're connected, and how it matters.
onConnect: (connection) => {
set({
edges: addEdge(connection, get().edges)
});
},
// handles attempted reconnections of a previously disconnected edge
onReconnect: (oldEdge: Edge, newConnection: Connection) => {
const edges = addEdge(connection, get().edges);
const nodes = get().nodes;
// connection has: { source, sourceHandle, target, targetHandle }
// Let's find the source and target ID's.
let sourceNode = nodes.find((n) => n.id == connection.source);
let targetNode = nodes.find((n) => n.id == connection.target);
// In case the nodes weren't found, return basic functionality.
if (sourceNode == undefined || targetNode == undefined || sourceNode.type == undefined || targetNode.type == undefined) {
set({ nodes, edges });
return;
}
// We should find out how their data changes by calling their respective functions.
let sourceConnectFunction = Object.entries(NodeConnects).find(([t, _]) => t == sourceNode.type)?.[1]
let targetConnectFunction = Object.entries(NodeConnects).find(([t, _]) => t == targetNode.type)?.[1]
if (sourceConnectFunction == undefined || targetConnectFunction == undefined) {
set({ nodes, edges });
return;
}
// We're going to have to update their data based on how they want to update it.
sourceConnectFunction(sourceNode, targetNode, true)
targetConnectFunction(targetNode, sourceNode, false)
set({ nodes, edges });
},
onReconnect: (oldEdge, newConnection) => {
get().edgeReconnectSuccessful = true;
set({
edges: reconnectEdge(oldEdge, newConnection, get().edges)
});
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; }) => {
onReconnectStart: () => set({ edgeReconnectSuccessful: false }),
onReconnectEnd: (_evt, edge) => {
if (!get().edgeReconnectSuccessful) {
set({
edges: get().edges.filter((e) => e.id !== edge.id),
});
set({ edges: get().edges.filter((e) => e.id !== edge.id) });
}
set({
edgeReconnectSuccessful: true
});
set({ edgeReconnectSuccessful: true });
},
deleteNode: (nodeId: string) => {
deleteNode: (nodeId) =>
set({
nodes: get().nodes.filter((n) => n.id !== nodeId),
edges: get().edges.filter((e) => e.source !== nodeId && e.target !== nodeId)
});
},
setNodes: (nodes) => {
set({nodes});
},
setEdges: (edges) => {
set({edges});
},
/**
* handles updating the data component of a node,
* if the provided data object contains entries that aren't present in the updated node's data component
* those entries are added to the data component,
* entries that do exist within the node's data component,
* are simply updated to contain the new value
*
* the data object
* @param {string} nodeId
* @param {object} data
*/
updateNodeData: (nodeId: string, data) => {
set({
nodes: get().nodes.map((node) : AppNode => {
if (node.id === nodeId) {
return {
...node,
data: {
...node.data,
...data
}
};
} else { return node; }
})
});
}
}),
);
edges: get().edges.filter((e) => e.source !== nodeId && e.target !== nodeId),
}),
export default useFlowStore;
setNodes: (nodes) => set({ nodes }),
setEdges: (edges) => set({ edges }),
updateNodeData: (nodeId, data) => {
set({
nodes: get().nodes.map((node) => {
if (node.id === nodeId) {
node.data = { ...node.data, ...data };
}
return node;
}),
});
},
addNode: (node: Node) => {
set({ nodes: [...get().nodes, node] });
},
}));
export default useFlowStore;