feat: The Big One UI #47

Merged
j.gerla merged 115 commits from temp_screenshot_manual into dev 2026-01-28 08:27:30 +00:00
5 changed files with 128 additions and 20 deletions
Showing only changes of commit d6d74d4c6b - Show all commits

View File

@@ -1,10 +1,18 @@
import type {Edge, Node} from "@xyflow/react";
import type {StateCreator, StoreApi } from 'zustand/vanilla';
import type {
SeverityIndex,
WarningRegistry
} from "./components/EditorWarnings.tsx";
import type {FlowState} from "./VisProgTypes.tsx";
export type FlowSnapshot = {
nodes: Node[];
edges: Edge[];
warnings: {
warningRegistry: WarningRegistry;
severityIndex: SeverityIndex;
}
}
/**
@@ -41,7 +49,11 @@ export const UndoRedo = (
*/
const getSnapshot = (state : BaseFlowState) : FlowSnapshot => (structuredClone({
nodes: state.nodes,
edges: state.edges
edges: state.edges,
warnings: {
warningRegistry: state.editorWarningRegistry,
severityIndex: state.severityIndex,
}
}));
const initialState = config(set, get, api);
@@ -78,6 +90,8 @@ export const UndoRedo = (
set({
nodes: snapshot.nodes,
edges: snapshot.edges,
editorWarningRegistry: snapshot.warnings.warningRegistry,
severityIndex: snapshot.warnings.severityIndex,
});
state.future.push(currentSnapshot); // push current to redo
@@ -97,6 +111,8 @@ export const UndoRedo = (
set({
nodes: snapshot.nodes,
edges: snapshot.edges,
editorWarningRegistry: snapshot.warnings.warningRegistry,
severityIndex: snapshot.warnings.severityIndex,
});
state.past.push(currentSnapshot); // push current to undo

View File

@@ -19,6 +19,7 @@ export type WarningType =
| 'MISSING_OUTPUT'
| 'PLAN_IS_UNDEFINED'
| 'INCOMPLETE_PROGRAM'
| 'NOT_CONNECTED_TO_PROGRAM'
| string
export type WarningSeverity =
@@ -87,7 +88,7 @@ export type EditorWarningRegistry = {
/**
* unregisters warnings from the warningRegistry and the SeverityIndex
* @param {EditorWarning} warning
* @param {WarningId} warning
*/
unregisterWarningsForId: (id: WarningId) => void;
}
@@ -187,13 +188,15 @@ export function editorWarningRegistry(get: ZustandGet, set: ZustandSet) : Editor
// remove from severity index
if (nodeWarnings) {
nodeWarnings.forEach((warning, warningKey) => {
nodeWarnings.forEach((warning) => {
const warningKey = `${warning.type}:${warning.scope.handleId}`;
sIndex.get(warning.severity)?.delete(`${id}|${warningKey}`);
});
}
// remove from warning registry
wRegistry.delete(id);
console.log(wRegistry.get(id));
set({
editorWarningRegistry: wRegistry,

View File

@@ -19,9 +19,6 @@ export function WarningsSidebar() {
? warnings
: warnings.filter(w => w.severity === severityFilter);
return (
<aside className={styles.warningsSidebar}>
<WarningsHeader
@@ -83,7 +80,7 @@ function WarningsList(props: { warnings: EditorWarning[] }) {
)
}
return (
<div>
<div style={{overflowY: 'auto'}}>
<div className={"warningGroup"}>
<div className={styles.warningGroupHeader}>global:</div>
<div className={styles.warningsList}>

View File

@@ -4,7 +4,7 @@ import {
type Node, useNodeConnections
} from '@xyflow/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 styles from '../../VisProg.module.css';
import {SingleConnectionHandle, MultiConnectionHandle} from "../components/RuleBasedHandle.tsx";
@@ -39,16 +39,29 @@ export type PhaseNode = Node<PhaseNodeData>
*/
export default function PhaseNode(props: NodeProps<PhaseNode>) {
const data = props.data;
const {updateNodeData} = useFlowStore();
const {updateNodeData, registerWarning, unregisterWarning} = useFlowStore();
const updateLabel = (value: string) => updateNodeData(props.id, {...data, label: value});
const label_input_id = `phase_${props.id}_label_input`;
const {registerWarning, unregisterWarning} = useFlowStore.getState();
const connections = useNodeConnections({
id: props.id,
handleType: "target",
handleId: 'data'
})
const phaseOutCons = useNodeConnections({
id: props.id,
handleType: "source",
handleId: 'source',
})
const phaseInCons = useNodeConnections({
id: props.id,
handleType: "target",
handleId: 'target',
})
useEffect(() => {
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"
}
if (connections.length === 0) { registerWarning(noConnectionWarning); }
else { unregisterWarning(props.id, `${noConnectionWarning.type}:data`); }
if (connections.length === 0) { registerWarning(noConnectionWarning); return; }
unregisterWarning(props.id, `${noConnectionWarning.type}:data`);
}, [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 (
<>
<Toolbar nodeId={props.id} allowDelete={true}/>

View File

@@ -34,10 +34,17 @@ describe("UndoRedo Middleware", () => {
type: 'default',
position: {x: 0, y: 0},
data: {label: 'A'}
},
}
],
edges: []
edges: [],
warnings: {
warningRegistry: new Map(),
severityIndex: new Map()
}
}],
ruleRegistry: new Map(),
editorWarningRegistry: new Map(),
severityIndex: new Map()
});
act(() => {
@@ -53,7 +60,11 @@ describe("UndoRedo Middleware", () => {
position: {x: 0, y: 0},
data: {label: 'A'}
}],
edges: []
edges: [],
warnings: {
warningRegistry: {},
severityIndex: {}
}
});
expect(state.future).toEqual([]);
});
@@ -80,7 +91,9 @@ describe("UndoRedo Middleware", () => {
position: {x: 0, y: 0},
data: {label: 'A'}
}],
edges: []
edges: [],
editorWarningRegistry: new Map(),
severityIndex: new Map()
});
act(() => {
@@ -114,7 +127,11 @@ describe("UndoRedo Middleware", () => {
position: {x: 0, y: 0},
data: {label: 'B'}
}],
edges: []
edges: [],
warnings: {
warningRegistry: {},
severityIndex: {}
}
});
});
@@ -140,7 +157,9 @@ describe("UndoRedo Middleware", () => {
position: {x: 0, y: 0},
data: {label: 'A'}
}],
edges: []
edges: [],
editorWarningRegistry: new Map(),
severityIndex: new Map()
});
act(() => {
@@ -176,7 +195,11 @@ describe("UndoRedo Middleware", () => {
position: {x: 0, y: 0},
data: {label: 'A'}
}],
edges: []
edges: [],
warnings: {
warningRegistry: {},
severityIndex: {}
}
});
});
@@ -199,7 +222,9 @@ describe("UndoRedo Middleware", () => {
position: {x: 0, y: 0},
data: {label: 'A'}
}],
edges: []
edges: [],
editorWarningRegistry: new Map(),
severityIndex: new Map()
});
act(() => { store.getState().beginBatchAction(); });