Please help me with my issue. I'm currently making an open-source project using Reactflow. But I got some error.
I want to Add my drag and drop node on my "+" Edge Button. The elements are dropped on (+) it should have the same effect as the menu popup. How can I do that? Please help me.
Here's the live link: https://whimsical-pegasus-f563db.netlify.app/
The code is really big. I think the problem is When I drag and drop the "Nodes", the "Nodes" can't get Id. And that's why it shows an error.
Here's some important File:
Automation.jsx :
import React, { useCallback, useEffect, useRef, useState } from "react";
import ReactFlow, {
Controls,
MiniMap,
ReactFlowProvider,
useEdgesState,
useNodesState,
} from "reactflow";
// File imports
import "./Automation.css";
import { nodeTypes } from "./Nodes/index.jsx";
import { edgeTypes } from "./Edges/index.jsx";
import { getLayoutedElements } from "./Utils/WorkflowLayoutUtils.jsx";
import Sidebar from "./Sidebar/Sidebar";
import { getUpdatedElementsAfterNodeAddition } from "./Utils/WorkflowElementUtils";
export const Automation = (props) => {
const { elements, onAddNodeCallback } = props;
const reactFlowWrapper = useRef(null);
const [nodes, setNodes, onNodesChange] = useNodesState();
const [edges, setEdges, onEdgesChange] = useEdgesState();
const [reactFlowInstance, setReactFlowInstance] = useState(null);
useEffect(() => {
const layoutElements = getLayoutedElements(elements);
const layoutNodes = layoutElements.filter((x) => x.position);
const layoutEdges = layoutElements.filter((x) => !x.position);
setNodes(layoutNodes);
setEdges(layoutEdges);
}, [elements]);
const onConnect = useCallback(
(params) => setEdges((eds) => eds.concat(params)), // Modified: Concatenate edges
[setEdges]
);
// ============================>
// ==============================>
let id = 0;
const getId = () => `dndnode_${id++}`;
const onDrop = useCallback(
(event) => {
event.preventDefault();
const reactFlowBounds =
reactFlowWrapper.current.getBoundingClientRect();
const type = event.dataTransfer.getData("application/reactflow");
// check if the dropped element is valid
if (typeof type === "undefined" || !type) {
return;
}
const position = reactFlowInstance.project({
x: event.clientX - reactFlowBounds.left,
y: event.clientY - reactFlowBounds.top,
});
const newNode = {
id: getId(),
type,
position,
data: { label: `${type} node` },
};
setNodes((nds) => nds.concat(newNode));
// ===========
// setNodes((elements) =>
// getUpdatedElementsAfterNodeAddition({
// elements,
// newNode: newNode,
// targetEdgeId: "e1-2",
// })
// );
// ===========
},
[reactFlowInstance, setNodes]
);
const onDragOver = useCallback((event) => {
event.preventDefault();
event.dataTransfer.dropEffect = "move";
}, []);
// =================================>
return (
<div className="AutomationCanvas">
<ReactFlowProvider>
<div ref={reactFlowWrapper} className="reactflow-wrapper">
<ReactFlow
nodes={nodes}
edges={edges}
// nodesDraggable={false}
// nodesConnectable={false}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
// zoomOnScroll={false}
// zoomOnPinch={false}
// panOnScroll
// panOnDrag
// preventScrolling
onConnect={onConnect}
fitView
onInit={setReactFlowInstance}
onDrop={onDrop}
onDragOver={onDragOver}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
>
<Controls
// showInteractive={false}
className="Controls"
/>
<MiniMap />
</ReactFlow>
</div>
</ReactFlowProvider>
</div>
);
};
const Layout = (props) => (
<ReactFlowProvider>
<Automation {...props} />
</ReactFlowProvider>
);
export default Layout;
App.jsx:
import React from "react";
import _ from "lodash";
import "antd/dist/reset.css";
import "./index.scss";
import { getIncomers, getOutgoers } from "react-flow-renderer";
// File Importing from folder
import Layout from "./Automation.jsx";
import { initialElements } from "./Data/Elements1.jsx";
import { getUpdatedElementsAfterNodeAddition } from "./Utils/WorkflowElementUtils.jsx";
import Sidebar from "./Sidebar/Sidebar";
const App = () => {
const [elements, setElements] = React.useState([]);
const onAddNodeCallback = ({ id, type }) => {
setElements((elements) =>
getUpdatedElementsAfterNodeAddition({
elements,
targetEdgeId: id,
type,
onDeleteNodeCallback,
onNodeClickCallback,
onAddNodeCallback,
})
);
};
const onDeleteNodeCallback = (id) => {
setElements((elements) => {
const clonedElements = _.cloneDeep(elements);
const incomingEdges = clonedElements.filter((x) => x.target === id);
const outgoingEdges = clonedElements.filter((x) => x.source === id);
const updatedIncomingEdges = incomingEdges.map((x) => ({
...x,
target: outgoingEdges[0].target,
}));
const filteredElements = clonedElements.filter(
(x) =>
x.id !== id &&
x.target !== incomingEdges[0].target &&
x.source !== outgoingEdges[0].source
);
filteredElements.push(...updatedIncomingEdges);
return filteredElements;
});
};
const onNodeClickCallback = (id) => {
setElements((elements) => {
const currentNode = elements.find((x) => x.id === id);
const nodes = elements.filter((x) => x.position);
const edges = elements.filter((x) => !x.position);
console.error({
incomers: getIncomers(currentNode, nodes, edges),
outgoers: getOutgoers(currentNode, nodes, edges),
});
return elements;
});
alert(`You clicked the "${id}" node`);
};
React.useEffect(() => {
const nodes = initialElements
.filter((x) => !x.target)
.map((x) => ({
...x,
data: { ...x.data, onDeleteNodeCallback, onNodeClickCallback },
}));
const edges = initialElements
.filter((x) => x.target)
.map((x) => ({ ...x, data: { ...x.data, onAddNodeCallback } }));
setElements([...nodes, ...edges]);
}, []);
return (
<div className="App">
<Layout elements={elements} />
<Sidebar />
</div>
);
};
export default App;
Edges:
import EdgeAddButton from "../Buttons/EdgeAddButton/EdgeAddButton.jsx";
import "./Style.scss";
import {
getEdgeCenter,
getBezierPath,
getMarkerEnd,
} from "react-flow-renderer";
const [buttonWidth, buttonHeight] = [100, 40];
export const Condition = (props) => {
const {
id,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
arrowHeadType,
markerEndId,
data,
} = props;
const edgePath = getBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
});
const markerEnd = getMarkerEnd(arrowHeadType, markerEndId);
const [edgeCenterX, edgeCenterY] = getEdgeCenter({
sourceX,
sourceY,
targetX,
targetY,
});
const { isAddButtonHidden } = data;
return (
<>
<path
id={id}
d={edgePath}
markerEnd={markerEnd}
className="react-flow__edge-path"
/>
{isAddButtonHidden ? null : (
<>
<foreignObject
width={buttonWidth}
height={buttonHeight}
x={edgeCenterX - buttonWidth / 2}
y={edgeCenterY - buttonHeight / 2}
requiredExtensions="http://www.w3.org/1999/xhtml"
>
<EdgeAddButton
{...props}
onClick={() => console.log("clicked")}
style={{ width: buttonWidth, height: buttonHeight }}
/>
</foreignObject>
</>
)}
</>
);
};
Workflow Elements:
import { v4 as uuidv4 } from "uuid";
import _ from "lodash";
const position = { x: 0, y: 0 };
const getTitleAndDescription = (type) => {
switch (type) {
case "email":
return { title: "Email", description: "Send email to contacts." };
case "sms":
return { title: "Sms", description: "Send sms to contacts." };
case "waitThenCheck":
return {
title: "New Rule",
description: "Check behaviour of the Rule",
};
case "end":
return { title: "End", description: "Process ends" };
default:
return { title: "", description: "" };
}
};
const getUpdatedElementsAfterActionNodeAddition = ({
elements,
newNodeId,
targetNodeId,
onAddNodeCallback,
}) => {
const clonedElements = _.cloneDeep(elements);
const newEdge = {
id: uuidv4(),
source: newNodeId,
target: targetNodeId,
type: "condition",
data: { onAddNodeCallback },
};
clonedElements.push(newEdge);
return clonedElements;
};
const getUpdatedElementsAfterEndNodeAddition = () => {};
const getUpdatedElementsAfterRuleNodeAdditon = ({
elements,
newNodeId,
targetNodeId,
onAddNodeCallback,
}) => {
const clonedElements = _.cloneDeep(elements);
const emptyNode1Id = uuidv4();
const emptyNode2Id = uuidv4();
const mergeNodeId = uuidv4();
const emptyNode1 = {
id: emptyNode1Id,
type: "empty",
data: {},
position,
height: 6,
// width: 40,
};
const emptyNode2 = {
id: emptyNode2Id,
type: "empty",
data: {},
position,
height: 6,
// width: 40,
};
const mergeNode = {
id: mergeNodeId,
type: "empty",
data: {},
position,
height: 6,
};
const ruleNodeToEmptyNodeEdge1 = {
id: uuidv4(),
source: newNodeId,
target: emptyNode1Id,
type: "condition",
// animated: true,
data: { onAddNodeCallback },
};
const emptyNode1ToMergeNodeEdge = {
id: uuidv4(),
source: emptyNode1Id,
target: mergeNodeId,
type: "condition",
// animated: true,
data: { onAddNodeCallback, isAddButtonHidden: true },
};
const ruleNodeToEmptyNodeEdge2 = {
id: uuidv4(),
source: newNodeId,
target: emptyNode2Id,
type: "condition",
// animated: true,
data: { onAddNodeCallback },
};
const emptyNode2ToMergeNodeEdge = {
id: uuidv4(),
source: emptyNode2Id,
target: mergeNodeId,
type: "condition",
// animated: true,
data: { onAddNodeCallback, isAddButtonHidden: true },
};
const mergeNodeEdge = {
id: uuidv4(),
source: mergeNodeId,
target: targetNodeId,
type: "condition",
data: { onAddNodeCallback },
mergeNodeOfParentId: newNodeId,
};
clonedElements.push(
...[
emptyNode1,
emptyNode2,
ruleNodeToEmptyNodeEdge1,
emptyNode1ToMergeNodeEdge,
ruleNodeToEmptyNodeEdge2,
emptyNode2ToMergeNodeEdge,
mergeNode,
mergeNodeEdge,
]
);
console.error({ clonedElements });
return clonedElements;
};
const getUpdatedElementsAfterNodeAddition = ({
elements,
targetEdgeId,
type,
onDeleteNodeCallback,
onNodeClickCallback,
onAddNodeCallback,
position,
}) => {
const newNodeId = uuidv4();
const { title, description } = getTitleAndDescription(type);
const newNode = {
id: newNodeId,
type,
data: {
title,
description,
onNodeClickCallback,
onDeleteNodeCallback,
},
position,
};
const clonedElements = _.cloneDeep(elements);
const targetEdgeIndex = clonedElements.findIndex(
(x) => x.id === targetEdgeId
);
const targetEdge = elements[targetEdgeIndex];
// Check if targetEdge is defined before accessing its properties
if (targetEdge) {
const { target: targetNodeId } = targetEdge;
const updatedTargetEdge = { ...targetEdge, target: newNodeId };
clonedElements[targetEdgeIndex] = updatedTargetEdge;
clonedElements.push(newNode);
switch (type) {
case "end":
return getUpdatedElementsAfterEndNodeAddition();
case "waitThenCheck":
return getUpdatedElementsAfterRuleNodeAdditon({
elements: clonedElements,
newNodeId,
targetNodeId,
onAddNodeCallback,
});
default:
return getUpdatedElementsAfterActionNodeAddition({
elements: clonedElements,
newNodeId,
newNode,
targetNodeId,
onAddNodeCallback,
});
}
} else {
// Handle the case when targetEdge is undefined
console.error("Target edge is undefined.");
return elements; // Return the original elements array
}
};
// ================
//
//
// const getUpdatedElementsAfterNodeAddition = ({
// elements,
// targetEdgeId,
// type,
// onDeleteNodeCallback,
// onNodeClickCallback,
// onAddNodeCallback,
// }) => {
// const newNodeId = uuidv4();
// const { title, description } = getTitleAndDescription(type);
// const newNode = {
// id: newNodeId,
// type,
// data: {
// title,
// description,
// onNodeClickCallback,
// onDeleteNodeCallback,
// },
// position,
// };
// const clonedElements = _.cloneDeep(elements);
// const targetEdgeIndex = clonedElements.findIndex(
// (x) => x.id === targetEdgeId
// );
// const targetEdge = elements[targetEdgeIndex];
// const { target: targetNodeId } = targetEdge;
// const updatedTargetEdge = { ...targetEdge, target: newNodeId };
// clonedElements[targetEdgeIndex] = updatedTargetEdge;
// clonedElements.push(newNode);
// switch (type) {
// case "end":
// return getUpdatedElementsAfterEndNodeAddition();
// case "waitThenCheck":
// return getUpdatedElementsAfterRuleNodeAdditon({
// elements: clonedElements,
// newNodeId,
// targetNodeId,
// onAddNodeCallback,
// });
// default:
// return getUpdatedElementsAfterActionNodeAddition({
// elements: clonedElements,
// newNodeId,
// newNode,
// targetNodeId,
// onAddNodeCallback,
// });
// }
// };
export { getUpdatedElementsAfterNodeAddition };
Thank You. And here's the main code: https://github.com/sayedulkrm/react-flow-drag
Is it possible for the "+" button to listen while doing drag and drop?
Here's the answer I found. Please check the repo. We have added a node dropzone
in the edge
button