Search code examples
javascriptreactjsreact-flow

Reactflow - Adding nodes by drag and drop in Custom Edges button


Please help me with my issue. I'm currently making an open-source project using Reactflow. But I got some error.

enter image description here

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?


Solution

  • Here's the answer I found. Please check the repo. We have added a node dropzone in the edge button

    https://github.com/sayedulkrm/react-flow-drag