build: merge

ref: N25B-402
This commit is contained in:
JobvAlewijk
2026-01-02 19:56:01 +01:00
8 changed files with 258 additions and 1 deletions

View File

@@ -9,6 +9,7 @@ import {
import '@xyflow/react/dist/style.css';
import {useEffect, useState} from "react";
import {useShallow} from 'zustand/react/shallow';
import useProgramStore from "../../utils/programStore.ts";
import {DndToolbar} from './visualProgrammingUI/components/DragDropSidebar.tsx';
import useFlowStore from './visualProgrammingUI/VisProgStores.tsx';
import type {FlowState} from './visualProgrammingUI/VisProgTypes.tsx';
@@ -153,6 +154,10 @@ function runProgramm() {
).then((res) => {
if (!res.ok) throw new Error("Failed communicating with the backend.")
console.log("Successfully sent the program to the backend.");
// store reduced program in global program store for further use in the UI
// when the program was sent to the backend successfully:
useProgramStore.getState().setProgramState(structuredClone(program));
}).catch(() => console.log("Failed to send program to the backend."));
}

View File

@@ -45,7 +45,7 @@ const initialNodes : Node[] = [
createNode('start', 'start', {x: 100, y: 100}, {label: "Start"}, false),
createNode('end', 'end', {x: 500, y: 100}, {label: "End"}, false),
createNode('phase-1', 'phase', {x:200, y:100}, {label: "Phase 1", children : []}),
createNode('norms-1', 'norm', {x:-200, y:100}, {label: "Initial Norms", normList: ["Be a robot", "get good"]}),
createNode('norms-1', 'norm', {x:-200, y:100}, {label: "Initial Norms", normList: ["Be a robot", "get good"], critical:false}),
];
// * Initial edges * /

View File

@@ -8,4 +8,5 @@ export const NormNodeDefaults: NormNodeData = {
droppable: true,
norm: "",
hasReduce: true,
critical: false,
};

View File

