build: merge
ref: N25B-402
This commit is contained in:
@@ -9,6 +9,7 @@ import {
|
|||||||
import '@xyflow/react/dist/style.css';
|
import '@xyflow/react/dist/style.css';
|
||||||
import {useEffect, useState} from "react";
|
import {useEffect, useState} from "react";
|
||||||
import {useShallow} from 'zustand/react/shallow';
|
import {useShallow} from 'zustand/react/shallow';
|
||||||
|
import useProgramStore from "../../utils/programStore.ts";
|
||||||
import {DndToolbar} from './visualProgrammingUI/components/DragDropSidebar.tsx';
|
import {DndToolbar} from './visualProgrammingUI/components/DragDropSidebar.tsx';
|
||||||
import useFlowStore from './visualProgrammingUI/VisProgStores.tsx';
|
import useFlowStore from './visualProgrammingUI/VisProgStores.tsx';
|
||||||
import type {FlowState} from './visualProgrammingUI/VisProgTypes.tsx';
|
import type {FlowState} from './visualProgrammingUI/VisProgTypes.tsx';
|
||||||
@@ -153,6 +154,10 @@ function runProgramm() {
|
|||||||
).then((res) => {
|
).then((res) => {
|
||||||
if (!res.ok) throw new Error("Failed communicating with the backend.")
|
if (!res.ok) throw new Error("Failed communicating with the backend.")
|
||||||
console.log("Successfully sent the program to 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."));
|
}).catch(() => console.log("Failed to send program to the backend."));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ const initialNodes : Node[] = [
|
|||||||
createNode('start', 'start', {x: 100, y: 100}, {label: "Start"}, false),
|
createNode('start', 'start', {x: 100, y: 100}, {label: "Start"}, false),
|
||||||
createNode('end', 'end', {x: 500, y: 100}, {label: "End"}, false),
|
createNode('end', 'end', {x: 500, y: 100}, {label: "End"}, false),
|
||||||
createNode('phase-1', 'phase', {x:200, y:100}, {label: "Phase 1", children : []}),
|
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 * /
|
// * Initial edges * /
|
||||||
|
|||||||
@@ -8,4 +8,5 @@ export const NormNodeDefaults: NormNodeData = {
|
|||||||
droppable: true,
|
droppable: true,
|
||||||
norm: "",
|
norm: "",
|
||||||
hasReduce: true,
|
hasReduce: true,
|
||||||
|
critical: false,
|
||||||
};
|
};
|
||||||
@@ -21,6 +21,7 @@ export type NormNodeData = {
|
|||||||
droppable: boolean;
|
droppable: boolean;
|
||||||
norm: string;
|
norm: string;
|
||||||
hasReduce: boolean;
|
hasReduce: boolean;
|
||||||
|
critical: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NormNode = Node<NormNodeData>
|
export type NormNode = Node<NormNodeData>
|
||||||
@@ -35,11 +36,16 @@ export default function NormNode(props: NodeProps<NormNode>) {
|
|||||||
const {updateNodeData} = useFlowStore();
|
const {updateNodeData} = useFlowStore();
|
||||||
|
|
||||||
const text_input_id = `norm_${props.id}_text_input`;
|
const text_input_id = `norm_${props.id}_text_input`;
|
||||||
|
const checkbox_id = `goal_${props.id}_checkbox`;
|
||||||
|
|
||||||
const setValue = (value: string) => {
|
const setValue = (value: string) => {
|
||||||
updateNodeData(props.id, {norm: value});
|
updateNodeData(props.id, {norm: value});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const setCritical = (value: boolean) => {
|
||||||
|
updateNodeData(props.id, {...data, critical: value});
|
||||||
|
}
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<Toolbar nodeId={props.id} allowDelete={true}/>
|
<Toolbar nodeId={props.id} allowDelete={true}/>
|
||||||
<div className={`${styles.defaultNode} ${styles.nodeNorm}`}>
|
<div className={`${styles.defaultNode} ${styles.nodeNorm}`}>
|
||||||
@@ -52,6 +58,15 @@ export default function NormNode(props: NodeProps<NormNode>) {
|
|||||||
placeholder={"Pepper should ..."}
|
placeholder={"Pepper should ..."}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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"/>
|
<Handle type="source" position={Position.Right} id="norms"/>
|
||||||
</div>
|
</div>
|
||||||
</>;
|
</>;
|
||||||
@@ -69,6 +84,7 @@ export function NormReduce(node: Node, _nodes: Node[]) {
|
|||||||
id: node.id,
|
id: node.id,
|
||||||
label: data.label,
|
label: data.label,
|
||||||
norm: data.norm,
|
norm: data.norm,
|
||||||
|
critical: data.critical,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
81
src/utils/programStore.ts
Normal file
81
src/utils/programStore.ts
Normal 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;
|
||||||
@@ -98,6 +98,7 @@ describe('NormNode', () => {
|
|||||||
droppable: true,
|
droppable: true,
|
||||||
norm: '',
|
norm: '',
|
||||||
hasReduce: true,
|
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 () => {
|
it('should not affect other nodes when updating one norm node', async () => {
|
||||||
const norm1: Node = {
|
const norm1: Node = {
|
||||||
id: 'norm-1',
|
id: 'norm-1',
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ import '@testing-library/jest-dom';
|
|||||||
import { cleanup } from '@testing-library/react';
|
import { cleanup } from '@testing-library/react';
|
||||||
import useFlowStore from '../src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx';
|
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
|
// To make sure that the tests are working, it's important that you are using
|
||||||
// this implementation of ResizeObserver and DOMMatrixReadOnly
|
// this implementation of ResizeObserver and DOMMatrixReadOnly
|
||||||
|
|||||||
100
test/utils/programStore.test.ts
Normal file
100
test/utils/programStore.test.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user