feat: finished basic warning system
nodes can now register warnings to prevent running the program ref: N25B-450
This commit is contained in:
@@ -90,6 +90,8 @@ const VisProgUI = () => {
|
||||
return () => window.removeEventListener('keydown', handler);
|
||||
});
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className={`${styles.innerEditorContainer} round-lg border-lg`} style={({'--flow-zoom': zoom} as CSSProperties)}>
|
||||
<ReactFlow
|
||||
@@ -120,6 +122,7 @@ const VisProgUI = () => {
|
||||
</Panel>
|
||||
<Panel position="bottom-center">
|
||||
<button onClick={() => undo()}>undo</button>
|
||||
|
||||
<button onClick={() => redo()}>Redo</button>
|
||||
</Panel>
|
||||
<Controls/>
|
||||
@@ -187,16 +190,20 @@ function graphReducer() {
|
||||
*/
|
||||
function VisProgPage() {
|
||||
const [programValidity, setProgramValidity] = useState<boolean>(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 (
|
||||
<>
|
||||
<VisualProgrammingUI/>
|
||||
<button onClick={runProgram} disabled={programValidity}>run program</button>
|
||||
<button onClick={runProgram} disabled={!programValidity}>run program</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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<WarningId , Map<WarningKey, EditorWarning>>;
|
||||
export type SeverityIndex = Map<WarningSeverity, Set<{ id: WarningId, warningKey: WarningKey}>>;
|
||||
export type SeverityIndex = Map<WarningSeverity, Set<CompositeWarningKey>>;
|
||||
|
||||
type ZustandSet = (partial: Partial<FlowState> | ((state: FlowState) => Partial<FlowState>)) => 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<NodeId, Map<WarningKey, EditorWarning>>(),
|
||||
severityIndex: new Map([
|
||||
['INFO', new Set<{ id: WarningId, warningKey: WarningKey}>()],
|
||||
['WARNING', new Set<{ id: WarningId, warningKey: WarningKey}>()]
|
||||
['INFO', new Set<CompositeWarningKey>()],
|
||||
['WARNING', new Set<CompositeWarningKey>()],
|
||||
['ERROR', new Set<CompositeWarningKey>()],
|
||||
]),
|
||||
|
||||
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}`);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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<StartNodeData>
|
||||
* @returns React.JSX.Element
|
||||
*/
|
||||
export default function StartNode(props: NodeProps<StartNode>) {
|
||||
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 (
|
||||
<>
|
||||
<Toolbar nodeId={props.id} allowDelete={false}/>
|
||||
|
||||
Reference in New Issue
Block a user