refactor: moved visualProgrammingUI into page folder

BREAKING: moved directory visualProgrammingUI and contents into VisProgPage

ref: N25B-114
This commit is contained in:
JGerla
2025-10-08 16:31:11 +02:00
parent 47266c4ef0
commit 5b8213d5ef
7 changed files with 1 additions and 1 deletions

View File

@@ -1,5 +1,5 @@
import { Link } from 'react-router'
import VisProgUI from "../../visualProgrammingUI/VisProgUI.tsx";
import VisProgUI from "./visualProgrammingUI/VisProgUI.tsx";
//this is your css file where you can style your buttons and such

View File

@@ -0,0 +1,94 @@
import { create } from 'zustand';
import {
applyNodeChanges,
applyEdgeChanges,
addEdge,
reconnectEdge, type Edge, type Connection
} from '@xyflow/react';
import { type FlowState } from './VisProgTypes.tsx';
const initialNodes = [
{
id: 'start',
type: 'start',
position: {x: 0, y: 0},
data: {label: 'start'}
},
{
id: 'genericPhase',
type: 'phase',
position: {x: 0, y: 150},
data: {label: 'Generic Phase', number: 1},
},
{
id: 'end',
type: 'end',
position: {x: 0, y: 300},
data: {label: 'End'}
}
];
const initialEdges = [
{
id: 'start-end',
source: 'start',
target: 'end'
}
];
// this is our useStore hook that we can use in our components to get parts of the store and call actions
const useFlowStore = create<FlowState>((set, get) => ({
nodes: initialNodes,
edges: initialEdges,
edgeReconnectSuccessful: true,
onNodesChange: (changes) => {
set({
nodes: applyNodeChanges(changes, get().nodes)
});
},
onEdgesChange: (changes) => {
set({
edges: applyEdgeChanges(changes, get().edges)
});
},
// handles connection of newly created edges
onConnect: (connection) => {
set({
edges: addEdge(connection, get().edges)
});
},
// handles attempted reconnections of a previously disconnected edge
onReconnect: (oldEdge: Edge, newConnection: Connection) => {
get().edgeReconnectSuccessful = true;
set({
edges: reconnectEdge(oldEdge, newConnection, get().edges)
});
},
// Handles initiation of reconnection of edges that are manually disconnected from a node
onReconnectStart: () => {
set({
edgeReconnectSuccessful: false
});
},
// Drops the edge from the set of edges, removing it from the flow, if no successful reconnection occurred
onReconnectEnd: (_: unknown, edge: { id: string; }) => {
if (!get().edgeReconnectSuccessful) {
set({
edges: get().edges.filter((e) => e.id !== edge.id),
});
}
set({
edgeReconnectSuccessful: true
});
},
setNodes: (nodes) => {
set({ nodes });
},
setEdges: (edges) => {
set({ edges });
},
}),
);
export default useFlowStore;

View File

@@ -0,0 +1,25 @@
import {
type Edge,
type Node,
type OnNodesChange,
type OnEdgesChange,
type OnConnect,
type OnReconnect,
} from '@xyflow/react';
export type AppNode = Node;
export type FlowState = {
nodes: Node[];
edges: Edge[];
edgeReconnectSuccessful: boolean;
onNodesChange: OnNodesChange;
onEdgesChange: OnEdgesChange;
onConnect: OnConnect;
onReconnect: OnReconnect;
onReconnectStart: () => void;
onReconnectEnd: (_: unknown, edge: { id: string }) => void;
setNodes: (nodes: AppNode[]) => void;
setEdges: (edges: Edge[]) => void;
};

View File

@@ -0,0 +1,79 @@
.default-node {
padding: 10px 15px;
background-color: canvas;
border-radius: 5pt;
outline: black solid 2pt;
filter: drop-shadow(0 0 0.75rem black);
}
.default-node__norm {
padding: 10px 15px;
background-color: canvas;
border-radius: 5pt;
outline: forestgreen solid 2pt;
filter: drop-shadow(0 0 0.25rem forestgreen);
}
.default-node__phase {
padding: 10px 15px;
background-color: canvas;
border-radius: 5pt;
outline: dodgerblue solid 2pt;
filter: drop-shadow(0 0 0.25rem dodgerblue);
}
.default-node__start {
padding: 10px 15px;
background-color: canvas;
border-radius: 5pt;
outline: orange solid 2pt;
filter: drop-shadow(0 0 0.25rem orange);
}
.default-node__end {
padding: 10px 15px;
background-color: canvas;
border-radius: 5pt;
outline: red solid 2pt;
filter: drop-shadow(0 0 0.25rem red);
}
.draggable-node {
padding: 3px 10px;
background-color: canvas;
border-radius: 5pt;
outline: black solid 2pt;
filter: drop-shadow(0 0 0.75rem black);
}
.draggable-node__norm {
padding: 3px 10px;
background-color: canvas;
border-radius: 5pt;
outline: forestgreen solid 2pt;
filter: drop-shadow(0 0 0.25rem forestgreen);
}
.draggable-node__phase {
padding: 3px 10px;
background-color: canvas;
border-radius: 5pt;
outline: dodgerblue solid 2pt;
filter: drop-shadow(0 0 0.25rem dodgerblue);
}
.draggable-node__start {
padding: 3px 10px;
background-color: canvas;
border-radius: 5pt;
outline: orange solid 2pt;
filter: drop-shadow(0 0 0.25rem orange);
}
.draggable-node__end {
padding: 3px 10px;
background-color: canvas;
border-radius: 5pt;
outline: red solid 2pt;
filter: drop-shadow(0 0 0.25rem red);
}

