From 9df46c90a379ab8b6fa77836610d89c93661b01a Mon Sep 17 00:00:00 2001 From: JGerla Date: Wed, 1 Oct 2025 13:29:32 +0200 Subject: [PATCH] feat: added drag and drop menu for adding new nodes to flow added a sidebar that supports drag and dropping new nodes from the sidebar into the flow editor. also added a new package (neodrag) for easy draggable behavior outside the reactFlow editor. ref: N25B-114 --- package-lock.json | 7 ++ package.json | 1 + src/visualProgrammingUI/VisProgUI.tsx | 59 ++++++---- .../components/DragDropSidebar.tsx | 104 ++++++++++++++++++ 4 files changed, 150 insertions(+), 21 deletions(-) create mode 100644 src/visualProgrammingUI/components/DragDropSidebar.tsx diff --git a/package-lock.json b/package-lock.json index c20c730..81c5e48 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "pepperplus-ui", "version": "0.0.0", "dependencies": { + "@neodrag/react": "^2.3.1", "@xyflow/react": "^12.8.6", "react": "^19.1.1", "react-dom": "^19.1.1" @@ -1006,6 +1007,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@neodrag/react": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@neodrag/react/-/react-2.3.1.tgz", + "integrity": "sha512-mOVefo3mFmaVLs9PB5F5wMXnnclG81qjOaPHyf8YZUnw/Ciz0pAqyJDwDJk0nPTIK5I2x1JdjXSchGNdCxZNRQ==", + "license": "MIT" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", diff --git a/package.json b/package.json index 2757ba3..cbea372 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "preview": "vite preview" }, "dependencies": { + "@neodrag/react": "^2.3.1", "@xyflow/react": "^12.8.6", "react": "^19.1.1", "react-dom": "^19.1.1" diff --git a/src/visualProgrammingUI/VisProgUI.tsx b/src/visualProgrammingUI/VisProgUI.tsx index 48c987f..55c5a85 100644 --- a/src/visualProgrammingUI/VisProgUI.tsx +++ b/src/visualProgrammingUI/VisProgUI.tsx @@ -8,6 +8,7 @@ import { Background, Controls, ReactFlow, + ReactFlowProvider, useNodesState, useEdgesState, reconnectEdge, @@ -23,6 +24,8 @@ import { PhaseNode } from "./components/NodeDefinitions.tsx"; +import { Sidebar } from './components/DragDropSidebar.tsx'; + const nodeTypes = { start: StartNode, end: EndNode, @@ -59,7 +62,7 @@ const defaultEdgeOptions = { }, }; -const VisualProgrammingUI = ()=> { +const VisProgUI = ()=> { const edgeReconnectSuccessful = useRef(true); const [nodes, , onNodesChange] = useNodesState(initialNodes); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); @@ -89,27 +92,41 @@ const VisualProgrammingUI = ()=> { }, [setEdges]); return ( -
- - - - +
+
+ + + + +
+
+ +
+ ); }; -export default VisualProgrammingUI \ No newline at end of file +function VisualProgrammingUI(){ + return ( + + + + ); +} + +export default VisualProgrammingUI; \ No newline at end of file diff --git a/src/visualProgrammingUI/components/DragDropSidebar.tsx b/src/visualProgrammingUI/components/DragDropSidebar.tsx new file mode 100644 index 0000000..9c1e3dd --- /dev/null +++ b/src/visualProgrammingUI/components/DragDropSidebar.tsx @@ -0,0 +1,104 @@ +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(null); + const [position, setPosition] = useState({ 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 ( +
+ {children} +
+ ); +} + +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 = { + id: getId(), + type: nodeType, + position, + data: { label: `${nodeType} node` }, + }; + + setNodes((nds) => nds.concat(newNode)); + } + }, + [setNodes, screenToFlowPosition], + ); + + return ( + + ); +} \ No newline at end of file