feat: added undo and redo functionality
This commit is contained in:
committed by
JobvAlewijk
parent
608bd54617
commit
5e22ed8806
@@ -10,6 +10,7 @@ import {
|
||||
} from '@xyflow/react';
|
||||
import type { FlowState } from './VisProgTypes';
|
||||
import { NodeDefaults, NodeConnects, NodeDeletes } from './NodeRegistry';
|
||||
import { UndoRedo } from "./EditorUndoRedo.ts";
|
||||
|
||||
|
||||
/**
|
||||
@@ -34,7 +35,7 @@ function createNode(id: string, type: string, position: XYPosition, data: Record
|
||||
return {...defaultData, ...newData}
|
||||
}
|
||||
|
||||
//* Initial nodes to populate the flow at startup.
|
||||
//* Initial nodes, created by using createNode. */
|
||||
const initialNodes : Node[] = [
|
||||
createNode('start', 'start', {x: 100, y: 100}, {label: "Start"}, false),
|
||||
createNode('end', 'end', {x: 500, y: 100}, {label: "End"}, false),
|
||||
@@ -42,7 +43,7 @@ const initialNodes : Node[] = [
|
||||
createNode('norms-1', 'norm', {x:-200, y:100}, {label: "Initial Norms", normList: ["Be a robot", "get good"]}),
|
||||
];
|
||||
|
||||
//* Initial edges to connect the startup nodes.
|
||||
// * Initial edges * /
|
||||
const initialEdges: Edge[] = [
|
||||
{ id: 'start-phase-1', source: 'start', target: 'phase-1' },
|
||||
{ id: 'phase-1-end', source: 'phase-1', target: 'end' },
|
||||
@@ -50,17 +51,17 @@ const initialEdges: Edge[] = [
|
||||
|
||||
|
||||
/**
|
||||
* How we have defined the functions for our FlowState.
|
||||
* We have the normal functionality of a default FlowState with some exceptions to account for extra functionality.
|
||||
* The biggest changes are in onConnect and onDelete, which we have given extra functionality based on the nodes defined functions.
|
||||
*
|
||||
* useFlowStore contains the implementation for all editor functionality
|
||||
* and stores the current state of the visual programming editor
|
||||
*
|
||||
* * Provides:
|
||||
* - Node and edge state management
|
||||
* - Node creation, deletion, and updates
|
||||
* - Custom connection handling via NodeConnects
|
||||
* - Edge reconnection handling
|
||||
* - Undo Redo functionality through custom middleware
|
||||
*/
|
||||
const useFlowStore = create<FlowState>((set, get) => ({
|
||||
const useFlowStore = create<FlowState>(UndoRedo((set, get) => ({
|
||||
nodes: initialNodes,
|
||||
edges: initialEdges,
|
||||
edgeReconnectSuccessful: true,
|
||||
@@ -68,8 +69,7 @@ const useFlowStore = create<FlowState>((set, get) => ({
|
||||
/**
|
||||
* Handles changes to nodes triggered by ReactFlow.
|
||||
*/
|
||||
onNodesChange: (changes) =>
|
||||
set({nodes: applyNodeChanges(changes, get().nodes)}),
|
||||
onNodesChange: (changes) => set({nodes: applyNodeChanges(changes, get().nodes)}),
|
||||
|
||||
/**
|
||||
* Handles changes to edges triggered by ReactFlow.
|
||||
@@ -81,28 +81,34 @@ const useFlowStore = create<FlowState>((set, get) => ({
|
||||
* Updates edges and calls the node-specific connection functions.
|
||||
*/
|
||||
onConnect: (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.
|
||||
const sourceNode = nodes.find((n) => n.id == connection.source);
|
||||
const 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;
|
||||
}
|
||||
get().pushSnapshot();
|
||||
|
||||
// We should find out how their data changes by calling their respective functions.
|
||||
const sourceConnectFunction = NodeConnects[sourceNode.type as keyof typeof NodeConnects]
|
||||
const targetConnectFunction = NodeConnects[targetNode.type as keyof typeof NodeConnects]
|
||||
|
||||
// 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 });
|
||||
},
|
||||
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.
|
||||
const sourceNode = nodes.find((n) => n.id == connection.source);
|
||||
const 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.
|
||||
const sourceConnectFunction = NodeConnects[sourceNode.type as keyof typeof NodeConnects]
|
||||
const targetConnectFunction = NodeConnects[targetNode.type as keyof typeof NodeConnects]
|
||||
|
||||
// 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 });
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles reconnecting an edge between nodes.
|
||||
@@ -112,19 +118,32 @@ const useFlowStore = create<FlowState>((set, get) => ({
|
||||
set({ edges: reconnectEdge(oldEdge, newConnection, get().edges) });
|
||||
},
|
||||
|
||||
onReconnectStart: () => set({ edgeReconnectSuccessful: false }),
|
||||
onReconnectStart: () => {
|
||||
get().pushSnapshot();
|
||||
set({ edgeReconnectSuccessful: false })
|
||||
},
|
||||
|
||||
/**
|
||||
* handles potential dropping (deleting) of an edge
|
||||
* if it is not reconnected to a node after detaching it
|
||||
*
|
||||
* @param _evt - the event
|
||||
* @param {{id: string}} edge - the described edge
|
||||
*/
|
||||
onReconnectEnd: (_evt, edge) => {
|
||||
if (!get().edgeReconnectSuccessful) {
|
||||
set({ edges: get().edges.filter((e) => e.id !== edge.id) });
|
||||
}
|
||||
set({ edgeReconnectSuccessful: true });
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Deletes a node by ID, respecting NodeDeletes rules.
|
||||
* Also removes all edges connected to that node.
|
||||
*/
|
||||
deleteNode: (nodeId) => {
|
||||
get().pushSnapshot();
|
||||
|
||||
// Let's find our node to check if they have a special deletion function
|
||||
const ourNode = get().nodes.find((n)=>n.id==nodeId);
|
||||
const ourFunction = Object.entries(NodeDeletes).find(([t])=>t==ourNode?.type)?.[1]
|
||||
@@ -135,7 +154,7 @@ const useFlowStore = create<FlowState>((set, get) => ({
|
||||
nodes: get().nodes.filter((n) => n.id !== nodeId),
|
||||
edges: get().edges.filter((e) => e.source !== nodeId && e.target !== nodeId),
|
||||
})}
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Replaces the entire nodes array in the store.
|
||||
@@ -151,6 +170,7 @@ const useFlowStore = create<FlowState>((set, get) => ({
|
||||
* Updates the data of a node by merging new data with existing data.
|
||||
*/
|
||||
updateNodeData: (nodeId, data) => {
|
||||
get().pushSnapshot();
|
||||
set({
|
||||
nodes: get().nodes.map((node) => {
|
||||
if (node.id === nodeId) {
|
||||
@@ -165,8 +185,15 @@ const useFlowStore = create<FlowState>((set, get) => ({
|
||||
* Adds a new node to the flow store.
|
||||
*/
|
||||
addNode: (node: Node) => {
|
||||
get().pushSnapshot();
|
||||
set({ nodes: [...get().nodes, node] });
|
||||
},
|
||||
}));
|
||||
|
||||
// undo redo default values
|
||||
past: [],
|
||||
future: [],
|
||||
isBatchAction: false,
|
||||
}))
|
||||
);
|
||||
|
||||
export default useFlowStore;
|
||||
|
||||
Reference in New Issue
Block a user