Files
pepperplus-ui/src/pages/VisProgPage/VisProg.tsx
JGerla 0ac5a1973c Merge branch 'feat/editor-user-feedback' into temp_screenshot_manual
# Conflicts:
#	src/pages/VisProgPage/VisProg.tsx
#	src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode.tsx
2026-01-27 10:57:46 +01:00

246 lines
7.7 KiB
TypeScript

import {
Background,
Controls,
Panel,
ReactFlow,
ReactFlowProvider,
MarkerType, getOutgoers
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import warningStyles from './visualProgrammingUI/components/WarningSidebar.module.css'
import {type CSSProperties, useEffect, useState} from "react";
import {useShallow} from 'zustand/react/shallow';
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 useFlowStore from './visualProgrammingUI/VisProgStores.tsx';
import type {FlowState} from './visualProgrammingUI/VisProgTypes.tsx';
import styles from './VisProg.module.css'
import {NodeTypes} from './visualProgrammingUI/NodeRegistry.ts';
import SaveLoadPanel from './visualProgrammingUI/components/SaveLoadPanel.tsx';
import MonitoringPage from '../MonitoringPage/MonitoringPage.tsx';
import { graphReducer, runProgramm } from './VisProgLogic.ts';
// --| config starting params for flow |--
/**
* defines how the default edge looks inside the editor
*/
const DEFAULT_EDGE_OPTIONS = {
type: 'default',
markerEnd: {
type: MarkerType.ArrowClosed,
color: '#505050',
},
};
/**
* defines what functions in the FlowState store map to which names,
* @param state
*/
const selector = (state: FlowState) => ({
nodes: state.nodes,
edges: state.edges,
onNodesChange: state.onNodesChange,
onNodesDelete: state.onNodesDelete,
onEdgesDelete: state.onEdgesDelete,
onEdgesChange: state.onEdgesChange,
onConnect: state.onConnect,
onReconnectStart: state.onReconnectStart,
onReconnectEnd: state.onReconnectEnd,
onReconnect: state.onReconnect,
undo: state.undo,
redo: state.redo,
beginBatchAction: state.beginBatchAction,
endBatchAction: state.endBatchAction,
scrollable: state.scrollable
});
// --| define ReactFlow editor |--
/**
* Defines the ReactFlow visual programming editor component
* any implementations of editor logic should be encapsulated where possible
* so the Component definition stays as readable as possible
* @constructor
*/
const VisProgUI = () => {
const {
nodes, edges,
onNodesChange,
onNodesDelete,
onEdgesDelete,
onEdgesChange,
onConnect,
onReconnect,
onReconnectStart,
onReconnectEnd,
undo,
redo,
beginBatchAction,
endBatchAction,
scrollable
} = useFlowStore(useShallow(selector)); // instructs the editor to use the corresponding functions from the FlowStore
const [zoom, setZoom] = useState(1);
// adds ctrl+z and ctrl+y support to respectively undo and redo actions
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (e.ctrlKey && e.key === 'z') undo();
if (e.ctrlKey && e.key === 'y') redo();
};
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])
return (
<div className={`${styles.innerEditorContainer} round-lg border-lg flex-row`} style={({'--flow-zoom': zoom} as CSSProperties)}>
<ReactFlow
nodes={nodes}
edges={edges}
defaultEdgeOptions={DEFAULT_EDGE_OPTIONS}
nodeTypes={NodeTypes}
onNodesChange={onNodesChange}
onNodesDelete={onNodesDelete}
onEdgesDelete={onEdgesDelete}
onEdgesChange={onEdgesChange}
onReconnect={onReconnect}
onReconnectStart={onReconnectStart}
onReconnectEnd={onReconnectEnd}
onConnect={onConnect}
onNodeDragStart={beginBatchAction}
onNodeDragStop={endBatchAction}
preventScrolling={scrollable}
onMove={(_, viewport) => setZoom(viewport.zoom)}
reconnectRadius={15}
snapToGrid
fitView
proOptions={{hideAttribution: true}}
style={{flexGrow: 3}}
>
<Panel position="top-center" className={styles.dndPanel}>
<DndToolbar/> {/* contains the drag and drop panel for nodes */}
</Panel>
<Panel position = "bottom-left" className={styles.saveLoadPanel}>
<SaveLoadPanel></SaveLoadPanel>
</Panel>
<Panel position="bottom-center">
<button onClick={() => undo()}>Undo</button>
<button onClick={() => redo()}>Redo</button>
</Panel>
<Panel position="center-right" className={warningStyles.warningsSidebar}>
<WarningsSidebar/>
</Panel>
<Controls/>
<Background/>
</ReactFlow>
</div>
);
};
/**
* Places the VisProgUI component inside a ReactFlowProvider
*
* Wrapping the editor component inside a ReactFlowProvider
* allows us to access and interact with the components inside the editor, outside the editor definition,
* thus facilitating the addition of node specific functions inside their node definitions
*/
function VisualProgrammingUI() {
return (
<ReactFlowProvider>
<VisProgUI/>
</ReactFlowProvider>
);
}
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);
return !!next;
}
return checkForCompleteChain('start');
};
/**
* houses the entire page, so also UI elements
* that are not a part of the Visual Programming UI
* @constructor
*/
function VisProgPage() {
const [showSimpleProgram, setShowSimpleProgram] = useState(false);
const [programValidity, setProgramValidity] = useState<boolean>(true);
const {isProgramValid, severityIndex} = useFlowStore();
const setProgramState = useProgramStore((state) => state.setProgramState);
const validity = () => {return isProgramValid();}
useEffect(() => {
setProgramValidity(validity);
// 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]);
const processProgram = () => {
const phases = graphReducer(); // reduce graph
setProgramState({ phases }); // <-- save to store
setShowSimpleProgram(true); // show SimpleProgram
runProgramm(); // send to backend if needed
};
if (showSimpleProgram) {
return (
<div>
<button className={styles.backButton} onClick={() => setShowSimpleProgram(false)}>
Back to Editor
</button>
<MonitoringPage/>
</div>
);
}
return (
<>
<VisualProgrammingUI/>
<button onClick={processProgram} disabled={!programValidity}>Run Program</button>
</>
)
}
export default VisProgPage