diff --git a/src/pages/VisProgPage/VisProg.tsx b/src/pages/VisProgPage/VisProg.tsx index ee9df14..919e1af 100644 --- a/src/pages/VisProgPage/VisProg.tsx +++ b/src/pages/VisProgPage/VisProg.tsx @@ -119,7 +119,7 @@ const VisProgUI = () => { - + @@ -175,7 +175,7 @@ function VisProgPage() { return ( <> - + ) } diff --git a/src/pages/VisProgPage/visualProgrammingUI/HandleRuleLogic.ts b/src/pages/VisProgPage/visualProgrammingUI/HandleRuleLogic.ts index 427542a..e212ed2 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/HandleRuleLogic.ts +++ b/src/pages/VisProgPage/visualProgrammingUI/HandleRuleLogic.ts @@ -107,4 +107,16 @@ export function useHandleRules( // finally we return a function that evaluates all rules using the created 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); } \ No newline at end of file diff --git a/src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx b/src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx index defa934..2831748 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx +++ b/src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx @@ -9,6 +9,7 @@ import { type XYPosition, } from '@xyflow/react'; import '@xyflow/react/dist/style.css'; +import {type ConnectionContext, validateConnectionWithRules} from "./HandleRuleLogic.ts"; import type { FlowState } from './VisProgTypes'; import { NodeDefaults, @@ -129,7 +130,41 @@ const useFlowStore = create(UndoRedo((set, get) => ({ * Handles reconnecting an edge between nodes. */ 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) }); // We make sure to perform any required data updates on the newly reconnected nodes @@ -188,7 +223,7 @@ const useFlowStore = create(UndoRedo((set, get) => ({ // Let's find our node to check if they have a special deletion function const ourNode = get().nodes.find((n)=>n.id==nodeId); const ourFunction = Object.entries(NodeDeletes).find(([t])=>t==ourNode?.type)?.[1] - + // If there's no function, OR, our function tells us we can delete it, let's do so... if (ourFunction == undefined || ourFunction()) { set({ diff --git a/src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode.tsx b/src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode.tsx index dfb9420..03664e2 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode.tsx +++ b/src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode.tsx @@ -10,6 +10,7 @@ import {allowOnlyConnectionsFromHandle} from "../HandleRules.ts"; import useFlowStore from '../VisProgStores.tsx'; import { TextField } from '../../../../components/TextField.tsx'; import { MultilineTextField } from '../../../../components/MultilineTextField.tsx'; +import {noMatchingLeftRightBelief} from "./BeliefGlobals.ts"; /** * The default data structure for a BasicBelief node @@ -191,7 +192,8 @@ export default function BasicBeliefNode(props: NodeProps) { )} diff --git a/src/pages/VisProgPage/visualProgrammingUI/nodes/InferredBeliefNode.default.ts b/src/pages/VisProgPage/visualProgrammingUI/nodes/InferredBeliefNode.default.ts index 71f9f6b..976ee16 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/nodes/InferredBeliefNode.default.ts +++ b/src/pages/VisProgPage/visualProgrammingUI/nodes/InferredBeliefNode.default.ts @@ -5,7 +5,7 @@ import type { InferredBeliefNodeData } from "./InferredBeliefNode.tsx"; * Default data for this node */ export const InferredBeliefNodeDefaults: InferredBeliefNodeData = { - label: "Inferred Belief", + label: "AND/OR", droppable: true, inferredBelief: { left: undefined, diff --git a/src/pages/VisProgPage/visualProgrammingUI/nodes/TriggerNode.tsx b/src/pages/VisProgPage/visualProgrammingUI/nodes/TriggerNode.tsx index ea8d350..3ca0f7a 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/nodes/TriggerNode.tsx +++ b/src/pages/VisProgPage/visualProgrammingUI/nodes/TriggerNode.tsx @@ -50,9 +50,9 @@ export default function TriggerNode(props: NodeProps) { const setName= (value: string) => { updateNodeData(props.id, {...data, name: value}) } - + return <> - +
) { type="target" position={Position.Bottom} id="TriggerBeliefs" - style={{ left: '40%' }} + style={{ left: '40%' }} 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) 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; } @@ -172,7 +172,7 @@ export function TriggerDisconnectionTarget(_thisNode: Node, _sourceNodeId: strin const data = _thisNode.data as TriggerNodeData; // remove if the target of disconnection was our condition if (_sourceNodeId == data.condition) data.condition = undefined - + data.plan = deleteGoalInPlanByID(structuredClone(data.plan) as Plan, _sourceNodeId) } diff --git a/test/pages/visProgPage/visualProgrammingUI/components/SaveLoadPanel.test.tsx b/test/pages/visProgPage/visualProgrammingUI/components/SaveLoadPanel.test.tsx index 9d85323..65458c9 100644 --- a/test/pages/visProgPage/visualProgrammingUI/components/SaveLoadPanel.test.tsx +++ b/test/pages/visProgPage/visualProgrammingUI/components/SaveLoadPanel.test.tsx @@ -105,6 +105,8 @@ describe("SaveLoadPanel - combined tests", () => { }); 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" }); file.text = jest.fn(() => Promise.resolve(`{"bad json`)); @@ -112,20 +114,19 @@ describe("SaveLoadPanel - combined tests", () => { render(); const input = document.querySelector('input[type="file"]') as HTMLInputElement; - expect(input).toBeTruthy(); - - // Give some input + act(() => { fireEvent.change(input, { target: { files: [file] } }); }); await waitFor(() => { expect(window.alert).toHaveBeenCalledTimes(1); - const nodesAfter = useFlowStore.getState().nodes; 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 () => {