Merge branch 'feat/monitoringpage-pim' of ssh://git.science.uu.nl/ics/sp/2025/n25b/pepperplus-ui into feat/monitoringpage-pim

This commit is contained in:
JobvAlewijk
2026-01-29 16:24:19 +01:00
7 changed files with 67 additions and 17 deletions

View File

@@ -119,7 +119,7 @@ const VisProgUI = () => {
<SaveLoadPanel></SaveLoadPanel> <SaveLoadPanel></SaveLoadPanel>
</Panel> </Panel>
<Panel position="bottom-center"> <Panel position="bottom-center">
<button onClick={() => undo()}>undo</button> <button onClick={() => undo()}>Undo</button>
<button onClick={() => redo()}>Redo</button> <button onClick={() => redo()}>Redo</button>
</Panel> </Panel>
<Controls/> <Controls/>
@@ -175,7 +175,7 @@ function VisProgPage() {
return ( return (
<> <>
<VisualProgrammingUI/> <VisualProgrammingUI/>
<button onClick={runProgram}>run program</button> <button onClick={runProgram}>Run Program</button>
</> </>
) )
} }

View File

@@ -108,3 +108,15 @@ export function useHandleRules(
return evaluateRules(targetRules, connection, context); return evaluateRules(targetRules, connection, context);
}; };
} }
export function validateConnectionWithRules(
connection: Connection,
context: ConnectionContext
): RuleResult {
const rules = useFlowStore.getState().getTargetRules(
connection.target!,
connection.targetHandle!
);
return evaluateRules(rules,connection, context);
}

View File

