refactor: moved visualProgrammingUI into page folder
BREAKING: moved directory visualProgrammingUI and contents into VisProgPage ref: N25B-114
This commit is contained in:
@@ -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
|
||||
|
||||
94
src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx
Normal file
94
src/pages/VisProgPage/visualProgrammingUI/VisProgStores.tsx
Normal 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;
|
||||
25
src/pages/VisProgPage/visualProgrammingUI/VisProgTypes.tsx
Normal file
25
src/pages/VisProgPage/visualProgrammingUI/VisProgTypes.tsx
Normal 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;
|
||||
};
|
||||
79
src/pages/VisProgPage/visualProgrammingUI/VisProgUI.css
Normal file
79
src/pages/VisProgPage/visualProgrammingUI/VisProgUI.css
Normal 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);
|
||||
}
|
||||
108
src/pages/VisProgPage/visualProgrammingUI/VisProgUI.tsx
Normal file
108
src/pages/VisProgPage/visualProgrammingUI/VisProgUI.tsx
Normal 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;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user