Refactoring all nodes functionality into their own files, create a modular framework for the visual programming. #21

Merged
9828273 merged 15 commits from refactor/node-encapsulation into dev 2025-11-20 15:00:02 +00:00
12 changed files with 56 additions and 74 deletions
Showing only changes of commit f37df1c726 - Show all commits

View File

@@ -141,4 +141,4 @@ function VisProgPage() {
) )
} }
export default VisProgPage export default VisProgPage

View File

@@ -25,6 +25,7 @@ export const NodeTypes = {
/** /**
* The default functions of the nodes we have registered. * The default functions of the nodes we have registered.
* These are defined in the <node>.default.ts files.
*/ */
export const NodeDefaults = { export const NodeDefaults = {
start: StartNodeDefaults, start: StartNodeDefaults,

View File

@@ -47,6 +47,12 @@ const initialEdges: Edge[] = [
{ id: 'phase-1-end', source: 'phase-1', target: 'end' }, { id: 'phase-1-end', source: 'phase-1', target: 'end' },
]; ];
/**
* 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.
*/
const useFlowStore = create<FlowState>((set, get) => ({ const useFlowStore = create<FlowState>((set, get) => ({
nodes: initialNodes, nodes: initialNodes,
edges: initialEdges, edges: initialEdges,
@@ -56,7 +62,6 @@ const useFlowStore = create<FlowState>((set, get) => ({
set({nodes: applyNodeChanges(changes, get().nodes)}), set({nodes: applyNodeChanges(changes, get().nodes)}),
onEdgesChange: (changes) => set({ edges: applyEdgeChanges(changes, get().edges) }), 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) => { onConnect: (connection) => {
const edges = addEdge(connection, get().edges); const edges = addEdge(connection, get().edges);
const nodes = get().nodes; const nodes = get().nodes;

View File

@@ -47,12 +47,13 @@ function DraggableNode({ className, children, nodeType, onDrop }: DraggableNodeP
* addNode — adds a new node to the flow using the unified class-based system. * addNode — adds a new node to the flow using the unified class-based system.
* Keeps numbering logic for phase/norm nodes. * Keeps numbering logic for phase/norm nodes.
*/ */
function addNode(nodeType: keyof typeof NodeTypes, position: XYPosition) { function addNode(nodeType: keyof typeof NodeTypes, position: XYPosition) {
const { nodes, setNodes } = useFlowStore.getState(); const { nodes, setNodes } = useFlowStore.getState();
const defaultData = NodeDefaults[nodeType]
if (!defaultData) throw new Error(`Node type '${nodeType}' not found in registry`);
// Find out if there's any default data about our ndoe
const defaultData = NodeDefaults[nodeType] ?? {}
// Currently, we find out what the Id is by checking the last node and adding one
const sameTypeNodes = nodes.filter((node) => node.type === nodeType); const sameTypeNodes = nodes.filter((node) => node.type === nodeType);
const nextNumber = const nextNumber =
sameTypeNodes.length > 0 sameTypeNodes.length > 0
@@ -63,9 +64,9 @@ function DraggableNode({ className, children, nodeType, onDrop }: DraggableNodeP
return Number.isNaN(lastNum) ? sameTypeNodes.length + 1 : lastNum + 1; return Number.isNaN(lastNum) ? sameTypeNodes.length + 1 : lastNum + 1;
})() })()
: 1; : 1;
const id = `${nodeType}-${nextNumber}`; const id = `${nodeType}-${nextNumber}`;
// Create new node
const newNode = { const newNode = {
id: id, id: id,
type: nodeType, type: nodeType,
@@ -104,6 +105,7 @@ export function DndToolbar() {
); );
// Map over our default settings to see which of them have their droppable data set to true
const droppableNodes = Object.entries(NodeDefaults) const droppableNodes = Object.entries(NodeDefaults)
.filter(([, data]) => data.droppable) .filter(([, data]) => data.droppable)
.map(([type, data]) => ({ .map(([type, data]) => ({
@@ -111,20 +113,16 @@ export function DndToolbar() {
data data
})); }));
return ( return (
<div className={`flex-col gap-lg padding-md ${styles.innerDndPanel}`}> <div className={`flex-col gap-lg padding-md ${styles.innerDndPanel}`}>
<div className="description"> <div className="description">
You can drag these nodes to the pane to create new nodes. You can drag these nodes to the pane to create new nodes.
</div> </div>
<div className={`flex-row gap-lg ${styles.dndNodeContainer}`}> <div className={`flex-row gap-lg ${styles.dndNodeContainer}`}>
{ {/* Maps over all the nodes that are droppable, and puts them in the panel */}
// Maps over all the nodes that are droppable, and puts them in the panel
}
{droppableNodes.map(({type, data}) => ( {droppableNodes.map(({type, data}) => (
<DraggableNode <DraggableNode
className={styles[`draggable-node-${type}`]} className={styles[`draggable-node-${type}`]} // Our current style signature for nodes
nodeType={type} nodeType={type}
onDrop={handleNodeDrop} onDrop={handleNodeDrop}
> >

View File

@@ -54,7 +54,7 @@ export function EndReduce(node: Node, nodes: Node[]) {
} }
/** /**
* Any connection functionality that should get called when a connection is made to this node * Any connection functionality that should get called when a connection is made to this node type (end)
* @param thisNode the node of which the functionality gets called * @param thisNode the node of which the functionality gets called
* @param otherNode the other node which has connected * @param otherNode the other node which has connected
* @param isThisSource whether this node is the one that is the source of the connection * @param isThisSource whether this node is the one that is the source of the connection

View File

@@ -2,8 +2,6 @@ import {
Handle, Handle,
type NodeProps, type NodeProps,
Position, Position,
type Connection,
type Edge,
type Node, type Node,
} from '@xyflow/react'; } from '@xyflow/react';
import { Toolbar } from '../components/NodeComponents'; import { Toolbar } from '../components/NodeComponents';
@@ -26,15 +24,9 @@ export type GoalNodeData = {
hasReduce: boolean; hasReduce: boolean;
}; };
export type GoalNode = Node<GoalNodeData> export type GoalNode = Node<GoalNodeData>
export function GoalNodeCanConnect(connection: Connection | Edge): boolean {
return (connection != undefined);
}
/** /**
* Defines how a Goal node should be rendered * Defines how a Goal node should be rendered
* @param props NodeProps, like id, label, children * @param props NodeProps, like id, label, children

View File

@@ -2,8 +2,6 @@ import {
Handle, Handle,
type NodeProps, type NodeProps,
Position, Position,
type Connection,
type Edge,
type Node, type Node,
} from '@xyflow/react'; } from '@xyflow/react';
import { Toolbar } from '../components/NodeComponents'; import { Toolbar } from '../components/NodeComponents';
@@ -25,15 +23,8 @@ export type NormNodeData = {
hasReduce: boolean; hasReduce: boolean;
}; };
export type NormNode = Node<NormNodeData> export type NormNode = Node<NormNodeData>
export function NormNodeCanConnect(connection: Connection | Edge): boolean {
return (connection != undefined);
}
/** /**
* Defines how a Norm node should be rendered * Defines how a Norm node should be rendered
* @param props NodeProps, like id, label, children * @param props NodeProps, like id, label, children

View File

@@ -24,10 +24,8 @@ export type PhaseNodeData = {
hasReduce: boolean; hasReduce: boolean;
}; };
export type PhaseNode = Node<PhaseNodeData> export type PhaseNode = Node<PhaseNodeData>
/** /**
* Defines how a phase node should be rendered * Defines how a phase node should be rendered
* @param props NodeProps, like id, label, children * @param props NodeProps, like id, label, children
@@ -36,9 +34,7 @@ export type PhaseNode = Node<PhaseNodeData>
export default function PhaseNode(props: NodeProps<Node>) { export default function PhaseNode(props: NodeProps<Node>) {
const data = props.data as PhaseNodeData; const data = props.data as PhaseNodeData;
const {updateNodeData} = useFlowStore(); const {updateNodeData} = useFlowStore();
const updateLabel = (value: string) => updateNodeData(props.id, {...data, label: value}); const updateLabel = (value: string) => updateNodeData(props.id, {...data, label: value});
const label_input_id = `phase_${props.id}_label_input`; const label_input_id = `phase_${props.id}_label_input`;
return ( return (
@@ -62,10 +58,11 @@ export default function PhaseNode(props: NodeProps<Node>) {
); );
}; };
/** /**
* Reduces each phase, including its children down into its relevant data. * Reduces each phase, including its children down into its relevant data.
* @param props: The Node Properties of this node. * @param node the node which is being reduced
* @param nodes all the nodes currently in the flow.
* @returns A collection of all reduced nodes in this phase, starting with this phases' reduced data.
*/ */
export function PhaseReduce(node: Node, nodes: Node[]) { export function PhaseReduce(node: Node, nodes: Node[]) {
const thisnode = node as PhaseNode; const thisnode = node as PhaseNode;
@@ -104,7 +101,12 @@ export function PhaseReduce(node: Node, nodes: Node[]) {
return result; return result;
} }
/**
* This function is called whenever a connection is made with this node type (phase)
* @param thisNode the node of this node type which function is called
* @param otherNode the other node which was part of the connection
* @param isThisSource whether this instance of the node was the source in the connection, true = yes.
*/
export function PhaseConnects(thisNode: Node, otherNode: Node, isThisSource: boolean) { export function PhaseConnects(thisNode: Node, otherNode: Node, isThisSource: boolean) {
console.log("Connect functionality called.") console.log("Connect functionality called.")
const node = thisNode as PhaseNode const node = thisNode as PhaseNode

View File

@@ -41,6 +41,12 @@ export function StartReduce(node: Node, nodes: Node[]) {
} }
} }
/**
* This function is called whenever a connection is made with this node type (start)
* @param thisNode the node of this node type which function is called
* @param otherNode the other node which was part of the connection
* @param isThisSource whether this instance of the node was the source in the connection, true = yes.
*/
export function StartConnects(thisNode: Node, otherNode: Node, isThisSource: boolean) { export function StartConnects(thisNode: Node, otherNode: Node, isThisSource: boolean) {
// Replace this for connection logic // Replace this for connection logic
if (thisNode == undefined && otherNode == undefined && isThisSource == false) { if (thisNode == undefined && otherNode == undefined && isThisSource == false) {

View File

@@ -81,6 +81,12 @@ export function TriggerReduce(node: Node, nodes: Node[]) {
} }
} }
/**
* This function is called whenever a connection is made with this node type (trigger)
* @param thisNode the node of this node type which function is called
* @param otherNode the other node which was part of the connection
* @param isThisSource whether this instance of the node was the source in the connection, true = yes.
*/
export function TriggerConnects(thisNode: Node, otherNode: Node, isThisSource: boolean) { export function TriggerConnects(thisNode: Node, otherNode: Node, isThisSource: boolean) {
// Replace this for connection logic // Replace this for connection logic
if (thisNode == undefined && otherNode == undefined && isThisSource == false) { if (thisNode == undefined && otherNode == undefined && isThisSource == false) {
@@ -88,14 +94,13 @@ export function TriggerConnects(thisNode: Node, otherNode: Node, isThisSource: b
} }
} }
// Definitions for the possible triggers, being keywords and emotions
type Keyword = { id: string, keyword: string };
export type EmotionTriggerNodeProps = { export type EmotionTriggerNodeProps = {
type: "emotion"; type: "emotion";
value: string; value: string;
} }
type Keyword = { id: string, keyword: string };
export type KeywordTriggerNodeProps = { export type KeywordTriggerNodeProps = {
type: "keywords"; type: "keywords";
value: Keyword[]; value: Keyword[];
@@ -103,6 +108,11 @@ export type KeywordTriggerNodeProps = {
export type TriggerNodeProps = EmotionTriggerNodeProps | KeywordTriggerNodeProps; export type TriggerNodeProps = EmotionTriggerNodeProps | KeywordTriggerNodeProps;
/**
* The JSX element that is responsible for updating the field and showing the text
* @param param0 the function that updates the field
* @returns React.JSX.Element that handles adding keywords
*/
function KeywordAdder({ addKeyword }: { addKeyword: (keyword: string) => void }) { function KeywordAdder({ addKeyword }: { addKeyword: (keyword: string) => void }) {
const [input, setInput] = useState(""); const [input, setInput] = useState("");

View File

@@ -0,0 +1,5 @@
describe('not yet implemented', () => {
test('nothing yet', () => {
expect(true);
});
});

View File

@@ -1,33 +1,5 @@
// import { mockReactFlow } from '../../../../setupFlowTests.ts'; describe('Not implemented', () => {
// import {act} from "@testing-library/react"; test('nothing yet', () => {
// import useFlowStore from "../../../../../src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx"; expect(true)
// import {addNode} from "../../../../../src/pages/VisProgPage/visualProgrammingUI/components/DragDropSidebar.tsx"; });
});
// beforeAll(() => {
// mockReactFlow();
// });
// describe('Drag-and-Drop sidebar', () => {
// test.each(['phase', 'phase'])('new nodes get added correctly', (nodeType: string) => {
// act(()=> {
// addNode(nodeType, {x:100, y:100});
// })
// const updatedState = useFlowStore.getState();
// expect(updatedState.nodes.length).toBe(1);
// expect(updatedState.nodes[0].type).toBe(nodeType);
// });
// test.each(['phase', 'norm'])('new nodes get correct Id', (nodeType) => {
// act(()=> {
// addNode(nodeType, {x:100, y:100});
// addNode(nodeType, {x:100, y:100});
// })
// const updatedState = useFlowStore.getState();
// expect(updatedState.nodes.length).toBe(2);
// expect(updatedState.nodes[0].id).toBe(`${nodeType}-1`);
// expect(updatedState.nodes[1].id).toBe(`${nodeType}-2`);
// });
// test('throws error on unexpected node type', () => {
// expect(() => addNode('I do not Exist', {x:100, y:100})).toThrow("Node I do not Exist not found");
// })
// });