# Conflicts: # src/pages/VisProgPage/VisProg.tsx # src/pages/VisProgPage/visualProgrammingUI/nodes/BasicBeliefNode.tsx
246 lines
7.7 KiB
TypeScript
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
|