feat: made (reduced) program data available on all pages
This commit is contained in:
committed by
JobvAlewijk
parent
faaf67138d
commit
bd93b04bfd
@@ -9,6 +9,7 @@ import {
|
|||||||
import '@xyflow/react/dist/style.css';
|
import '@xyflow/react/dist/style.css';
|
||||||
import {useEffect} from "react";
|
import {useEffect} 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';
|
||||||
@@ -152,6 +153,10 @@ function runProgram() {
|
|||||||
).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."));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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;
|
||||||
@@ -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
|
||||||
|
|||||||
116
test/utils/programStore.test.ts
Normal file
116
test/utils/programStore.test.ts
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
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');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clone program state when setting it (no shared references should exist)', () => {
|
||||||
|
const changeableMockProgram: 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' }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
useProgramStore.getState().setProgramState(changeableMockProgram);
|
||||||
|
|
||||||
|
const storedProgram = useProgramStore.getState().getProgramState();
|
||||||
|
|
||||||
|
// mutate original
|
||||||
|
(changeableMockProgram.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