View File

@@ -0,0 +1,108 @@
import './VisProgUI.css'
import {
Background,
Controls,
ReactFlow,
ReactFlowProvider,
MarkerType,
Panel,
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import {
StartNode,
EndNode,
PhaseNode,
NormNode
} from "./components/NodeDefinitions.tsx";
import { Sidebar } from './components/DragDropSidebar.tsx';
import useFlowStore from "./VisProgStores.tsx";
import {useShallow} from "zustand/react/shallow";
import type {FlowState} from "./VisProgTypes.tsx";
// --| config starting params for flow |--
const nodeTypes = {
start: StartNode,
end: EndNode,
phase: PhaseNode,
norm: NormNode
};
const defaultEdgeOptions = {
type: 'default',
markerEnd: {
type: MarkerType.ArrowClosed,
color: '#505050',
},
};
const selector = (state: FlowState) => ({
nodes: state.nodes,
edges: state.edges,
onNodesChange: state.onNodesChange,
onEdgesChange: state.onEdgesChange,
onConnect: state.onConnect,
onReconnectStart: state.onReconnectStart,
onReconnectEnd: state.onReconnectEnd,
onReconnect: state.onReconnect
});
// --| define ReactFlow editor |--
const VisProgUI = ()=> {
const { nodes, edges, onNodesChange, onEdgesChange, onConnect, onReconnect, onReconnectStart, onReconnectEnd } = useFlowStore(
useShallow(selector),
);
return (
<div style={{marginInline: 'auto',display: 'flex',justifySelf: 'center', padding:'10px', alignItems: 'center', width: '80vw', height: '80vh'}}>
<div style={{outlineStyle: 'solid', borderRadius: '10pt',width: '90%', height:'100%' }}>
<ReactFlow
nodes={nodes}
edges={edges}
defaultEdgeOptions={defaultEdgeOptions}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
nodeTypes={nodeTypes}
onReconnect={onReconnect}
onReconnectStart={onReconnectStart}
onReconnectEnd={onReconnectEnd}
onConnect={onConnect}
snapToGrid
fitView
proOptions={{hideAttribution: true }}
>
<Panel position="top-center" style={{marginInlineStart: 'auto', marginInlineEnd:'auto',backgroundColor: 'canvas' ,marginBottom: '0.5rem', marginTop:'auto', width: '50%', height:'7%'}}>
<Sidebar />
</Panel>
<Controls />
<Background />
</ReactFlow>
</div>
</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,
* thus facilitating the addition of node specific functions inside their node definitions
*/
function VisualProgrammingUI(){
return (
<ReactFlowProvider>
<VisProgUI />
</ReactFlowProvider>
);
}
export default VisualProgrammingUI;

View File

@@ -0,0 +1,137 @@
import { useDraggable } from '@neodrag/react';
import {
useReactFlow,
type XYPosition
} from '@xyflow/react';
import {
type ReactNode,
useCallback,
useRef,
useState
} from 'react';
// improve later to create better automatic IDs
let id = 0;
const getId = () => `dndnode_${id++}`;
interface DraggableNodeProps {
className?: string;
children: ReactNode;
nodeType: string;
onDrop: (nodeType: string, position: XYPosition) => void;
}
function DraggableNode({ className, children, nodeType, onDrop }: DraggableNodeProps) {
const draggableRef = useRef<HTMLDivElement>(null);
const [position, setPosition] = useState<XYPosition>({ x: 0, y: 0 });
// @ts-ignore
useDraggable(draggableRef, {
position: position,
onDrag: ({ offsetX, offsetY }) => {
// Calculate position relative to the viewport
setPosition({
x: offsetX,
y: offsetY,
});
},
onDragEnd: ({ event }) => {
setPosition({ x: 0, y: 0 });
onDrop(nodeType, {
x: event.clientX,
y: event.clientY,
});
},
});
return (
<div className={className === "default" ? "draggable-node" : "draggable-node" + "__" + className} ref={draggableRef}>
{children}
</div>
);
}
export function Sidebar() {
const { setNodes, screenToFlowPosition } = useReactFlow();
const handleNodeDrop = useCallback(
(nodeType: string, screenPosition: XYPosition) => {
const flow = document.querySelector('.react-flow');
const flowRect = flow?.getBoundingClientRect();
const isInFlow =
flowRect &&
screenPosition.x >= flowRect.left &&
screenPosition.x <= flowRect.right &&
screenPosition.y >= flowRect.top &&
screenPosition.y <= flowRect.bottom;
// Create a new node and add it to the flow
if (isInFlow) {
const position = screenToFlowPosition(screenPosition);
const newNode = () => {
switch (nodeType) {
case "phase":
return {
id: getId(),
type: nodeType,
position,
data: {label: `"new"`, number: (-1)},
};
case "start":
return {
id: getId(),
type: nodeType,
position,
data: {label: `new start node`},
};
case "end":
return {
id: getId(),
type: nodeType,
position,
data: {label: `new end node`},
};
case "norm":
return {
id: getId(),
type: nodeType,
position,
data: {label: `new norm node`},
};
default: {
return {
id: getId(),
type: nodeType,
position,
data: {label: `new default node`},
};
}
}
}
setNodes((nds) => nds.concat(newNode()));
}
},
[setNodes, screenToFlowPosition],
);
return (
<aside style={{padding: '10px 10px',outline: '2.5pt solid black',borderRadius: '0pt 0pt 5pt 5pt', borderColor:'dimgrey', backgroundColor:'canvas', display:'flex', flexDirection: 'column', gap: '1rem'}}>
<div className="description">
You can drag these nodes to the pane to create new nodes.
</div>
<div className="DraggableNodeContainer" style={{backgroundColor:'canvas', display: 'flex', flexDirection: 'row', gap: '1rem', justifyContent: 'center'}}>
<DraggableNode className="phase" nodeType="phase" onDrop={handleNodeDrop}>
phase Node
</DraggableNode>
<DraggableNode className="norm" nodeType="norm" onDrop={handleNodeDrop}>
norm Node
</DraggableNode>
</div>
</aside>
);
}

View File

@@ -0,0 +1,122 @@
import {Handle, NodeToolbar, Position, useReactFlow} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import '../VisProgUI.css';
// Datatypes for NodeTypes
type defaultNodeData = {
label: string;
};
type startNodeData = defaultNodeData;
type endNodeData = defaultNodeData;
type normNodeData = defaultNodeData;
type phaseNodeData = defaultNodeData & {
number: number;
};
export type nodeData = defaultNodeData | startNodeData | phaseNodeData | endNodeData;
// Node Toolbar definition, contains node delete functionality
type ToolbarProps= {
nodeId: string;
allowDelete: boolean;
};
export function Toolbar({nodeId, allowDelete}:ToolbarProps) {
const { setNodes, setEdges } = useReactFlow();
const handleDelete = () => {
setNodes((nds) => nds.filter((n) => n.id !== nodeId));
setEdges((eds) => eds.filter((e) => e.source !== nodeId && e.target !== nodeId));
};
return (
<NodeToolbar >
<button className="Node-toolbar__deletebutton" onClick={handleDelete} disabled={!allowDelete}>delete</button>
</NodeToolbar>);
}
// Definitions of Nodes
// Start Node definition:
type StartNodeProps = {
id: string;
data: startNodeData;
};
export const StartNode= ({ id, data }: StartNodeProps) => {
return (
<>
<Toolbar nodeId={id} allowDelete={false}/>
<div className="default-node__start">
<div> data test {data.label} </div>
<Handle type="source" position={Position.Right} id="start" />
</div>
</>
);
};
// End node definition:
type EndNodeProps = {
id: string;
data: endNodeData;
};
export const EndNode= ({ id, data }: EndNodeProps) => {
return (
<>
<Toolbar nodeId={id} allowDelete={false}/>
<div className="default-node__end">
<div> {data.label} </div>
<Handle type="target" position={Position.Left} id="end" />
</div>
</>
);
};
// Phase node definition:
type PhaseNodeProps = {
id: string;
data: phaseNodeData;
};
export const PhaseNode= ({ id, data }: PhaseNodeProps) => {
return (
<>
<Toolbar nodeId={id} allowDelete={true}/>
<div className="default-node__phase">
<div> phase {data.number} {data.label} </div>
<Handle type="target" position={Position.Left} id="target" />
<Handle type="target" position={Position.Bottom } id="norms" />
<Handle type="source" position={Position.Right} id="source" />
</div>
</>
);
};
// Norm node definition:
type NormNodeProps = {
id: string;
data: normNodeData;
};
export const NormNode= ({ id, data }: NormNodeProps) => {
return (
<>
<Toolbar nodeId={id} allowDelete={true}/>
<div className="default-node__norm">
<div> Norm {data.label} </div>
<Handle type="source" position={Position.Right} id="NormSource" />
</div>
</>
);
};