@@ -9,6 +9,7 @@ import {
type XYPosition, type XYPosition,
} from '@xyflow/react'; } from '@xyflow/react';
import '@xyflow/react/dist/style.css'; import '@xyflow/react/dist/style.css';
import {type ConnectionContext, validateConnectionWithRules} from "./HandleRuleLogic.ts";
import type { FlowState } from './VisProgTypes'; import type { FlowState } from './VisProgTypes';
import { import {
NodeDefaults, NodeDefaults,
@@ -129,7 +130,41 @@ const useFlowStore = create<FlowState>(UndoRedo((set, get) => ({
* Handles reconnecting an edge between nodes. * Handles reconnecting an edge between nodes.
*/ */
onReconnect: (oldEdge, newConnection) => { onReconnect: (oldEdge, newConnection) => {
get().edgeReconnectSuccessful = true;
function createContext(
source: {id: string, handleId: string},
target: {id: string, handleId: string}
) : ConnectionContext {
const edges = get().edges;
const targetConnections = edges.filter(edge => edge.target === target.id && edge.targetHandle === target.handleId).length
return {
connectionCount: targetConnections,
source: source,
target: target
}
}
// connection validation
const context: ConnectionContext = oldEdge.source === newConnection.source
? createContext({id: newConnection.source, handleId: newConnection.sourceHandle!}, {id: newConnection.target, handleId: newConnection.targetHandle!})
: createContext({id: newConnection.target, handleId: newConnection.targetHandle!}, {id: newConnection.source, handleId: newConnection.sourceHandle!});
const result = validateConnectionWithRules(
newConnection,
context
);
if (!result.isSatisfied) {
set({
edges: get().edges.map(e =>
e.id === oldEdge.id ? oldEdge : e
),
});
return;
}
// further reconnect logic
set({ edgeReconnectSuccessful: true });
set({ edges: reconnectEdge(oldEdge, newConnection, get().edges) }); set({ edges: reconnectEdge(oldEdge, newConnection, get().edges) });
// We make sure to perform any required data updates on the newly reconnected nodes // We make sure to perform any required data updates on the newly reconnected nodes

View File

@@ -10,6 +10,7 @@ import {allowOnlyConnectionsFromHandle} from "../HandleRules.ts";
import useFlowStore from '../VisProgStores.tsx'; import useFlowStore from '../VisProgStores.tsx';
import { TextField } from '../../../../components/TextField.tsx'; import { TextField } from '../../../../components/TextField.tsx';
import { MultilineTextField } from '../../../../components/MultilineTextField.tsx'; import { MultilineTextField } from '../../../../components/MultilineTextField.tsx';
import {noMatchingLeftRightBelief} from "./BeliefGlobals.ts";
/** /**
* The default data structure for a BasicBelief node * The default data structure for a BasicBelief node
@@ -191,6 +192,7 @@ export default function BasicBeliefNode(props: NodeProps<BasicBeliefNode>) {
</div> </div>
)} )}
<MultiConnectionHandle type="source" position={Position.Right} id="source" rules={[ <MultiConnectionHandle type="source" position={Position.Right} id="source" rules={[
noMatchingLeftRightBelief,
allowOnlyConnectionsFromHandle([{nodeType:"trigger",handleId:"TriggerBeliefs"}, {nodeType:"norm",handleId:"NormBeliefs"},{nodeType:"InferredBelief",handleId:"inferred_belief"}]), allowOnlyConnectionsFromHandle([{nodeType:"trigger",handleId:"TriggerBeliefs"}, {nodeType:"norm",handleId:"NormBeliefs"},{nodeType:"InferredBelief",handleId:"inferred_belief"}]),
]}/> ]}/>
</div> </div>

View File

@@ -5,7 +5,7 @@ import type { InferredBeliefNodeData } from "./InferredBeliefNode.tsx";
* Default data for this node * Default data for this node
*/ */
export const InferredBeliefNodeDefaults: InferredBeliefNodeData = { export const InferredBeliefNodeDefaults: InferredBeliefNodeData = {
label: "Inferred Belief", label: "AND/OR",
droppable: true, droppable: true,
inferredBelief: { inferredBelief: {
left: undefined, left: undefined,

View File

@@ -72,7 +72,7 @@ export default function TriggerNode(props: NodeProps<TriggerNode>) {
id="TriggerBeliefs" id="TriggerBeliefs"
style={{ left: '40%' }} style={{ left: '40%' }}
rules={[ rules={[
allowOnlyConnectionsFromType(['basic_belief', "inferred_belief"]), allowOnlyConnectionsFromType(['basic_belief']),
]} ]}
/> />
@@ -136,7 +136,7 @@ export function TriggerConnectionTarget(_thisNode: Node, _sourceNodeId: string)
const otherNode = nodes.find((x) => x.id === _sourceNodeId) const otherNode = nodes.find((x) => x.id === _sourceNodeId)
if (!otherNode) return; if (!otherNode) return;
if (otherNode.type === 'basic_belief'|| otherNode.type ==='inferred_belief') { if (otherNode.type === 'basic_belief' /* TODO: Add the option for an inferred belief */) {
data.condition = _sourceNodeId; data.condition = _sourceNodeId;
} }

View File

@@ -105,6 +105,8 @@ describe("SaveLoadPanel - combined tests", () => {
}); });
test("onLoad with invalid JSON does not update store", async () => { test("onLoad with invalid JSON does not update store", async () => {
const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
const file = new File(["not json"], "bad.json", { type: "application/json" }); const file = new File(["not json"], "bad.json", { type: "application/json" });
file.text = jest.fn(() => Promise.resolve(`{"bad json`)); file.text = jest.fn(() => Promise.resolve(`{"bad json`));
@@ -112,20 +114,19 @@ describe("SaveLoadPanel - combined tests", () => {
render(<SaveLoadPanel />); render(<SaveLoadPanel />);
const input = document.querySelector('input[type="file"]') as HTMLInputElement; const input = document.querySelector('input[type="file"]') as HTMLInputElement;
expect(input).toBeTruthy();
// Give some input
act(() => { act(() => {
fireEvent.change(input, { target: { files: [file] } }); fireEvent.change(input, { target: { files: [file] } });
}); });
await waitFor(() => { await waitFor(() => {
expect(window.alert).toHaveBeenCalledTimes(1); expect(window.alert).toHaveBeenCalledTimes(1);
const nodesAfter = useFlowStore.getState().nodes; const nodesAfter = useFlowStore.getState().nodes;
expect(nodesAfter).toHaveLength(0); expect(nodesAfter).toHaveLength(0);
expect(input.value).toBe("");
}); });
// Clean up the spy
consoleSpy.mockRestore();
}); });
test("onLoad resolves to null when no file is chosen (user cancels) and does not update store", async () => { test("onLoad resolves to null when no file is chosen (user cancels) and does not update store", async () => {