feat: made warnings undo redo safe and added warnings to phase nodes
ref: N25B-450
This commit is contained in:
@@ -1,10 +1,18 @@
|
|||||||
import type {Edge, Node} from "@xyflow/react";
|
import type {Edge, Node} from "@xyflow/react";
|
||||||
import type {StateCreator, StoreApi } from 'zustand/vanilla';
|
import type {StateCreator, StoreApi } from 'zustand/vanilla';
|
||||||
|
import type {
|
||||||
|
SeverityIndex,
|
||||||
|
WarningRegistry
|
||||||
|
} from "./components/EditorWarnings.tsx";
|
||||||
import type {FlowState} from "./VisProgTypes.tsx";
|
import type {FlowState} from "./VisProgTypes.tsx";
|
||||||
|
|
||||||
export type FlowSnapshot = {
|
export type FlowSnapshot = {
|
||||||
nodes: Node[];
|
nodes: Node[];
|
||||||
edges: Edge[];
|
edges: Edge[];
|
||||||
|
warnings: {
|
||||||
|
warningRegistry: WarningRegistry;
|
||||||
|
severityIndex: SeverityIndex;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -41,7 +49,11 @@ export const UndoRedo = (
|
|||||||
*/
|
*/
|
||||||
const getSnapshot = (state : BaseFlowState) : FlowSnapshot => (structuredClone({
|
const getSnapshot = (state : BaseFlowState) : FlowSnapshot => (structuredClone({
|
||||||
nodes: state.nodes,
|
nodes: state.nodes,
|
||||||
edges: state.edges
|
edges: state.edges,
|
||||||
|
warnings: {
|
||||||
|
warningRegistry: state.editorWarningRegistry,
|
||||||
|
severityIndex: state.severityIndex,
|
||||||
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const initialState = config(set, get, api);
|
const initialState = config(set, get, api);
|
||||||
@@ -78,6 +90,8 @@ export const UndoRedo = (
|
|||||||
set({
|
set({
|
||||||
nodes: snapshot.nodes,
|
nodes: snapshot.nodes,
|
||||||
edges: snapshot.edges,
|
edges: snapshot.edges,
|
||||||
|
editorWarningRegistry: snapshot.warnings.warningRegistry,
|
||||||
|
severityIndex: snapshot.warnings.severityIndex,
|
||||||
});
|
});
|
||||||
|
|
||||||
state.future.push(currentSnapshot); // push current to redo
|
state.future.push(currentSnapshot); // push current to redo
|
||||||
@@ -97,6 +111,8 @@ export const UndoRedo = (
|
|||||||
set({
|
set({
|
||||||
nodes: snapshot.nodes,
|
nodes: snapshot.nodes,
|
||||||
edges: snapshot.edges,
|
edges: snapshot.edges,
|
||||||
|
editorWarningRegistry: snapshot.warnings.warningRegistry,
|
||||||
|
severityIndex: snapshot.warnings.severityIndex,
|
||||||
});
|
});
|
||||||
|
|
||||||
state.past.push(currentSnapshot); // push current to undo
|
state.past.push(currentSnapshot); // push current to undo
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export type WarningType =
|
|||||||
| 'MISSING_OUTPUT'
|
| 'MISSING_OUTPUT'
|
||||||
| 'PLAN_IS_UNDEFINED'
|
| 'PLAN_IS_UNDEFINED'
|
||||||
| 'INCOMPLETE_PROGRAM'
|
| 'INCOMPLETE_PROGRAM'
|
||||||
|
| 'NOT_CONNECTED_TO_PROGRAM'
|
||||||
| string
|
| string
|
||||||
|
|
||||||
export type WarningSeverity =
|
export type WarningSeverity =
|
||||||
@@ -87,7 +88,7 @@ export type EditorWarningRegistry = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* unregisters warnings from the warningRegistry and the SeverityIndex
|
* unregisters warnings from the warningRegistry and the SeverityIndex
|
||||||
* @param {EditorWarning} warning
|
* @param {WarningId} warning
|
||||||
*/
|
*/
|
||||||
unregisterWarningsForId: (id: WarningId) => void;
|
unregisterWarningsForId: (id: WarningId) => void;
|
||||||
}
|
}
|
||||||
@@ -187,13 +188,15 @@ export function editorWarningRegistry(get: ZustandGet, set: ZustandSet) : Editor
|
|||||||
|
|
||||||
// remove from severity index
|
// remove from severity index
|
||||||
if (nodeWarnings) {
|
if (nodeWarnings) {
|
||||||
nodeWarnings.forEach((warning, warningKey) => {
|
nodeWarnings.forEach((warning) => {
|
||||||
|
const warningKey = `${warning.type}:${warning.scope.handleId}`;
|
||||||
sIndex.get(warning.severity)?.delete(`${id}|${warningKey}`);
|
sIndex.get(warning.severity)?.delete(`${id}|${warningKey}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove from warning registry
|
// remove from warning registry
|
||||||
wRegistry.delete(id);
|
wRegistry.delete(id);
|
||||||
|
console.log(wRegistry.get(id));
|
||||||
|
|
||||||
set({
|
set({
|
||||||
editorWarningRegistry: wRegistry,
|
editorWarningRegistry: wRegistry,
|
||||||
|
|||||||
@@ -19,9 +19,6 @@ export function WarningsSidebar() {
|
|||||||
? warnings
|
? warnings
|
||||||
: warnings.filter(w => w.severity === severityFilter);
|
: warnings.filter(w => w.severity === severityFilter);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className={styles.warningsSidebar}>
|
<aside className={styles.warningsSidebar}>
|
||||||
<WarningsHeader
|
<WarningsHeader
|
||||||
@@ -83,7 +80,7 @@ function WarningsList(props: { warnings: EditorWarning[] }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div>
|
<div style={{overflowY: 'auto'}}>
|
||||||
<div className={"warningGroup"}>
|
<div className={"warningGroup"}>
|
||||||
<div className={styles.warningGroupHeader}>global:</div>
|
<div className={styles.warningGroupHeader}>global:</div>
|
||||||
<div className={styles.warningsList}>
|
<div className={styles.warningsList}>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
type Node, useNodeConnections
|
type Node, useNodeConnections
|
||||||
} from '@xyflow/react';
|
} from '@xyflow/react';
|
||||||
import {useEffect} from "react";
|
import {useEffect} from "react";
|
||||||
import type {EditorWarning} from "../components/EditorWarnings.tsx";
|
import {type EditorWarning} from "../components/EditorWarnings.tsx";
|
||||||
import { Toolbar } from '../components/NodeComponents';
|
import { Toolbar } from '../components/NodeComponents';
|
||||||
import styles from '../../VisProg.module.css';
|
import styles from '../../VisProg.module.css';
|
||||||
import {SingleConnectionHandle, MultiConnectionHandle} from "../components/RuleBasedHandle.tsx";
|
import {SingleConnectionHandle, MultiConnectionHandle} from "../components/RuleBasedHandle.tsx";
|
||||||
@@ -39,16 +39,29 @@ export type PhaseNode = Node<PhaseNodeData>
|
|||||||
*/
|
*/
|
||||||
export default function PhaseNode(props: NodeProps<PhaseNode>) {
|
export default function PhaseNode(props: NodeProps<PhaseNode>) {
|
||||||
const data = props.data;
|
const data = props.data;
|
||||||
const {updateNodeData} = useFlowStore();
|
const {updateNodeData, registerWarning, unregisterWarning} = useFlowStore();
|
||||||
const updateLabel = (value: string) => updateNodeData(props.id, {...data, label: value});
|
const updateLabel = (value: string) => updateNodeData(props.id, {...data, label: value});
|
||||||
const label_input_id = `phase_${props.id}_label_input`;
|
const label_input_id = `phase_${props.id}_label_input`;
|
||||||
|
|
||||||
const {registerWarning, unregisterWarning} = useFlowStore.getState();
|
|
||||||
const connections = useNodeConnections({
|
const connections = useNodeConnections({
|
||||||
id: props.id,
|
id: props.id,
|
||||||
handleType: "target",
|
handleType: "target",
|
||||||
handleId: 'data'
|
handleId: 'data'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const phaseOutCons = useNodeConnections({
|
||||||
|
id: props.id,
|
||||||
|
handleType: "source",
|
||||||
|
handleId: 'source',
|
||||||
|
})
|
||||||
|
|
||||||
|
const phaseInCons = useNodeConnections({
|
||||||
|
id: props.id,
|
||||||
|
handleType: "target",
|
||||||
|
handleId: 'target',
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const noConnectionWarning : EditorWarning = {
|
const noConnectionWarning : EditorWarning = {
|
||||||
@@ -61,10 +74,64 @@ export default function PhaseNode(props: NodeProps<PhaseNode>) {
|
|||||||
description: "the phaseNode has no incoming goals, norms, and/or triggers"
|
description: "the phaseNode has no incoming goals, norms, and/or triggers"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (connections.length === 0) { registerWarning(noConnectionWarning); }
|
if (connections.length === 0) { registerWarning(noConnectionWarning); return; }
|
||||||
else { unregisterWarning(props.id, `${noConnectionWarning.type}:data`); }
|
unregisterWarning(props.id, `${noConnectionWarning.type}:data`);
|
||||||
}, [connections.length, props.id, registerWarning, unregisterWarning]);
|
}, [connections.length, props.id, registerWarning, unregisterWarning]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const notConnectedInfo : EditorWarning = {
|
||||||
|
scope: {
|
||||||
|
id: props.id,
|
||||||
|
handleId: undefined,
|
||||||
|
},
|
||||||
|
type: 'NOT_CONNECTED_TO_PROGRAM',
|
||||||
|
severity: "INFO",
|
||||||
|
description: "The PhaseNode is not connected to other nodes"
|
||||||
|
};
|
||||||
|
const noIncomingPhaseWarning : EditorWarning = {
|
||||||
|
scope: {
|
||||||
|
id: props.id,
|
||||||
|
handleId: 'target'
|
||||||
|
},
|
||||||
|
type: 'MISSING_INPUT',
|
||||||
|
severity: "WARNING",
|
||||||
|
description: "the phaseNode has no incoming connection from a phase or the startNode"
|
||||||
|
}
|
||||||
|
const noOutgoingPhaseWarning : EditorWarning = {
|
||||||
|
scope: {
|
||||||
|
id: props.id,
|
||||||
|
handleId: 'source'
|
||||||
|
},
|
||||||
|
type: 'MISSING_OUTPUT',
|
||||||
|
severity: "WARNING",
|
||||||
|
description: "the phaseNode has no outgoing connection to a phase or the endNode"
|
||||||
|
}
|
||||||
|
|
||||||
|
// register relevant warning and unregister others
|
||||||
|
if (phaseInCons.length === 0 && phaseOutCons.length === 0) {
|
||||||
|
registerWarning(notConnectedInfo);
|
||||||
|
unregisterWarning(props.id, `${noOutgoingPhaseWarning.type}:${noOutgoingPhaseWarning.scope.handleId}`);
|
||||||
|
unregisterWarning(props.id, `${noIncomingPhaseWarning.type}:${noIncomingPhaseWarning.scope.handleId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (phaseOutCons.length === 0) {
|
||||||
|
registerWarning(noOutgoingPhaseWarning);
|
||||||
|
unregisterWarning(props.id, `${noIncomingPhaseWarning.type}:${noIncomingPhaseWarning.scope.handleId}`);
|
||||||
|
unregisterWarning(notConnectedInfo.scope.id, notConnectedInfo.type);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (phaseInCons.length === 0) {
|
||||||
|
registerWarning(noIncomingPhaseWarning);
|
||||||
|
unregisterWarning(props.id, `${noOutgoingPhaseWarning.type}:${noOutgoingPhaseWarning.scope.handleId}`);
|
||||||
|
unregisterWarning(notConnectedInfo.scope.id, notConnectedInfo.type);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// unregister all warnings if none should be present
|
||||||
|
unregisterWarning(notConnectedInfo.scope.id, notConnectedInfo.type);
|
||||||
|
unregisterWarning(props.id, `${noOutgoingPhaseWarning.type}:${noOutgoingPhaseWarning.scope.handleId}`);
|
||||||
|
unregisterWarning(props.id, `${noIncomingPhaseWarning.type}:${noIncomingPhaseWarning.scope.handleId}`);
|
||||||
|
}, [phaseInCons.length, phaseOutCons.length, props.id, registerWarning, unregisterWarning]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Toolbar nodeId={props.id} allowDelete={true}/>
|
<Toolbar nodeId={props.id} allowDelete={true}/>
|
||||||
|
|||||||
@@ -34,10 +34,17 @@ describe("UndoRedo Middleware", () => {
|
|||||||
type: 'default',
|
type: 'default',
|
||||||
position: {x: 0, y: 0},
|
position: {x: 0, y: 0},
|
||||||
data: {label: 'A'}
|
data: {label: 'A'}
|
||||||
},
|
}
|
||||||
],
|
],
|
||||||
edges: []
|
edges: [],
|
||||||
|
warnings: {
|
||||||
|
warningRegistry: new Map(),
|
||||||
|
severityIndex: new Map()
|
||||||
|
}
|
||||||
}],
|
}],
|
||||||
|
ruleRegistry: new Map(),
|
||||||
|
editorWarningRegistry: new Map(),
|
||||||
|
severityIndex: new Map()
|
||||||
});
|
});
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
@@ -53,7 +60,11 @@ describe("UndoRedo Middleware", () => {
|
|||||||
position: {x: 0, y: 0},
|
position: {x: 0, y: 0},
|
||||||
data: {label: 'A'}
|
data: {label: 'A'}
|
||||||
}],
|
}],
|
||||||
edges: []
|
edges: [],
|
||||||
|
warnings: {
|
||||||
|
warningRegistry: {},
|
||||||
|
severityIndex: {}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
expect(state.future).toEqual([]);
|
expect(state.future).toEqual([]);
|
||||||
});
|
});
|
||||||
@@ -80,7 +91,9 @@ describe("UndoRedo Middleware", () => {
|
|||||||
position: {x: 0, y: 0},
|
position: {x: 0, y: 0},
|
||||||
data: {label: 'A'}
|
data: {label: 'A'}
|
||||||
}],
|
}],
|
||||||
edges: []
|
edges: [],
|
||||||
|
editorWarningRegistry: new Map(),
|
||||||
|
severityIndex: new Map()
|
||||||
});
|
});
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
@@ -114,7 +127,11 @@ describe("UndoRedo Middleware", () => {
|
|||||||
position: {x: 0, y: 0},
|
position: {x: 0, y: 0},
|
||||||
data: {label: 'B'}
|
data: {label: 'B'}
|
||||||
}],
|
}],
|
||||||
edges: []
|
edges: [],
|
||||||
|
warnings: {
|
||||||
|
warningRegistry: {},
|
||||||
|
severityIndex: {}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -140,7 +157,9 @@ describe("UndoRedo Middleware", () => {
|
|||||||
position: {x: 0, y: 0},
|
position: {x: 0, y: 0},
|
||||||
data: {label: 'A'}
|
data: {label: 'A'}
|
||||||
}],
|
}],
|
||||||
edges: []
|
edges: [],
|
||||||
|
editorWarningRegistry: new Map(),
|
||||||
|
severityIndex: new Map()
|
||||||
});
|
});
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
@@ -176,7 +195,11 @@ describe("UndoRedo Middleware", () => {
|
|||||||
position: {x: 0, y: 0},
|
position: {x: 0, y: 0},
|
||||||
data: {label: 'A'}
|
data: {label: 'A'}
|
||||||
}],
|
}],
|
||||||
edges: []
|
edges: [],
|
||||||
|
warnings: {
|
||||||
|
warningRegistry: {},
|
||||||
|
severityIndex: {}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -199,7 +222,9 @@ describe("UndoRedo Middleware", () => {
|
|||||||
position: {x: 0, y: 0},
|
position: {x: 0, y: 0},
|
||||||
data: {label: 'A'}
|
data: {label: 'A'}
|
||||||
}],
|
}],
|
||||||
edges: []
|
edges: [],
|
||||||
|
editorWarningRegistry: new Map(),
|
||||||
|
severityIndex: new Map()
|
||||||
});
|
});
|
||||||
|
|
||||||
act(() => { store.getState().beginBatchAction(); });
|
act(() => { store.getState().beginBatchAction(); });
|
||||||
|
|||||||
Reference in New Issue
Block a user