diff --git a/package-lock.json b/package-lock.json
index a52343e..cb39172 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,6 +10,7 @@
"dependencies": {
"@neodrag/react": "^2.3.1",
"@xyflow/react": "^12.8.6",
+ "clsx": "^2.1.1",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-router": "^7.9.3",
@@ -3971,6 +3972,15 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/co": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
@@ -6945,9 +6955,9 @@
}
},
"node_modules/react-router": {
- "version": "7.9.3",
- "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.3.tgz",
- "integrity": "sha512-4o2iWCFIwhI/eYAIL43+cjORXYn/aRQPgtFRRZb3VzoyQ5Uej0Bmqj7437L97N9NJW4wnicSwLOLS+yCXfAPgg==",
+ "version": "7.12.0",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.12.0.tgz",
+ "integrity": "sha512-kTPDYPFzDVGIIGNLS5VJykK0HfHLY5MF3b+xj0/tTyNYL1gF1qs7u67Z9jEhQk2sQ98SUaHxlG31g1JtF7IfVw==",
"license": "MIT",
"dependencies": {
"cookie": "^1.0.1",
diff --git a/package.json b/package.json
index 45f66df..ea362b7 100644
--- a/package.json
+++ b/package.json
@@ -14,6 +14,7 @@
"dependencies": {
"@neodrag/react": "^2.3.1",
"@xyflow/react": "^12.8.6",
+ "clsx": "^2.1.1",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-router": "^7.9.3",
diff --git a/src/pages/VisProgPage/VisProg.tsx b/src/pages/VisProgPage/VisProg.tsx
index 753dcbe..3df70fb 100644
--- a/src/pages/VisProgPage/VisProg.tsx
+++ b/src/pages/VisProgPage/VisProg.tsx
@@ -4,7 +4,7 @@ import {
Panel,
ReactFlow,
ReactFlowProvider,
- MarkerType,
+ MarkerType, getOutgoers
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import {type CSSProperties, useEffect, useState} from "react";
@@ -12,6 +12,8 @@ import {useShallow} from 'zustand/react/shallow';
import orderPhaseNodeArray from "../../utils/orderPhaseNodes.ts";
import useProgramStore from "../../utils/programStore.ts";
import {DndToolbar} from './visualProgrammingUI/components/DragDropSidebar.tsx';
+import {type EditorWarning, globalWarning} from "./visualProgrammingUI/components/EditorWarnings.tsx";
+import {WarningsSidebar} from "./visualProgrammingUI/components/WarningSidebar.tsx";
import type {PhaseNode} from "./visualProgrammingUI/nodes/PhaseNode.tsx";
import useFlowStore from './visualProgrammingUI/VisProgStores.tsx';
import type {FlowState} from './visualProgrammingUI/VisProgTypes.tsx';
@@ -89,9 +91,31 @@ const VisProgUI = () => {
window.addEventListener('keydown', handler);
return () => window.removeEventListener('keydown', handler);
});
+ const {unregisterWarning, registerWarning} = useFlowStore();
+ useEffect(() => {
+
+ if (checkPhaseChain()) {
+ unregisterWarning(globalWarning,'INCOMPLETE_PROGRAM');
+ } else {
+ // create global warning for incomplete program chain
+ const incompleteProgramWarning : EditorWarning = {
+ scope: {
+ id: globalWarning,
+ handleId: undefined
+ },
+ type: 'INCOMPLETE_PROGRAM',
+ severity: "ERROR",
+ description: "there is no complete phase chain from the startNode to the EndNode"
+ }
+
+ registerWarning(incompleteProgramWarning);
+ }
+ },[edges, registerWarning, unregisterWarning])
+
+
return (
-
+
{
snapToGrid
fitView
proOptions={{hideAttribution: true}}
+ style={{flexGrow: 3}}
>
{/* contains the drag and drop panel for nodes */}
@@ -120,11 +145,13 @@ const VisProgUI = () => {
+
+
);
};
@@ -179,6 +206,24 @@ function graphReducer() {
});
}
+const checkPhaseChain = (): boolean => {
+ const {nodes, edges} = useFlowStore.getState();
+
+ function checkForCompleteChain(currentNodeId: string): boolean {
+ const outgoingPhases = getOutgoers({id: currentNodeId}, nodes, edges)
+ .filter(node => ["end", "phase"].includes(node.type!));
+
+ if (outgoingPhases.length === 0) return false;
+ if (outgoingPhases.some(node => node.type === "end" )) return true;
+
+ const next = outgoingPhases.map(node => checkForCompleteChain(node.id))
+ .find(result => result);
+ console.log(next);
+ return !!next;
+ }
+
+ return checkForCompleteChain('start');
+};
/**
@@ -187,10 +232,19 @@ function graphReducer() {
* @constructor
*/
function VisProgPage() {
+ const [programValidity, setProgramValidity] = useState
(true);
+ const {isProgramValid, severityIndex} = useFlowStore();
+
+ useEffect(() => {
+ setProgramValidity(isProgramValid);
+ // the following eslint disable is required as it wants us to use all possible dependencies for the useEffect statement,
+ // however this would cause unneeded updates
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [severityIndex]);
return (
<>
-
+
>
)
}
diff --git a/src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx b/src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx
index 2831748..20306d9 100644
--- a/src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx
+++ b/src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx
@@ -10,6 +10,7 @@ import {
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import {type ConnectionContext, validateConnectionWithRules} from "./HandleRuleLogic.ts";
+import {editorWarningRegistry} from "./components/EditorWarnings.tsx";
import type { FlowState } from './VisProgTypes';
import {
NodeDefaults,
@@ -50,7 +51,7 @@ function createNode(id: string, type: string, position: XYPosition, data: Record
const endNode = createNode('end', 'end', {x: 590, y: 100}, {label: "End"}, false)
const initialPhaseNode = createNode(crypto.randomUUID(), 'phase', {x:235, y:100}, {label: "Phase 1", children : [], isFirstPhase: false, nextPhaseId: null})
- const initialNodes : Node[] = [startNode, endNode, initialPhaseNode,];
+ const initialNodes : Node[] = [startNode, endNode, initialPhaseNode];
// Initial edges, leave empty as setting initial edges...
// ...breaks logic that is dependent on connection events
@@ -341,8 +342,12 @@ const useFlowStore = create(UndoRedo((set, get) => ({
})
return { ruleRegistry: registry };
})
- }
+ },
+ ...editorWarningRegistry(get, set),
}))
);
+
+
export default useFlowStore;
+
diff --git a/src/pages/VisProgPage/visualProgrammingUI/VisProgTypes.tsx b/src/pages/VisProgPage/visualProgrammingUI/VisProgTypes.tsx
index 8ae3cad..afb1024 100644
--- a/src/pages/VisProgPage/visualProgrammingUI/VisProgTypes.tsx
+++ b/src/pages/VisProgPage/visualProgrammingUI/VisProgTypes.tsx
@@ -9,6 +9,7 @@ import type {
OnEdgesDelete,
OnNodesDelete
} from '@xyflow/react';
+import type {EditorWarningRegistry} from "./components/EditorWarnings.tsx";
import type {HandleRule} from "./HandleRuleLogic.ts";
import type { NodeTypes } from './NodeRegistry';
import type {FlowSnapshot} from "./EditorUndoRedo.ts";
@@ -94,7 +95,7 @@ export type FlowState = {
* @param node - the Node object to add
*/
addNode: (node: Node) => void;
-} & UndoRedoState & HandleRuleRegistry;
+} & UndoRedoState & HandleRuleRegistry & EditorWarningRegistry;
export type UndoRedoState = {
// UndoRedo Types
@@ -129,4 +130,7 @@ export type HandleRuleRegistry = {
// cleans up all registered rules of all handles of the provided node
unregisterNodeRules: (nodeId: string) => void
-}
\ No newline at end of file
+}
+
+
+
diff --git a/src/pages/VisProgPage/visualProgrammingUI/components/EditorWarnings.tsx b/src/pages/VisProgPage/visualProgrammingUI/components/EditorWarnings.tsx
new file mode 100644
index 0000000..eb5a0f6
--- /dev/null
+++ b/src/pages/VisProgPage/visualProgrammingUI/components/EditorWarnings.tsx
@@ -0,0 +1,217 @@
+/* contains all logic for the VisProgEditor warning system
+*
+* Missing but desirable features:
+* - Warning filtering:
+* - if there is no completely connected chain of startNode-[PhaseNodes]-EndNode
+* then hide any startNode, phaseNode, or endNode specific warnings
+*/
+import useFlowStore from "../VisProgStores.tsx";
+import type {FlowState} from "../VisProgTypes.tsx";
+
+// --| Type definitions |--
+
+export type WarningId = NodeId | "GLOBAL_WARNINGS";
+export type NodeId = string;
+
+
+export type WarningType =
+ | 'MISSING_INPUT'
+ | 'MISSING_OUTPUT'
+ | 'PLAN_IS_UNDEFINED'
+ | 'INCOMPLETE_PROGRAM'
+ | string
+
+export type WarningSeverity =
+ | 'INFO' // Acceptable, but important to be aware of
+ | 'WARNING' // Acceptable, but probably undesirable behavior
+ | 'ERROR' // Prevents running program, should be fixed before running program is allowed
+
+export type WarningScope = {
+ id: string;
+ handleId?: string;
+}
+
+export type EditorWarning = {
+ scope: WarningScope;
+ type: WarningType;
+ severity: WarningSeverity;
+ description: string;
+};
+
+/**
+ * a scoped WarningKey,
+ * the handleId scoping is only needed for handle specific errors
+ *
+ * "`WarningType`:`handleId`"
+ */
+export type WarningKey = string; // for warnings that can occur on a per-handle basis
+
+/**
+ * a composite key used in the severityIndex
+ *
+ * "`WarningId`|`WarningKey`"
+ */
+export type CompositeWarningKey = string;
+
+export type WarningRegistry = Map>;
+export type SeverityIndex = Map>;
+
+type ZustandSet = (partial: Partial | ((state: FlowState) => Partial)) => void;
+type ZustandGet = () => FlowState;
+
+export type EditorWarningRegistry = {
+ editorWarningRegistry: WarningRegistry;
+ severityIndex: SeverityIndex;
+
+ getWarnings: () => EditorWarning[];
+
+ getWarningsBySeverity: (warningSeverity: WarningSeverity) => EditorWarning[];
+
+ /**
+ * checks if there are no warnings of breaking severity
+ * @returns {boolean}
+ */
+ isProgramValid: () => boolean;
+
+ /**
+ * registers a warning to the warningRegistry and the SeverityIndex
+ * @param {EditorWarning} warning
+ */
+ registerWarning: (warning: EditorWarning) => void;
+
+ /**
+ * unregisters a warning from the warningRegistry and the SeverityIndex
+ * @param {EditorWarning} warning
+ */
+ unregisterWarning: (id: WarningId, warningKey: WarningKey) => void
+
+ /**
+ * unregisters warnings from the warningRegistry and the SeverityIndex
+ * @param {EditorWarning} warning
+ */
+ unregisterWarningsForId: (id: WarningId) => void;
+}
+
+// --| implemented logic |--
+
+export const globalWarning = "GLOBAL_WARNINGS";
+
+export function editorWarningRegistry(get: ZustandGet, set: ZustandSet) : EditorWarningRegistry { return {
+ editorWarningRegistry: new Map>(),
+ severityIndex: new Map([
+ ['INFO', new Set()],
+ ['WARNING', new Set()],
+ ['ERROR', new Set()],
+ ]),
+
+ getWarningsBySeverity: (warningSeverity) => {
+ const wRegistry = get().editorWarningRegistry;
+ const sIndex = get().severityIndex;
+ const warningKeys = sIndex.get(warningSeverity);
+ const warnings: EditorWarning[] = [];
+
+ warningKeys?.forEach(
+ (compositeKey) => {
+ const [id, warningKey] = compositeKey.split('|');
+ const warning = wRegistry.get(id)?.get(warningKey);
+
+ if (warning) {
+ warnings.push(warning);
+ }
+ }
+ )
+
+ return warnings;
+ },
+
+ isProgramValid: () => {
+ const sIndex = get().severityIndex;
+ return (sIndex.get("ERROR")!.size === 0);
+ },
+
+ getWarnings: () => Array.from(get().editorWarningRegistry.values())
+ .flatMap(innerMap => Array.from(innerMap.values())),
+
+
+ registerWarning: (warning) => {
+ const { scope: {id, handleId}, type, severity } = warning;
+ const warningKey = handleId ? `${type}:${handleId}` : type;
+ const compositeKey = `${id}|${warningKey}`;
+ const wRegistry = structuredClone(get().editorWarningRegistry);
+ const sIndex = structuredClone(get().severityIndex);
+ console.log("register")
+ // add to warning registry
+ if (!wRegistry.has(id)) {
+ wRegistry.set(id, new Map());
+ }
+ wRegistry.get(id)!.set(warningKey, warning);
+
+
+ // add to severityIndex
+ if (!sIndex.get(severity)!.has(compositeKey)) {
+ sIndex.get(severity)!.add(compositeKey);
+ }
+
+ set({
+ editorWarningRegistry: wRegistry,
+ severityIndex: sIndex
+ })
+ },
+
+ unregisterWarning: (id, warningKey) => {
+ const wRegistry = structuredClone(get().editorWarningRegistry);
+ const sIndex = structuredClone(get().severityIndex);
+ console.log("unregister")
+ // verify if the warning was created already
+ const warning = wRegistry.get(id)?.get(warningKey);
+ if (!warning) return;
+
+ // remove from warning registry
+ wRegistry.get(id)!.delete(warningKey);
+
+
+ // remove from severityIndex
+ sIndex.get(warning.severity)!.delete(`${id}|${warningKey}`);
+
+ set({
+ editorWarningRegistry: wRegistry,
+ severityIndex: sIndex
+ })
+ },
+
+ unregisterWarningsForId: (id) => {
+ const wRegistry = structuredClone(get().editorWarningRegistry);
+ const sIndex = structuredClone(get().severityIndex);
+
+ const nodeWarnings = wRegistry.get(id);
+
+ // remove from severity index
+ if (nodeWarnings) {
+ nodeWarnings.forEach((warning, warningKey) => {
+ sIndex.get(warning.severity)?.delete(`${id}|${warningKey}`);
+ });
+ }
+
+ // remove from warning registry
+ wRegistry.delete(id);
+
+ set({
+ editorWarningRegistry: wRegistry,
+ severityIndex: sIndex
+ })
+ },
+}}
+
+
+// returns a summary of the warningRegistry
+export function warningSummary() {
+ const {severityIndex, isProgramValid} = useFlowStore.getState();
+ return {
+ info: severityIndex.get('INFO')!.size,
+ warning: severityIndex.get('WARNING')!.size,
+ error: severityIndex.get('ERROR')!.size,
+ isValid: isProgramValid(),
+ };
+}
+
+
diff --git a/src/pages/VisProgPage/visualProgrammingUI/components/WarningSidebar.module.css b/src/pages/VisProgPage/visualProgrammingUI/components/WarningSidebar.module.css
new file mode 100644
index 0000000..2a8241e
--- /dev/null
+++ b/src/pages/VisProgPage/visualProgrammingUI/components/WarningSidebar.module.css
@@ -0,0 +1,78 @@
+.warnings-sidebar {
+ width: 320px;
+ height: 100%;
+ background: canvas;
+ border-left: 2px solid black;
+ display: flex;
+ flex-direction: column;
+}
+
+.warnings-header {
+ padding: 12px;
+ border-bottom: 1px solid #2a2a2e;
+}
+
+.severity-tabs {
+ display: flex;
+ gap: 4px;
+}
+
+.severity-tab {
+ flex: 1;
+ padding: 4px;
+ background: ButtonFace;
+ color: GrayText;
+ border: none;
+ cursor: pointer;
+}
+
+.count {
+ padding: 4px;
+ color: GrayText;
+ border: none;
+ cursor: pointer;
+}
+
+.severity-tab.active {
+ color: ButtonText;
+ border: 2px solid currentColor;
+}
+
+.warnings-list {
+ flex: 1;
+ overflow-y: auto;
+}
+
+.warnings-empty {
+ margin: auto;
+}
+
+.warning-item {
+ display: flex;
+ margin: 5px;
+ gap: 8px;
+ padding: 8px 12px;
+ border-radius: 5px;
+ cursor: pointer;
+}
+
+.warning-item:hover {
+ background: ButtonFace;
+}
+
+.warning-item--error {
+ border: 2px solid red;
+}
+
+.warning-item--warning {
+ border: 3px solid orange;
+}
+
+.warning-item--info {
+ border: 3px solid steelblue;
+}
+
+.warning-item .meta {
+ font-size: 11px;
+ opacity: 0.6;
+}
\ No newline at end of file
diff --git a/src/pages/VisProgPage/visualProgrammingUI/components/WarningSidebar.tsx b/src/pages/VisProgPage/visualProgrammingUI/components/WarningSidebar.tsx
new file mode 100644
index 0000000..fc3b347
--- /dev/null
+++ b/src/pages/VisProgPage/visualProgrammingUI/components/WarningSidebar.tsx
@@ -0,0 +1,131 @@
+import {useReactFlow, useStoreApi} from "@xyflow/react";
+import clsx from "clsx";
+import {useEffect, useState} from "react";
+import useFlowStore from "../VisProgStores.tsx";
+import {
+ warningSummary,
+ type WarningSeverity,
+ type EditorWarning, globalWarning
+} from "./EditorWarnings.tsx";
+import styles from "./WarningSidebar.module.css";
+
+export function WarningsSidebar() {
+ const warnings = useFlowStore.getState().getWarnings();
+
+ const [severityFilter, setSeverityFilter] = useState('ALL');
+
+ useEffect(() => {}, [warnings]);
+ const filtered = severityFilter === 'ALL'
+ ? warnings
+ : warnings.filter(w => w.severity === severityFilter);
+
+ return (
+
+ );
+}
+
+function WarningsHeader({
+ severityFilter,
+ onChange,
+}: {
+ severityFilter: WarningSeverity | 'ALL';
+ onChange: (severity: WarningSeverity | 'ALL') => void;
+}) {
+ const summary = warningSummary();
+
+ return (
+
+
Warnings
+
+
+ {(['ALL', 'ERROR', 'WARNING', 'INFO'] as const).map(severity => (
+
+ ))}
+
+
+ );
+}
+
+
+
+function WarningsList(props: { warnings: EditorWarning[] }) {
+ if (props.warnings.length === 0) {
+ return (
+
+ No warnings!
+
+ )
+ }
+ return (
+
+ {props.warnings.map((warning) => (
+
+ ))}
+
+ );
+}
+
+function WarningListItem(props: { warning: EditorWarning }) {
+ const jumpToNode = useJumpToNode();
+
+ return (
+ jumpToNode(props.warning.scope.id)}
+ >
+
+ {props.warning.description}
+
+
+
+ {props.warning.scope.id}
+ {props.warning.scope.handleId && (
+ @{props.warning.scope.handleId}
+ )}
+
+
+ );
+}
+
+function useJumpToNode() {
+ const { getNode, setCenter } = useReactFlow();
+ const { addSelectedNodes } = useStoreApi().getState();
+
+ return (nodeId: string) => {
+ // user can't jump to global warning, so prevent further logic from running
+ if (nodeId === globalWarning) return;
+ const node = getNode(nodeId);
+ if (!node) return;
+
+ const { position, width = 0, height = 0} = node;
+
+ // move to node
+ setCenter(
+ position!.x + width / 2,
+ position!.y + height / 2,
+ { zoom: 2, duration: 300 }
+ ).then(() => {
+ // select the node
+ addSelectedNodes([nodeId]);
+ });
+
+ };
+}
\ No newline at end of file
diff --git a/src/pages/VisProgPage/visualProgrammingUI/nodes/EndNode.tsx b/src/pages/VisProgPage/visualProgrammingUI/nodes/EndNode.tsx
index 5c456b5..5bed205 100644
--- a/src/pages/VisProgPage/visualProgrammingUI/nodes/EndNode.tsx
+++ b/src/pages/VisProgPage/visualProgrammingUI/nodes/EndNode.tsx
@@ -1,12 +1,15 @@
import {
type NodeProps,
Position,
- type Node,
+ type Node, useNodeConnections
} from '@xyflow/react';
+import {useEffect} from "react";
+import type {EditorWarning} from "../components/EditorWarnings.tsx";
import { Toolbar } from '../components/NodeComponents';
import styles from '../../VisProg.module.css';
import {SingleConnectionHandle} from "../components/RuleBasedHandle.tsx";
import {allowOnlyConnectionsFromType} from "../HandleRules.ts";
+import useFlowStore from "../VisProgStores.tsx";
@@ -27,6 +30,27 @@ export type EndNode = Node
* @returns React.JSX.Element
*/
export default function EndNode(props: NodeProps) {
+ const {registerWarning, unregisterWarning} = useFlowStore.getState();
+ const connections = useNodeConnections({
+ id: props.id,
+ handleId: 'target'
+ })
+
+ useEffect(() => {
+ const noConnectionWarning : EditorWarning = {
+ scope: {
+ id: props.id,
+ handleId: 'target'
+ },
+ type: 'MISSING_INPUT',
+ severity: "ERROR",
+ description: "the endNode does not have an incoming connection from a phaseNode"
+ }
+
+ if (connections.length === 0) { registerWarning(noConnectionWarning); }
+ else { unregisterWarning(props.id, `${noConnectionWarning.type}:target`); }
+ }, [connections.length, props.id, registerWarning, unregisterWarning]);
+
return (
<>
diff --git a/src/pages/VisProgPage/visualProgrammingUI/nodes/PhaseNode.tsx b/src/pages/VisProgPage/visualProgrammingUI/nodes/PhaseNode.tsx
index 50e81b6..c5a283a 100644
--- a/src/pages/VisProgPage/visualProgrammingUI/nodes/PhaseNode.tsx
+++ b/src/pages/VisProgPage/visualProgrammingUI/nodes/PhaseNode.tsx
@@ -1,8 +1,10 @@
import {
type NodeProps,
Position,
- type Node
+ type Node, useNodeConnections
} from '@xyflow/react';
+import {useEffect} from "react";
+import type {EditorWarning} from "../components/EditorWarnings.tsx";
import { Toolbar } from '../components/NodeComponents';
import styles from '../../VisProg.module.css';
import {SingleConnectionHandle, MultiConnectionHandle} from "../components/RuleBasedHandle.tsx";
@@ -41,6 +43,28 @@ export default function PhaseNode(props: NodeProps) {
const updateLabel = (value: string) => updateNodeData(props.id, {...data, label: value});
const label_input_id = `phase_${props.id}_label_input`;
+ const {registerWarning, unregisterWarning} = useFlowStore.getState();
+ const connections = useNodeConnections({
+ id: props.id,
+ handleType: "target",
+ handleId: 'data'
+ })
+
+ useEffect(() => {
+ const noConnectionWarning : EditorWarning = {
+ scope: {
+ id: props.id,
+ handleId: 'data'
+ },
+ type: 'MISSING_INPUT',
+ severity: "WARNING",
+ description: "the phaseNode has no incoming goals, norms, and/or triggers"
+ }
+
+ if (connections.length === 0) { registerWarning(noConnectionWarning); }
+ else { unregisterWarning(props.id, `${noConnectionWarning.type}:data`); }
+ }, [connections.length, props.id, registerWarning, unregisterWarning]);
+
return (
<>
diff --git a/src/pages/VisProgPage/visualProgrammingUI/nodes/StartNode.tsx b/src/pages/VisProgPage/visualProgrammingUI/nodes/StartNode.tsx
index 741b190..fab9b93 100644
--- a/src/pages/VisProgPage/visualProgrammingUI/nodes/StartNode.tsx
+++ b/src/pages/VisProgPage/visualProgrammingUI/nodes/StartNode.tsx
@@ -1,12 +1,15 @@
import {
type NodeProps,
Position,
- type Node,
+ type Node, useNodeConnections
} from '@xyflow/react';
+import {useEffect} from "react";
import { Toolbar } from '../components/NodeComponents';
import styles from '../../VisProg.module.css';
import {SingleConnectionHandle} from "../components/RuleBasedHandle.tsx";
+import {type EditorWarning} from "../components/EditorWarnings.tsx";
import {allowOnlyConnectionsFromHandle} from "../HandleRules.ts";
+import useFlowStore from "../VisProgStores.tsx";
export type StartNodeData = {
@@ -25,6 +28,27 @@ export type StartNode = Node
* @returns React.JSX.Element
*/
export default function StartNode(props: NodeProps) {
+ const {registerWarning, unregisterWarning} = useFlowStore.getState();
+ const connections = useNodeConnections({
+ id: props.id,
+ handleId: 'source'
+ })
+
+ useEffect(() => {
+ const noConnectionWarning : EditorWarning = {
+ scope: {
+ id: props.id,
+ handleId: 'source'
+ },
+ type: 'MISSING_OUTPUT',
+ severity: "ERROR",
+ description: "the startNode does not have an outgoing connection to a phaseNode"
+ }
+
+ if (connections.length === 0) { registerWarning(noConnectionWarning); }
+ else { unregisterWarning(props.id, `${noConnectionWarning.type}:source`); }
+ }, [connections.length, props.id, registerWarning, unregisterWarning]);
+
return (
<>