2

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?

1 Answer 1

3

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

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.