From 67558a7ac748e6c5aa56b2450cdb29218e76cfc0 Mon Sep 17 00:00:00 2001 From: JGerla Date: Wed, 14 Jan 2026 16:04:44 +0100 Subject: [PATCH 01/14] feat: added full definition of editor warning infrastructure Everything is now defined architecturally, and can be implemented properly. ref: N25B-450 --- .../visualProgrammingUI/EditorWarnings.tsx | 95 +++++++++++++++++++ .../visualProgrammingUI/VisProgStores.tsx | 4 +- .../visualProgrammingUI/VisProgTypes.tsx | 8 +- 3 files changed, 104 insertions(+), 3 deletions(-) create mode 100644 src/pages/VisProgPage/visualProgrammingUI/EditorWarnings.tsx diff --git a/src/pages/VisProgPage/visualProgrammingUI/EditorWarnings.tsx b/src/pages/VisProgPage/visualProgrammingUI/EditorWarnings.tsx new file mode 100644 index 0000000..54c5c11 --- /dev/null +++ b/src/pages/VisProgPage/visualProgrammingUI/EditorWarnings.tsx @@ -0,0 +1,95 @@ + + +// --| Type definitions |-- + +export type NodeId = string; +export type WarningType = + | 'MISSING_INPUT' + | 'MISSING_OUTPUT' + | string +export type WarningSeverity = + | 'INFO' // Acceptable, but probably not desirable + | 'WARNING' // Prevents running program, should be fixed before running program is allowed + + +export type EditorWarning = { + nodeId: NodeId; + type: WarningType; + severity: WarningSeverity; + description: string; + handleId?: string; +}; + +/** + * either a single warningType, or a scoped warningKey. + * + * supported-scopes: + * - `handle` + */ +export type WarningKey = + | WarningType // for warnings that can only occur once per node + | { type: WarningType, handleId: string }; // for warnings that can occur on a per-handle basis + +export type WarningRegistry = Map>; +export type SeverityIndex = Map>; + +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: ( + nodeId: NodeId, + warningKey: WarningKey, + ) => void + + /** + * unregisters warnings from the warningRegistry and the SeverityIndex + * @param {EditorWarning} warning + */ + unregisterWarningsForNode: (nodeId: string) => void; +} + +// --| implemented logic |-- + +export const editorWarningRegistry : EditorWarningRegistry = { + editorWarningRegistry: new Map>(), + severityIndex: new Map([ + ['INFO', new Set()], + ['WARNING', new Set()] + ]), + + getWarningsBySeverity: (_warningSeverity) => { return []}, + + isProgramValid: () => { return true}, + + getWarnings: () => { return []}, + + registerWarning: () => {}, + + unregisterWarning: () => {}, + + unregisterWarningsForNode: (_nodeId) => {}, +}; + diff --git a/src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx b/src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx index defa934..faa3f3c 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 { editorWarningRegistry } from "./EditorWarnings.tsx"; import type { FlowState } from './VisProgTypes'; import { NodeDefaults, @@ -306,7 +307,8 @@ const useFlowStore = create(UndoRedo((set, get) => ({ }) return { ruleRegistry: registry }; }) - } + }, + ...editorWarningRegistry, })) ); diff --git a/src/pages/VisProgPage/visualProgrammingUI/VisProgTypes.tsx b/src/pages/VisProgPage/visualProgrammingUI/VisProgTypes.tsx index 8ae3cad..bf1bffd 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 "./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 +} + + + From b3b77b94ad0bb0d68a98900b529c9c806efffe0c Mon Sep 17 00:00:00 2001 From: JGerla Date: Wed, 14 Jan 2026 16:11:48 +0100 Subject: [PATCH 02/14] feat: slightly modified structure for better global logic ref: N25B-450 --- .../visualProgrammingUI/EditorWarnings.tsx | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/pages/VisProgPage/visualProgrammingUI/EditorWarnings.tsx b/src/pages/VisProgPage/visualProgrammingUI/EditorWarnings.tsx index 54c5c11..65f8e6c 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/EditorWarnings.tsx +++ b/src/pages/VisProgPage/visualProgrammingUI/EditorWarnings.tsx @@ -21,17 +21,13 @@ export type EditorWarning = { }; /** - * either a single warningType, or a scoped warningKey. - * - * supported-scopes: - * - `handle` + * a scoped WarningKey, + * `handleId` is `null` if the warning is not specific to one handle on the node */ -export type WarningKey = - | WarningType // for warnings that can only occur once per node - | { type: WarningType, handleId: string }; // for warnings that can occur on a per-handle basis +export type WarningKey = { type: WarningType, handleId: string | null }; // for warnings that can occur on a per-handle basis export type WarningRegistry = Map>; -export type SeverityIndex = Map>; +export type SeverityIndex = Map>; export type EditorWarningRegistry = { editorWarningRegistry: WarningRegistry; @@ -76,8 +72,8 @@ export type EditorWarningRegistry = { export const editorWarningRegistry : EditorWarningRegistry = { editorWarningRegistry: new Map>(), severityIndex: new Map([ - ['INFO', new Set()], - ['WARNING', new Set()] + ['INFO', new Set<{ nodeId: NodeId, warningKey: WarningKey}>()], + ['WARNING', new Set<{ nodeId: NodeId, warningKey: WarningKey}>()] ]), getWarningsBySeverity: (_warningSeverity) => { return []}, From f174623a4c67527c17af074110ee66a1f5dd8493 Mon Sep 17 00:00:00 2001 From: JGerla Date: Wed, 14 Jan 2026 16:46:50 +0100 Subject: [PATCH 03/14] feat: implemented basic add and remove functions ref: N25B-450 --- .../visualProgrammingUI/EditorWarnings.tsx | 58 +++++++++++++++---- .../visualProgrammingUI/VisProgStores.tsx | 2 +- 2 files changed, 48 insertions(+), 12 deletions(-) diff --git a/src/pages/VisProgPage/visualProgrammingUI/EditorWarnings.tsx b/src/pages/VisProgPage/visualProgrammingUI/EditorWarnings.tsx index 65f8e6c..7ecde38 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/EditorWarnings.tsx +++ b/src/pages/VisProgPage/visualProgrammingUI/EditorWarnings.tsx @@ -2,6 +2,8 @@ // --| Type definitions |-- +import type {FlowState} from "./VisProgTypes.tsx"; + export type NodeId = string; export type WarningType = | 'MISSING_INPUT' @@ -29,6 +31,9 @@ export type WarningKey = { type: WarningType, handleId: string | null }; // for 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; @@ -47,18 +52,13 @@ export type EditorWarningRegistry = { * registers a warning to the warningRegistry and the SeverityIndex * @param {EditorWarning} warning */ - registerWarning: ( - warning: EditorWarning - ) => void; + registerWarning: (warning: EditorWarning) => void; /** * unregisters a warning from the warningRegistry and the SeverityIndex * @param {EditorWarning} warning */ - unregisterWarning: ( - nodeId: NodeId, - warningKey: WarningKey, - ) => void + unregisterWarning: (nodeId: NodeId, warningKey: WarningKey) => void /** * unregisters warnings from the warningRegistry and the SeverityIndex @@ -69,7 +69,7 @@ export type EditorWarningRegistry = { // --| implemented logic |-- -export const editorWarningRegistry : EditorWarningRegistry = { +export function editorWarningRegistry(get: ZustandGet, set: ZustandSet) : EditorWarningRegistry {return { editorWarningRegistry: new Map>(), severityIndex: new Map([ ['INFO', new Set<{ nodeId: NodeId, warningKey: WarningKey}>()], @@ -82,10 +82,46 @@ export const editorWarningRegistry : EditorWarningRegistry = { getWarnings: () => { return []}, - registerWarning: () => {}, + registerWarning: (warning) => { + const { nodeId, type, severity, handleId } = warning; + const warningKey = handleId ? { type, handleId } : { type, handleId: null}; + const wRegistry = get().editorWarningRegistry; + const sIndex = get().severityIndex; - unregisterWarning: () => {}, + // add to registry + if (!wRegistry.has(nodeId)) { + wRegistry.set(nodeId, new Map()); + } + wRegistry.get(nodeId)!.set(warningKey, warning); + + // add to severityIndex + sIndex.get(severity)!.add({nodeId,warningKey}); + + set({ + editorWarningRegistry: wRegistry, + severityIndex: sIndex + }) + }, + + unregisterWarning: (nodeId, warningKey) => { + + const wRegistry = get().editorWarningRegistry; + const sIndex = get().severityIndex; + + const warning = wRegistry.get(nodeId)!.get(warningKey); + // remove from registry + wRegistry.get(nodeId)!.delete(warningKey); + + + // remove from severityIndex + sIndex.get(warning!.severity)!.delete({nodeId,warningKey}); + + set({ + editorWarningRegistry: wRegistry, + severityIndex: sIndex + }) + }, unregisterWarningsForNode: (_nodeId) => {}, -}; +}} diff --git a/src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx b/src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx index faa3f3c..570d0df 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx +++ b/src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx @@ -308,7 +308,7 @@ const useFlowStore = create(UndoRedo((set, get) => ({ return { ruleRegistry: registry }; }) }, - ...editorWarningRegistry, + ...editorWarningRegistry(get, set), })) ); From 1a8670ba13aee5382955f77ed173abdd21aa64e4 Mon Sep 17 00:00:00 2001 From: JGerla Date: Wed, 14 Jan 2026 16:52:43 +0100 Subject: [PATCH 04/14] feat: implemented delete all warnings for a node ref: N25B-450 --- .../visualProgrammingUI/EditorWarnings.tsx | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/pages/VisProgPage/visualProgrammingUI/EditorWarnings.tsx b/src/pages/VisProgPage/visualProgrammingUI/EditorWarnings.tsx index 7ecde38..66c12b5 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/EditorWarnings.tsx +++ b/src/pages/VisProgPage/visualProgrammingUI/EditorWarnings.tsx @@ -122,6 +122,26 @@ export function editorWarningRegistry(get: ZustandGet, set: ZustandSet) : Editor }) }, - unregisterWarningsForNode: (_nodeId) => {}, + unregisterWarningsForNode: (nodeId) => { + const wRegistry = get().editorWarningRegistry; + const sIndex = get().severityIndex; + + const nodeWarnings = wRegistry.get(nodeId); + + // remove from severity index + if (nodeWarnings) { + nodeWarnings.forEach((warning, warningKey) => { + sIndex.get(warning.severity)?.delete({nodeId, warningKey}); + }); + } + + // remove from registry + wRegistry.delete(nodeId); + + set({ + editorWarningRegistry: wRegistry, + severityIndex: sIndex + }) + }, }} From e9acab456ea84eb72c5cd5818b63baa3ac025d20 Mon Sep 17 00:00:00 2001 From: JGerla Date: Wed, 14 Jan 2026 16:59:28 +0100 Subject: [PATCH 05/14] feat: implemented getWarningsBySeverity ref: N25B-450 --- .../visualProgrammingUI/EditorWarnings.tsx | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/pages/VisProgPage/visualProgrammingUI/EditorWarnings.tsx b/src/pages/VisProgPage/visualProgrammingUI/EditorWarnings.tsx index 66c12b5..554cc2c 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/EditorWarnings.tsx +++ b/src/pages/VisProgPage/visualProgrammingUI/EditorWarnings.tsx @@ -76,7 +76,26 @@ export function editorWarningRegistry(get: ZustandGet, set: ZustandSet) : Editor ['WARNING', new Set<{ nodeId: NodeId, warningKey: WarningKey}>()] ]), - getWarningsBySeverity: (_warningSeverity) => { return []}, + getWarningsBySeverity: (warningSeverity) => { + const wRegistry = get().editorWarningRegistry; + const sIndex = get().severityIndex; + + const warningKeys = sIndex.get(warningSeverity); + + const warnings: EditorWarning[] = []; + + warningKeys?.forEach( + ({nodeId, warningKey}) => { + const warning = wRegistry.get(nodeId)?.get(warningKey); + + if (warning) { + warnings.push(warning); + } + } + ) + + return warnings; + }, isProgramValid: () => { return true}, From 5d650b36ce9c91138425f2e41a608d841842bf23 Mon Sep 17 00:00:00 2001 From: JGerla Date: Thu, 15 Jan 2026 12:15:32 +0100 Subject: [PATCH 06/14] feat: implemented the rest of the warning registry ref: N25B-450 --- .../visualProgrammingUI/EditorWarnings.tsx | 96 ++++++++++++------- 1 file changed, 61 insertions(+), 35 deletions(-) diff --git a/src/pages/VisProgPage/visualProgrammingUI/EditorWarnings.tsx b/src/pages/VisProgPage/visualProgrammingUI/EditorWarnings.tsx index 554cc2c..2ff7397 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/EditorWarnings.tsx +++ b/src/pages/VisProgPage/visualProgrammingUI/EditorWarnings.tsx @@ -1,25 +1,43 @@ - +/* contains all logic for the VisProgEditor warning system +* +* Missing but desirable features: +* - Global Warnings, e.g. not node specific +* - could be done by creating a global scope entry with GLOBAL as nodeId +* - Warning filtering: +* - if there is no completely connected chain of startNode-[PhaseNodes]-EndNode +* then hide any startNode, phaseNode, or endNode specific warnings +* */ // --| Type definitions |-- import type {FlowState} from "./VisProgTypes.tsx"; + +export type WarningId = NodeId | "GLOBAL_WARNINGS"; export type NodeId = string; + + export type WarningType = | 'MISSING_INPUT' | 'MISSING_OUTPUT' + | 'PLAN_IS_UNDEFINED' | string -export type WarningSeverity = - | 'INFO' // Acceptable, but probably not desirable - | 'WARNING' // Prevents running program, should be fixed before running program is allowed +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 = { - nodeId: NodeId; + scope: WarningScope; type: WarningType; severity: WarningSeverity; description: string; - handleId?: string; }; /** @@ -28,8 +46,9 @@ export type EditorWarning = { */ export type WarningKey = { type: WarningType, handleId: string | null }; // for warnings that can occur on a per-handle basis -export type WarningRegistry = Map>; -export type SeverityIndex = Map>; + +export type WarningRegistry = Map>; +export type SeverityIndex = Map>; type ZustandSet = (partial: Partial | ((state: FlowState) => Partial)) => void; type ZustandGet = () => FlowState; @@ -58,35 +77,37 @@ export type EditorWarningRegistry = { * unregisters a warning from the warningRegistry and the SeverityIndex * @param {EditorWarning} warning */ - unregisterWarning: (nodeId: NodeId, warningKey: WarningKey) => void + unregisterWarning: (id: WarningId, warningKey: WarningKey) => void /** * unregisters warnings from the warningRegistry and the SeverityIndex * @param {EditorWarning} warning */ - unregisterWarningsForNode: (nodeId: string) => void; + unregisterWarningsForId: (id: WarningId) => void; } // --| implemented logic |-- -export function editorWarningRegistry(get: ZustandGet, set: ZustandSet) : EditorWarningRegistry {return { +export const globalWarningKey = (type: WarningType) : WarningKey => { return {type: type, handleId: null}}; +export const globalWarning = "GLOBAL_WARNINGS"; + + +export function editorWarningRegistry(get: ZustandGet, set: ZustandSet) : EditorWarningRegistry { return { editorWarningRegistry: new Map>(), severityIndex: new Map([ - ['INFO', new Set<{ nodeId: NodeId, warningKey: WarningKey}>()], - ['WARNING', new Set<{ nodeId: NodeId, warningKey: WarningKey}>()] + ['INFO', new Set<{ id: WarningId, warningKey: WarningKey}>()], + ['WARNING', new Set<{ id: WarningId, warningKey: WarningKey}>()] ]), getWarningsBySeverity: (warningSeverity) => { const wRegistry = get().editorWarningRegistry; const sIndex = get().severityIndex; - const warningKeys = sIndex.get(warningSeverity); - const warnings: EditorWarning[] = []; warningKeys?.forEach( - ({nodeId, warningKey}) => { - const warning = wRegistry.get(nodeId)?.get(warningKey); + ({id, warningKey}) => { + const warning = wRegistry.get(id)?.get(warningKey); if (warning) { warnings.push(warning); @@ -97,24 +118,29 @@ export function editorWarningRegistry(get: ZustandGet, set: ZustandSet) : Editor return warnings; }, - isProgramValid: () => { return true}, + isProgramValid: () => { + const sIndex = get().severityIndex; + return sIndex.get("ERROR")!.size === 0; + }, + + getWarnings: () => Array.from(get().editorWarningRegistry.values()) + .flatMap(innerMap => Array.from(innerMap.values())), - getWarnings: () => { return []}, registerWarning: (warning) => { - const { nodeId, type, severity, handleId } = warning; + const { scope: {id, handleId}, type, severity } = warning; const warningKey = handleId ? { type, handleId } : { type, handleId: null}; const wRegistry = get().editorWarningRegistry; const sIndex = get().severityIndex; - // add to registry - if (!wRegistry.has(nodeId)) { - wRegistry.set(nodeId, new Map()); + // add to warning registry + if (!wRegistry.has(id)) { + wRegistry.set(id, new Map()); } - wRegistry.get(nodeId)!.set(warningKey, warning); + wRegistry.get(id)!.set(warningKey, warning); // add to severityIndex - sIndex.get(severity)!.add({nodeId,warningKey}); + sIndex.get(severity)!.add({id,warningKey}); set({ editorWarningRegistry: wRegistry, @@ -122,18 +148,18 @@ export function editorWarningRegistry(get: ZustandGet, set: ZustandSet) : Editor }) }, - unregisterWarning: (nodeId, warningKey) => { + unregisterWarning: (id, warningKey) => { const wRegistry = get().editorWarningRegistry; const sIndex = get().severityIndex; - const warning = wRegistry.get(nodeId)!.get(warningKey); - // remove from registry - wRegistry.get(nodeId)!.delete(warningKey); + const warning = wRegistry.get(id)!.get(warningKey); + // remove from warning registry + wRegistry.get(id)!.delete(warningKey); // remove from severityIndex - sIndex.get(warning!.severity)!.delete({nodeId,warningKey}); + sIndex.get(warning!.severity)!.delete({id,warningKey}); set({ editorWarningRegistry: wRegistry, @@ -141,21 +167,21 @@ export function editorWarningRegistry(get: ZustandGet, set: ZustandSet) : Editor }) }, - unregisterWarningsForNode: (nodeId) => { + unregisterWarningsForId: (id) => { const wRegistry = get().editorWarningRegistry; const sIndex = get().severityIndex; - const nodeWarnings = wRegistry.get(nodeId); + const nodeWarnings = wRegistry.get(id); // remove from severity index if (nodeWarnings) { nodeWarnings.forEach((warning, warningKey) => { - sIndex.get(warning.severity)?.delete({nodeId, warningKey}); + sIndex.get(warning.severity)?.delete({id, warningKey}); }); } - // remove from registry - wRegistry.delete(nodeId); + // remove from warning registry + wRegistry.delete(id); set({ editorWarningRegistry: wRegistry, From 66daafe1f0ec0a99a2cbb1b02ce4c16f68f7a864 Mon Sep 17 00:00:00 2001 From: JGerla Date: Thu, 15 Jan 2026 12:21:09 +0100 Subject: [PATCH 07/14] feat: added disabling of runProgram button if program is not valid ref: N25B-450 --- src/pages/VisProgPage/VisProg.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/pages/VisProgPage/VisProg.tsx b/src/pages/VisProgPage/VisProg.tsx index a083bbf..a31665f 100644 --- a/src/pages/VisProgPage/VisProg.tsx +++ b/src/pages/VisProgPage/VisProg.tsx @@ -186,10 +186,17 @@ function graphReducer() { * @constructor */ function VisProgPage() { + const [programValidity, setProgramValidity] = useState(true); + const sIndex = useFlowStore.getState().severityIndex; + + useEffect(() => { + setProgramValidity(useFlowStore.getState().isProgramValid) + }, [sIndex]); + return ( <> - + ) } From 35bf3ad9e50d8fcedeaf5fcb1f537d2b915c6062 Mon Sep 17 00:00:00 2001 From: JGerla Date: Thu, 15 Jan 2026 14:22:50 +0100 Subject: [PATCH 08/14] feat: finished basic warning system nodes can now register warnings to prevent running the program ref: N25B-450 --- src/pages/VisProgPage/VisProg.tsx | 18 ++++-- .../visualProgrammingUI/EditorWarnings.tsx | 62 +++++++++++-------- .../visualProgrammingUI/nodes/StartNode.tsx | 28 ++++++++- 3 files changed, 77 insertions(+), 31 deletions(-) diff --git a/src/pages/VisProgPage/VisProg.tsx b/src/pages/VisProgPage/VisProg.tsx index a31665f..500a6c0 100644 --- a/src/pages/VisProgPage/VisProg.tsx +++ b/src/pages/VisProgPage/VisProg.tsx @@ -90,6 +90,8 @@ const VisProgUI = () => { return () => window.removeEventListener('keydown', handler); }); + + return (
{ + @@ -187,16 +190,21 @@ function graphReducer() { */ function VisProgPage() { const [programValidity, setProgramValidity] = useState(true); - const sIndex = useFlowStore.getState().severityIndex; + + const {isProgramValid, severityIndex} = useFlowStore(); + useEffect(() => { - setProgramValidity(useFlowStore.getState().isProgramValid) - }, [sIndex]); - + setProgramValidity(isProgramValid); + // the following eslint disable is required as it wants us to use all possible dependencies for the useEffect statement, + // however this would result in an infinite loop because it would change one of its own dependencies + // so we only use those dependencies that we don't change ourselves + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [severityIndex]); return ( <> - + ) } diff --git a/src/pages/VisProgPage/visualProgrammingUI/EditorWarnings.tsx b/src/pages/VisProgPage/visualProgrammingUI/EditorWarnings.tsx index 2ff7397..436fc21 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/EditorWarnings.tsx +++ b/src/pages/VisProgPage/visualProgrammingUI/EditorWarnings.tsx @@ -1,12 +1,10 @@ /* contains all logic for the VisProgEditor warning system * * Missing but desirable features: -* - Global Warnings, e.g. not node specific -* - could be done by creating a global scope entry with GLOBAL as nodeId * - Warning filtering: * - if there is no completely connected chain of startNode-[PhaseNodes]-EndNode * then hide any startNode, phaseNode, or endNode specific warnings -* */ +*/ // --| Type definitions |-- @@ -42,13 +40,21 @@ export type EditorWarning = { /** * a scoped WarningKey, - * `handleId` is `null` if the warning is not specific to one handle on the node + * the handleId scoping is only needed for handle specific errors + * + * "`WarningType`:`handleId`" */ -export type WarningKey = { type: WarningType, handleId: string | null }; // for warnings that can occur on a per-handle basis +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>; +export type SeverityIndex = Map>; type ZustandSet = (partial: Partial | ((state: FlowState) => Partial)) => void; type ZustandGet = () => FlowState; @@ -88,15 +94,14 @@ export type EditorWarningRegistry = { // --| implemented logic |-- -export const globalWarningKey = (type: WarningType) : WarningKey => { return {type: type, handleId: null}}; export const globalWarning = "GLOBAL_WARNINGS"; - export function editorWarningRegistry(get: ZustandGet, set: ZustandSet) : EditorWarningRegistry { return { editorWarningRegistry: new Map>(), severityIndex: new Map([ - ['INFO', new Set<{ id: WarningId, warningKey: WarningKey}>()], - ['WARNING', new Set<{ id: WarningId, warningKey: WarningKey}>()] + ['INFO', new Set()], + ['WARNING', new Set()], + ['ERROR', new Set()], ]), getWarningsBySeverity: (warningSeverity) => { @@ -106,7 +111,8 @@ export function editorWarningRegistry(get: ZustandGet, set: ZustandSet) : Editor const warnings: EditorWarning[] = []; warningKeys?.forEach( - ({id, warningKey}) => { + (compositeKey) => { + const [id, warningKey] = compositeKey.split('|'); const warning = wRegistry.get(id)?.get(warningKey); if (warning) { @@ -120,7 +126,7 @@ export function editorWarningRegistry(get: ZustandGet, set: ZustandSet) : Editor isProgramValid: () => { const sIndex = get().severityIndex; - return sIndex.get("ERROR")!.size === 0; + return (sIndex.get("ERROR")!.size === 0); }, getWarnings: () => Array.from(get().editorWarningRegistry.values()) @@ -129,18 +135,22 @@ export function editorWarningRegistry(get: ZustandGet, set: ZustandSet) : Editor registerWarning: (warning) => { const { scope: {id, handleId}, type, severity } = warning; - const warningKey = handleId ? { type, handleId } : { type, handleId: null}; - const wRegistry = get().editorWarningRegistry; - const sIndex = get().severityIndex; - + 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 - sIndex.get(severity)!.add({id,warningKey}); + if (!sIndex.get(severity)!.has(compositeKey)) { + sIndex.get(severity)!.add(compositeKey); + } set({ editorWarningRegistry: wRegistry, @@ -149,17 +159,19 @@ export function editorWarningRegistry(get: ZustandGet, set: ZustandSet) : Editor }, 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; - const wRegistry = get().editorWarningRegistry; - const sIndex = get().severityIndex; - - const warning = wRegistry.get(id)!.get(warningKey); // remove from warning registry wRegistry.get(id)!.delete(warningKey); // remove from severityIndex - sIndex.get(warning!.severity)!.delete({id,warningKey}); + sIndex.get(warning.severity)!.delete(`${id}|${warningKey}`); set({ editorWarningRegistry: wRegistry, @@ -168,15 +180,15 @@ export function editorWarningRegistry(get: ZustandGet, set: ZustandSet) : Editor }, unregisterWarningsForId: (id) => { - const wRegistry = get().editorWarningRegistry; - const sIndex = get().severityIndex; + 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}); + sIndex.get(warning.severity)?.delete(`${id}|${warningKey}`); }); } diff --git a/src/pages/VisProgPage/visualProgrammingUI/nodes/StartNode.tsx b/src/pages/VisProgPage/visualProgrammingUI/nodes/StartNode.tsx index 741b190..d4bf0f6 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 "../EditorWarnings.tsx"; import {allowOnlyConnectionsFromHandle} from "../HandleRules.ts"; +import useFlowStore from "../VisProgStores.tsx"; export type StartNodeData = { @@ -25,6 +28,29 @@ 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 ( <> From 385ec250cc300409d1da4773b1ee0e8e67601755 Mon Sep 17 00:00:00 2001 From: JGerla Date: Thu, 15 Jan 2026 14:22:50 +0100 Subject: [PATCH 09/14] feat: finished basic warning system nodes can now register warnings to prevent running the program ref: N25B-450 --- src/pages/VisProgPage/VisProg.tsx | 17 +++-- .../visualProgrammingUI/EditorWarnings.tsx | 62 +++++++++++-------- .../visualProgrammingUI/nodes/StartNode.tsx | 28 ++++++++- 3 files changed, 76 insertions(+), 31 deletions(-) diff --git a/src/pages/VisProgPage/VisProg.tsx b/src/pages/VisProgPage/VisProg.tsx index a31665f..ef759c1 100644 --- a/src/pages/VisProgPage/VisProg.tsx +++ b/src/pages/VisProgPage/VisProg.tsx @@ -90,6 +90,8 @@ const VisProgUI = () => { return () => window.removeEventListener('keydown', handler); }); + + return (
{ + @@ -187,16 +190,20 @@ function graphReducer() { */ function VisProgPage() { const [programValidity, setProgramValidity] = useState(true); - const sIndex = useFlowStore.getState().severityIndex; + + const {isProgramValid, severityIndex} = useFlowStore(); + useEffect(() => { - setProgramValidity(useFlowStore.getState().isProgramValid) - }, [sIndex]); - + 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/EditorWarnings.tsx b/src/pages/VisProgPage/visualProgrammingUI/EditorWarnings.tsx index 2ff7397..436fc21 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/EditorWarnings.tsx +++ b/src/pages/VisProgPage/visualProgrammingUI/EditorWarnings.tsx @@ -1,12 +1,10 @@ /* contains all logic for the VisProgEditor warning system * * Missing but desirable features: -* - Global Warnings, e.g. not node specific -* - could be done by creating a global scope entry with GLOBAL as nodeId * - Warning filtering: * - if there is no completely connected chain of startNode-[PhaseNodes]-EndNode * then hide any startNode, phaseNode, or endNode specific warnings -* */ +*/ // --| Type definitions |-- @@ -42,13 +40,21 @@ export type EditorWarning = { /** * a scoped WarningKey, - * `handleId` is `null` if the warning is not specific to one handle on the node + * the handleId scoping is only needed for handle specific errors + * + * "`WarningType`:`handleId`" */ -export type WarningKey = { type: WarningType, handleId: string | null }; // for warnings that can occur on a per-handle basis +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>; +export type SeverityIndex = Map>; type ZustandSet = (partial: Partial | ((state: FlowState) => Partial)) => void; type ZustandGet = () => FlowState; @@ -88,15 +94,14 @@ export type EditorWarningRegistry = { // --| implemented logic |-- -export const globalWarningKey = (type: WarningType) : WarningKey => { return {type: type, handleId: null}}; export const globalWarning = "GLOBAL_WARNINGS"; - export function editorWarningRegistry(get: ZustandGet, set: ZustandSet) : EditorWarningRegistry { return { editorWarningRegistry: new Map>(), severityIndex: new Map([ - ['INFO', new Set<{ id: WarningId, warningKey: WarningKey}>()], - ['WARNING', new Set<{ id: WarningId, warningKey: WarningKey}>()] + ['INFO', new Set()], + ['WARNING', new Set()], + ['ERROR', new Set()], ]), getWarningsBySeverity: (warningSeverity) => { @@ -106,7 +111,8 @@ export function editorWarningRegistry(get: ZustandGet, set: ZustandSet) : Editor const warnings: EditorWarning[] = []; warningKeys?.forEach( - ({id, warningKey}) => { + (compositeKey) => { + const [id, warningKey] = compositeKey.split('|'); const warning = wRegistry.get(id)?.get(warningKey); if (warning) { @@ -120,7 +126,7 @@ export function editorWarningRegistry(get: ZustandGet, set: ZustandSet) : Editor isProgramValid: () => { const sIndex = get().severityIndex; - return sIndex.get("ERROR")!.size === 0; + return (sIndex.get("ERROR")!.size === 0); }, getWarnings: () => Array.from(get().editorWarningRegistry.values()) @@ -129,18 +135,22 @@ export function editorWarningRegistry(get: ZustandGet, set: ZustandSet) : Editor registerWarning: (warning) => { const { scope: {id, handleId}, type, severity } = warning; - const warningKey = handleId ? { type, handleId } : { type, handleId: null}; - const wRegistry = get().editorWarningRegistry; - const sIndex = get().severityIndex; - + 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 - sIndex.get(severity)!.add({id,warningKey}); + if (!sIndex.get(severity)!.has(compositeKey)) { + sIndex.get(severity)!.add(compositeKey); + } set({ editorWarningRegistry: wRegistry, @@ -149,17 +159,19 @@ export function editorWarningRegistry(get: ZustandGet, set: ZustandSet) : Editor }, 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; - const wRegistry = get().editorWarningRegistry; - const sIndex = get().severityIndex; - - const warning = wRegistry.get(id)!.get(warningKey); // remove from warning registry wRegistry.get(id)!.delete(warningKey); // remove from severityIndex - sIndex.get(warning!.severity)!.delete({id,warningKey}); + sIndex.get(warning.severity)!.delete(`${id}|${warningKey}`); set({ editorWarningRegistry: wRegistry, @@ -168,15 +180,15 @@ export function editorWarningRegistry(get: ZustandGet, set: ZustandSet) : Editor }, unregisterWarningsForId: (id) => { - const wRegistry = get().editorWarningRegistry; - const sIndex = get().severityIndex; + 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}); + sIndex.get(warning.severity)?.delete(`${id}|${warningKey}`); }); } diff --git a/src/pages/VisProgPage/visualProgrammingUI/nodes/StartNode.tsx b/src/pages/VisProgPage/visualProgrammingUI/nodes/StartNode.tsx index 741b190..d4bf0f6 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 "../EditorWarnings.tsx"; import {allowOnlyConnectionsFromHandle} from "../HandleRules.ts"; +import useFlowStore from "../VisProgStores.tsx"; export type StartNodeData = { @@ -25,6 +28,29 @@ 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 ( <> From 022a6708eadcdb50746d6f345d9b9adcf5a61e5f Mon Sep 17 00:00:00 2001 From: JGerla Date: Thu, 15 Jan 2026 16:03:33 +0100 Subject: [PATCH 10/14] feat: added a Warnings sidebar warnings are now displayed in the sidebar ref: N25B-450 --- package-lock.json | 16 ++- package.json | 1 + src/pages/VisProgPage/VisProg.tsx | 7 +- .../visualProgrammingUI/VisProgStores.tsx | 2 +- .../visualProgrammingUI/VisProgTypes.tsx | 2 +- .../{ => components}/EditorWarnings.tsx | 18 +++- .../components/WarningSidebar.module.css | 73 ++++++++++++++ .../components/WarningSidebar.tsx | 99 +++++++++++++++++++ .../visualProgrammingUI/nodes/StartNode.tsx | 2 +- 9 files changed, 208 insertions(+), 12 deletions(-) rename src/pages/VisProgPage/visualProgrammingUI/{ => components}/EditorWarnings.tsx (92%) create mode 100644 src/pages/VisProgPage/visualProgrammingUI/components/WarningSidebar.module.css create mode 100644 src/pages/VisProgPage/visualProgrammingUI/components/WarningSidebar.tsx 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 ef759c1..032dfb3 100644 --- a/src/pages/VisProgPage/VisProg.tsx +++ b/src/pages/VisProgPage/VisProg.tsx @@ -12,6 +12,7 @@ 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 {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'; @@ -93,7 +94,7 @@ const VisProgUI = () => { return ( -
+
{ snapToGrid fitView proOptions={{hideAttribution: true}} + style={{flexGrow: 3}} > {/* contains the drag and drop panel for nodes */} @@ -128,6 +130,7 @@ const VisProgUI = () => { +
); }; @@ -190,10 +193,8 @@ function graphReducer() { */ 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, diff --git a/src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx b/src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx index 570d0df..8305bb2 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx +++ b/src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx @@ -9,7 +9,7 @@ import { type XYPosition, } from '@xyflow/react'; import '@xyflow/react/dist/style.css'; -import { editorWarningRegistry } from "./EditorWarnings.tsx"; +import { editorWarningRegistry } from "./components/EditorWarnings.tsx"; import type { FlowState } from './VisProgTypes'; import { NodeDefaults, diff --git a/src/pages/VisProgPage/visualProgrammingUI/VisProgTypes.tsx b/src/pages/VisProgPage/visualProgrammingUI/VisProgTypes.tsx index bf1bffd..afb1024 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/VisProgTypes.tsx +++ b/src/pages/VisProgPage/visualProgrammingUI/VisProgTypes.tsx @@ -9,7 +9,7 @@ import type { OnEdgesDelete, OnNodesDelete } from '@xyflow/react'; -import type {EditorWarningRegistry} from "./EditorWarnings.tsx"; +import type {EditorWarningRegistry} from "./components/EditorWarnings.tsx"; import type {HandleRule} from "./HandleRuleLogic.ts"; import type { NodeTypes } from './NodeRegistry'; import type {FlowSnapshot} from "./EditorUndoRedo.ts"; diff --git a/src/pages/VisProgPage/visualProgrammingUI/EditorWarnings.tsx b/src/pages/VisProgPage/visualProgrammingUI/components/EditorWarnings.tsx similarity index 92% rename from src/pages/VisProgPage/visualProgrammingUI/EditorWarnings.tsx rename to src/pages/VisProgPage/visualProgrammingUI/components/EditorWarnings.tsx index 436fc21..a0c90d6 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/EditorWarnings.tsx +++ b/src/pages/VisProgPage/visualProgrammingUI/components/EditorWarnings.tsx @@ -5,12 +5,11 @@ * - 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 |-- -import type {FlowState} from "./VisProgTypes.tsx"; - - export type WarningId = NodeId | "GLOBAL_WARNINGS"; export type NodeId = string; @@ -202,3 +201,16 @@ export function editorWarningRegistry(get: ZustandGet, set: ZustandSet) : Editor }, }} + +// 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..182d99e --- /dev/null +++ b/src/pages/VisProgPage/visualProgrammingUI/components/WarningSidebar.module.css @@ -0,0 +1,73 @@ +.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; +} + +.warning-item--error { + border: 2px solid red; +} + +.warning-item--warning { + border-left: 3px solid orange; +} + +.warning-item--info { + border-left: 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..b91a17a --- /dev/null +++ b/src/pages/VisProgPage/visualProgrammingUI/components/WarningSidebar.tsx @@ -0,0 +1,99 @@ +import clsx from "clsx"; +import {useEffect, useState} from "react"; +import useFlowStore from "../VisProgStores.tsx"; +import { + warningSummary, + type WarningSeverity, type EditorWarning +} 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 }) { + return ( +
+
+ {props.warning.description} +
+ +
+ {props.warning.scope.id} + {props.warning.scope.handleId && ( + @{props.warning.scope.handleId} + )} +
+
+ ); +} \ No newline at end of file diff --git a/src/pages/VisProgPage/visualProgrammingUI/nodes/StartNode.tsx b/src/pages/VisProgPage/visualProgrammingUI/nodes/StartNode.tsx index d4bf0f6..3d6c2b2 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/nodes/StartNode.tsx +++ b/src/pages/VisProgPage/visualProgrammingUI/nodes/StartNode.tsx @@ -7,7 +7,7 @@ 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 "../EditorWarnings.tsx"; +import type {EditorWarning} from "../components/EditorWarnings.tsx"; import {allowOnlyConnectionsFromHandle} from "../HandleRules.ts"; import useFlowStore from "../VisProgStores.tsx"; From a6f24b677ff0a075853eb63b472af701237f7941 Mon Sep 17 00:00:00 2001 From: JGerla Date: Thu, 15 Jan 2026 16:09:24 +0100 Subject: [PATCH 11/14] feat: added two new warnings ref: N25B-450 --- .../components/WarningSidebar.module.css | 4 +-- .../visualProgrammingUI/nodes/EndNode.tsx | 26 ++++++++++++++++++- .../visualProgrammingUI/nodes/PhaseNode.tsx | 25 +++++++++++++++++- 3 files changed, 51 insertions(+), 4 deletions(-) diff --git a/src/pages/VisProgPage/visualProgrammingUI/components/WarningSidebar.module.css b/src/pages/VisProgPage/visualProgrammingUI/components/WarningSidebar.module.css index 182d99e..97f04b1 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/components/WarningSidebar.module.css +++ b/src/pages/VisProgPage/visualProgrammingUI/components/WarningSidebar.module.css @@ -60,11 +60,11 @@ } .warning-item--warning { - border-left: 3px solid orange; + border: 3px solid orange; } .warning-item--info { - border-left: 3px solid steelblue; + border: 3px solid steelblue; } .warning-item .meta { 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 9a48103..e80aec3 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,27 @@ 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, + 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}:source`); } + }, [connections.length, props.id, registerWarning, unregisterWarning]); + return ( <> From 5a9b78fdda24a7f93fb9b554dd7cabc684476d6a Mon Sep 17 00:00:00 2001 From: JGerla Date: Thu, 15 Jan 2026 16:24:08 +0100 Subject: [PATCH 12/14] feat: added jump to node on clicking the warning ref: N25B-450 --- .../components/WarningSidebar.module.css | 5 ++++ .../components/WarningSidebar.tsx | 27 ++++++++++++++++++- .../visualProgrammingUI/nodes/PhaseNode.tsx | 3 ++- 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/pages/VisProgPage/visualProgrammingUI/components/WarningSidebar.module.css b/src/pages/VisProgPage/visualProgrammingUI/components/WarningSidebar.module.css index 97f04b1..2a8241e 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/components/WarningSidebar.module.css +++ b/src/pages/VisProgPage/visualProgrammingUI/components/WarningSidebar.module.css @@ -53,6 +53,11 @@ gap: 8px; padding: 8px 12px; border-radius: 5px; + cursor: pointer; +} + +.warning-item:hover { + background: ButtonFace; } .warning-item--error { diff --git a/src/pages/VisProgPage/visualProgrammingUI/components/WarningSidebar.tsx b/src/pages/VisProgPage/visualProgrammingUI/components/WarningSidebar.tsx index b91a17a..fb740f6 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/components/WarningSidebar.tsx +++ b/src/pages/VisProgPage/visualProgrammingUI/components/WarningSidebar.tsx @@ -1,3 +1,4 @@ +import {useReactFlow} from "@xyflow/react"; import clsx from "clsx"; import {useEffect, useState} from "react"; import useFlowStore from "../VisProgStores.tsx"; @@ -82,8 +83,13 @@ function WarningsList(props: { warnings: EditorWarning[] }) { } function WarningListItem(props: { warning: EditorWarning }) { + const jumpToNode = useJumpToNode(); + return ( -
+
jumpToNode(props.warning.scope.id)} + >
{props.warning.description}
@@ -96,4 +102,23 @@ function WarningListItem(props: { warning: EditorWarning }) {
); +} + +function useJumpToNode() { + const { getNode, setCenter } = useReactFlow(); + + return (nodeId: string) => { + 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 } + ); + + }; } \ No newline at end of file diff --git a/src/pages/VisProgPage/visualProgrammingUI/nodes/PhaseNode.tsx b/src/pages/VisProgPage/visualProgrammingUI/nodes/PhaseNode.tsx index e80aec3..cfb287f 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/nodes/PhaseNode.tsx +++ b/src/pages/VisProgPage/visualProgrammingUI/nodes/PhaseNode.tsx @@ -46,6 +46,7 @@ export default function PhaseNode(props: NodeProps) { const {registerWarning, unregisterWarning} = useFlowStore.getState(); const connections = useNodeConnections({ id: props.id, + handleType: "target", handleId: 'data' }) @@ -61,7 +62,7 @@ export default function PhaseNode(props: NodeProps) { } if (connections.length === 0) { registerWarning(noConnectionWarning); } - else { unregisterWarning(props.id, `${noConnectionWarning.type}:source`); } + else { unregisterWarning(props.id, `${noConnectionWarning.type}:data`); } }, [connections.length, props.id, registerWarning, unregisterWarning]); return ( From 487ee30923df76e2befd653f66b008e840d292ad Mon Sep 17 00:00:00 2001 From: JGerla Date: Tue, 20 Jan 2026 10:22:08 +0100 Subject: [PATCH 13/14] feat: made jumpToNode select the node after focussing the editor ref: N25B-450 --- .../components/WarningSidebar.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/pages/VisProgPage/visualProgrammingUI/components/WarningSidebar.tsx b/src/pages/VisProgPage/visualProgrammingUI/components/WarningSidebar.tsx index fb740f6..fc3b347 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/components/WarningSidebar.tsx +++ b/src/pages/VisProgPage/visualProgrammingUI/components/WarningSidebar.tsx @@ -1,10 +1,11 @@ -import {useReactFlow} from "@xyflow/react"; +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 + type WarningSeverity, + type EditorWarning, globalWarning } from "./EditorWarnings.tsx"; import styles from "./WarningSidebar.module.css"; @@ -106,8 +107,11 @@ function WarningListItem(props: { warning: EditorWarning }) { 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; @@ -118,7 +122,10 @@ function useJumpToNode() { position!.x + width / 2, position!.y + height / 2, { zoom: 2, duration: 300 } - ); + ).then(() => { + // select the node + addSelectedNodes([nodeId]); + }); }; } \ No newline at end of file From 5d55ebaaa2b9e629ecd3a722f4cbc8df56a0b29a Mon Sep 17 00:00:00 2001 From: JGerla Date: Tue, 20 Jan 2026 11:53:51 +0100 Subject: [PATCH 14/14] feat: Added global warning for incomplete program chain ref: N25B-450 --- src/pages/VisProgPage/VisProg.tsx | 41 ++++++++++++++++++- .../visualProgrammingUI/VisProgStores.tsx | 7 +++- .../components/EditorWarnings.tsx | 1 + .../visualProgrammingUI/nodes/StartNode.tsx | 4 +- 4 files changed, 47 insertions(+), 6 deletions(-) diff --git a/src/pages/VisProgPage/VisProg.tsx b/src/pages/VisProgPage/VisProg.tsx index 032dfb3..a5f7887 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,7 @@ 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'; @@ -90,6 +91,26 @@ 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]) @@ -184,6 +205,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'); +}; /** diff --git a/src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx b/src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx index 8305bb2..dd754bf 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx +++ b/src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx @@ -9,7 +9,7 @@ import { type XYPosition, } from '@xyflow/react'; import '@xyflow/react/dist/style.css'; -import { editorWarningRegistry } from "./components/EditorWarnings.tsx"; +import {editorWarningRegistry} from "./components/EditorWarnings.tsx"; import type { FlowState } from './VisProgTypes'; import { NodeDefaults, @@ -50,7 +50,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 @@ -312,4 +312,7 @@ const useFlowStore = create(UndoRedo((set, get) => ({ })) ); + + export default useFlowStore; + diff --git a/src/pages/VisProgPage/visualProgrammingUI/components/EditorWarnings.tsx b/src/pages/VisProgPage/visualProgrammingUI/components/EditorWarnings.tsx index a0c90d6..eb5a0f6 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/components/EditorWarnings.tsx +++ b/src/pages/VisProgPage/visualProgrammingUI/components/EditorWarnings.tsx @@ -18,6 +18,7 @@ export type WarningType = | 'MISSING_INPUT' | 'MISSING_OUTPUT' | 'PLAN_IS_UNDEFINED' + | 'INCOMPLETE_PROGRAM' | string export type WarningSeverity = diff --git a/src/pages/VisProgPage/visualProgrammingUI/nodes/StartNode.tsx b/src/pages/VisProgPage/visualProgrammingUI/nodes/StartNode.tsx index 3d6c2b2..fab9b93 100644 --- a/src/pages/VisProgPage/visualProgrammingUI/nodes/StartNode.tsx +++ b/src/pages/VisProgPage/visualProgrammingUI/nodes/StartNode.tsx @@ -7,7 +7,7 @@ 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 {type EditorWarning} from "../components/EditorWarnings.tsx"; import {allowOnlyConnectionsFromHandle} from "../HandleRules.ts"; import useFlowStore from "../VisProgStores.tsx"; @@ -48,8 +48,6 @@ export default function StartNode(props: NodeProps) { if (connections.length === 0) { registerWarning(noConnectionWarning); } else { unregisterWarning(props.id, `${noConnectionWarning.type}:source`); } }, [connections.length, props.id, registerWarning, unregisterWarning]); - - return ( <>