@@ -21,6 +21,7 @@ export type NormNodeData = {
droppable: boolean;
norm: string;
hasReduce: boolean;
critical: boolean;
};
export type NormNode = Node<NormNodeData>
@@ -35,11 +36,16 @@ export default function NormNode(props: NodeProps<NormNode>) {
const {updateNodeData} = useFlowStore();
const text_input_id = `norm_${props.id}_text_input`;
const checkbox_id = `goal_${props.id}_checkbox`;
const setValue = (value: string) => {
updateNodeData(props.id, {norm: value});
}
const setCritical = (value: boolean) => {
updateNodeData(props.id, {...data, critical: value});
}
return <>
<Toolbar nodeId={props.id} allowDelete={true}/>
<div className={`${styles.defaultNode} ${styles.nodeNorm}`}>
@@ -52,6 +58,15 @@ export default function NormNode(props: NodeProps<NormNode>) {
placeholder={"Pepper should ..."}
/>
</div>
<div className={"flex-row gap-md align-center"}>
<label htmlFor={checkbox_id}>Critical:</label>
<input
id={checkbox_id}
type={"checkbox"}
checked={data.critical || false}
onChange={(e) => setCritical(e.target.checked)}
/>
</div>
<Handle type="source" position={Position.Right} id="norms"/>
</div>
</>;
@@ -69,6 +84,7 @@ export function NormReduce(node: Node, _nodes: Node[]) {
id: node.id,
label: data.label,
norm: data.norm,
critical: data.critical,
}
}

81
src/utils/programStore.ts Normal file
View File

@@ -0,0 +1,81 @@
import {create} from "zustand";
// the type of a reduced program
export type ReducedProgram = { phases: Record<string, unknown>[] };
/**
* the type definition of the programStore
*/
export type ProgramState = {
// Basic store functionality:
currentProgram: ReducedProgram;
setProgramState: (state: ReducedProgram) => void;
getProgramState: () => ReducedProgram;
// Utility functions:
// to avoid having to manually go through the entire state for every instance where data is required
getPhaseIds: () => string[];
getNormsInPhase: (currentPhaseId: string) => Record<string, unknown>[];
getGoalsInPhase: (currentPhaseId: string) => Record<string, unknown>[];
getTriggersInPhase: (currentPhaseId: string) => Record<string, unknown>[];
// if more specific utility functions are needed they can be added here:
}
/**
* the ProgramStore can be used to access all information of the most recently sent program,
* it contains basic functions to set and get the current program.
* And it contains some utility functions that allow you to easily gain access
* to the norms, triggers and goals of a specific phase.
*/
const useProgramStore = create<ProgramState>((set, get) => ({
currentProgram: { phases: [] as Record<string, unknown>[]},
/**
* sets the current program by cloning the provided program using a structuredClone
*/
setProgramState: (program: ReducedProgram) => set({currentProgram: structuredClone(program)}),
/**
* gets the current program
*/
getProgramState: () => get().currentProgram,
// utility functions:
/**
* gets the ids of all phases in the program
*/
getPhaseIds: () => get().currentProgram.phases.map(entry => entry["id"] as string),
/**
* gets the norms for the provided phase
*/
getNormsInPhase: (currentPhaseId) => {
const program = get().currentProgram;
const phase = program.phases.find(val => val["id"] === currentPhaseId);
if (phase) {
return phase["norms"] as Record<string, unknown>[];
}
throw new Error(`phase with id:"${currentPhaseId}" not found`)
},
/**
* gets the goals for the provided phase
*/
getGoalsInPhase: (currentPhaseId) => {
const program = get().currentProgram;
const phase = program.phases.find(val => val["id"] === currentPhaseId);
if (phase) {
return phase["goals"] as Record<string, unknown>[];
}
throw new Error(`phase with id:"${currentPhaseId}" not found`)
},
/**
* gets the triggers for the provided phase
*/
getTriggersInPhase: (currentPhaseId) => {
const program = get().currentProgram;
const phase = program.phases.find(val => val["id"] === currentPhaseId);
if (phase) {
return phase["triggers"] as Record<string, unknown>[];
}
throw new Error(`phase with id:"${currentPhaseId}" not found`)
}
}));
export default useProgramStore;

View File

@@ -98,6 +98,7 @@ describe('NormNode', () => {
droppable: true,
norm: '',
hasReduce: true,
critical: false
},
};
@@ -627,6 +628,54 @@ describe('NormNode', () => {
});
});
it('should properly update the store when editing critical checkbox', async () => {
const mockNode: Node = {
id: 'norm-1',
type: 'norm',
position: { x: 0, y: 0 },
data: {
label: 'Test Norm',
droppable: true,
norm: '',
hasReduce: true,
critical: false,
},
};
useFlowStore.setState({
nodes: [mockNode],
edges: [],
});
renderWithProviders(
<NormNode
id={mockNode.id}
type={mockNode.type as string}
data={mockNode.data as any}
selected={false}
isConnectable={true}
zIndex={0}
dragging={false}
selectable={true}
deletable={true}
draggable={true}
positionAbsoluteX={0}
positionAbsoluteY={0}
/>
);
const checkbox = screen.getByLabelText('Critical:');
await user.click(checkbox);
await waitFor(() => {
const state = useFlowStore.getState();
expect(state.nodes).toHaveLength(1);
expect(state.nodes[0].id).toBe('norm-1');
expect(state.nodes[0].data.norm).toBe('');
expect(state.nodes[0].data.critical).toBe(true);
});
});
it('should not affect other nodes when updating one norm node', async () => {
const norm1: Node = {
id: 'norm-1',

View File

@@ -2,6 +2,11 @@ import '@testing-library/jest-dom';
import { cleanup } from '@testing-library/react';
import useFlowStore from '../src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx';
if (!globalThis.structuredClone) {
globalThis.structuredClone = (obj: any) => {
return JSON.parse(JSON.stringify(obj));
};
}
// To make sure that the tests are working, it's important that you are using
// this implementation of ResizeObserver and DOMMatrixReadOnly

View File

@@ -0,0 +1,100 @@
import useProgramStore, {type ReducedProgram} from "../../src/utils/programStore.ts";
describe('useProgramStore', () => {
beforeEach(() => {
// Reset store before each test
useProgramStore.setState({
currentProgram: { phases: [] },
});
});
const mockProgram: ReducedProgram = {
phases: [
{
id: 'phase-1',
norms: [{ id: 'norm-1' }],
goals: [{ id: 'goal-1' }],
triggers: [{ id: 'trigger-1' }],
},
{
id: 'phase-2',
norms: [{ id: 'norm-2' }],
goals: [{ id: 'goal-2' }],
triggers: [{ id: 'trigger-2' }],
},
],
};
it('should set and get the program state', () => {
useProgramStore.getState().setProgramState(mockProgram);
const program = useProgramStore.getState().getProgramState();
expect(program).toEqual(mockProgram);
});
it('should return the ids of all phases in the program', () => {
useProgramStore.getState().setProgramState(mockProgram);
const phaseIds = useProgramStore.getState().getPhaseIds();
expect(phaseIds).toEqual(['phase-1', 'phase-2']);
});
it('should return all norms for a given phase', () => {
useProgramStore.getState().setProgramState(mockProgram);
const norms = useProgramStore.getState().getNormsInPhase('phase-1');
expect(norms).toEqual([{ id: 'norm-1' }]);
});
it('should return all goals for a given phase', () => {
useProgramStore.getState().setProgramState(mockProgram);
const goals = useProgramStore.getState().getGoalsInPhase('phase-2');
expect(goals).toEqual([{ id: 'goal-2' }]);
});
it('should return all triggers for a given phase', () => {
useProgramStore.getState().setProgramState(mockProgram);
const triggers = useProgramStore.getState().getTriggersInPhase('phase-1');
expect(triggers).toEqual([{ id: 'trigger-1' }]);
});
it('throws if phase does not exist when getting norms', () => {
useProgramStore.getState().setProgramState(mockProgram);
expect(() =>
useProgramStore.getState().getNormsInPhase('missing-phase')
).toThrow('phase with id:"missing-phase" not found');
});
it('throws if phase does not exist when getting goals', () => {
useProgramStore.getState().setProgramState(mockProgram);
expect(() =>
useProgramStore.getState().getGoalsInPhase('missing-phase')
).toThrow('phase with id:"missing-phase" not found');
});
it('throws if phase does not exist when getting triggers', () => {
useProgramStore.getState().setProgramState(mockProgram);
expect(() =>
useProgramStore.getState().getTriggersInPhase('missing-phase')
).toThrow('phase with id:"missing-phase" not found');
});
// this test should be at the bottom to avoid conflicts with the previous tests
it('should clone program state when setting it (no shared references should exist)', () => {
useProgramStore.getState().setProgramState(mockProgram);
const storedProgram = useProgramStore.getState().getProgramState();
// mutate original
(mockProgram.phases[0].norms as any[]).push({ id: 'norm-mutated' });
// store should NOT change
expect(storedProgram.phases[0]['norms']).toHaveLength(1);
});
});