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 ( <>