xyflow / xyflow

React Flow | Svelte Flow - Powerful open source libraries for building node-based UIs with React (https://reactflow.dev) or Svelte (https://svelteflow.dev). Ready out-of-the-box and infinitely customizable.
https://xyflow.com
MIT License
22.07k stars 1.46k forks source link

useZoomPanHelper() issue #945

Closed CamboimGabriel closed 3 years ago

CamboimGabriel commented 3 years ago

Hi, I'm trying just to call fitView and this error appears, trying search but found nothing, what can I do?

I put just this line of code: const { fitView } = useZoomPanHelper();

image

leejustin commented 3 years ago

I believe you need to wrap your ReactFlow with a ReactFlowProvider: https://reactflow.dev/examples/provider/

CamboimGabriel commented 3 years ago

But I'm already with provider

import React, { useState, useRef, useCallback, useEffect } from "react";
import ReactFlow, {
  Controls,
  addEdge,
  removeElements,
  ReactFlowProvider,
  Background,
  isNode,
  useStoreState,
  useZoomPanHelper,
  MiniMap,
} from "react-flow-renderer";
import { Sidebar } from "../playground/TestComponents";
import "../styles/styles.css";
import ConditionalNode from "../playground/ConditionalNode";
import { v4 as uuid } from "uuid";
import ConditionalEndNode from "../playground/ConditionalEndNode";
import ParallelNode from "../playground/ParallelNode";
import ParallelEndNode from "../playground/ParallelEndNode";
import TaskNode from "../playground/TaskNode";
import EventStartNode from "../playground/EventStartNode";
import EventEndNode from "../playground/EventEndNode";
import dagre from "dagre";
import { useHistory } from "react-router-dom";
import { connect, useDispatch, useSelector } from "react-redux";
import {
  addHandleClick,
  addLayoutElements,
  connectElement,
  dropElement,
  editFlowTitle,
  handleAddModelFlow,
  handleEditModelFlow,
  removeElement,
  updateElement,
} from "../actions/flows";
import CustomEdge from "../playground/CustomEdge";
import Popover from "@material-ui/core/Popover";
import { findFlow } from "../selectors/flows";
import { fetchFlows } from "../reducers/flows";

const dagreGraph = new dagre.graphlib.Graph();

dagreGraph.setDefaultEdgeLabel(() => ({}));

const getLayoutedElements = (
  elements,
  direction = elements[0].sourcePosition === "bottom" ? "TB" : "LR"
) => {
  const clone = JSON.parse(JSON.stringify(elements));

  const isHorizontal = direction === "LR";
  dagreGraph.setGraph({ rankdir: direction });
  clone.forEach((el) => {
    if (isNode(el)) {
      dagreGraph.setNode(el.id, { width: 150, height: 50 });
    } else {
      dagreGraph.setEdge(el.source, el.target);
    }
  });
  dagre.layout(dagreGraph);
  return clone.map((el) => {
    if (isNode(el)) {
      const nodeWithPosition = dagreGraph.node(el.id);
      el.targetPosition = isHorizontal ? "left" : "top";
      el.sourcePosition = isHorizontal ? "right" : "bottom";

      el.position = {
        x: nodeWithPosition.x + Math.random() / 1000,
        y: nodeWithPosition.y,
      };
    }

    return el;
  });
};

const FlowEditionPage = ({ location }) => {
  const params = new URLSearchParams(location.search);
  const flow = useSelector((state) =>
    findFlow(state.flows.entities, params.get("flowId"))
  );
  const dispatch = useDispatch();
  const [error, setError] = useState("");
  const [edgeId, setEdgeId] = useState("");
  const [nodeId, setNodeId] = useState("");
  const history = useHistory();
  const [text, setText] = useState("Padrão");
  const [anchorEl, setAnchorEl] = React.useState(null);
  const open = Boolean(anchorEl);
  const id = open ? "simple-popover" : undefined;
  const elements = flow && flow.elements;
  const flowId = flow && flow._id;
  const reactFlowWrapper = useRef(null);
  const [reactFlowInstance, setReactFlowInstance] = useState(null);
  const flowStatus = useSelector((state) => state.flows.status);

  const handleClick = (event, id, edges, nodes) => {
    const test =
      nodes.find((e) => e.id === id) !== undefined
        ? nodes.find((e) => e.id === id)
        : edges.find((e) => e.id === id);

    if (isNode(test, elements)) {
      setText(test.data.label);
      setNodeId(id);
      setEdgeId(null);
    } else {
      setText(test.data.text);
      setEdgeId(id);
      setNodeId(null);
    }
    setAnchorEl(event.currentTarget);
  };

  const handleClose = () => {
    setAnchorEl(null);
  };

  useEffect(() => {
    dispatch(updateElement({ nodeId, edgeId, text, _id: flowId }));
  }, [dispatch, edgeId, flowId, nodeId, text]);

  const onConnect = (params) => {
    if (elements.find((e) => e.id === params.source).type === "conditional") {
      if (params.sourceHandle === "a")
        dispatch(
          connectElement({
            _id: flowId,
            params: {
              ...params,
              type: "customEdge",
              data: { handleClick, text: "Etiqueta 1" },
            },
          })
        );
      else
        dispatch(
          connectElement({
            _id: flowId,
            params: {
              ...params,
              type: "customEdge",
              data: { handleClick, text: "Etiqueta 2" },
            },
          })
        );
    } else
      dispatch(
        connectElement({
          _id: flowId,
          params,
        })
      );
  };
  const onElementsRemove = (elementsToRemove) =>
    dispatch(removeElement({ elementsToRemove, _id: flowId }));
  const onLoad = (_reactFlowInstance) => {
    _reactFlowInstance.fitView();
    setReactFlowInstance(_reactFlowInstance);
  };
  const onDragOver = (event) => {
    event.preventDefault();
    event.dataTransfer.dropEffect = "move";
  };
  const onDrop = (event) => {
    event.preventDefault();
    const reactFlowBounds = reactFlowWrapper.current.getBoundingClientRect();
    const type = event.dataTransfer.getData("application/reactflow");
    const position = reactFlowInstance.project({
      x: event.clientX - reactFlowBounds.left,
      y: event.clientY - reactFlowBounds.top,
    });
    const newNode = {
      id: uuid(),
      type,
      position,
      data: {
        label:
          type === "conditional"
            ? "Início da condicional"
            : type === "eventEnd"
            ? "Evento de fim"
            : type === "eventStart"
            ? "Evento de início"
            : type === "task"
            ? "Tarefa"
            : type === "conditionalEnd"
            ? "Fim da condicional"
            : type === "parallel"
            ? "Paralelo"
            : type === "parallelEnd"
            ? "Fim do paralelo"
            : null,
        status: "model",
        handleClick,
        expiration: {
          number: 5,
          time: "days",
        },
        subtasks: [],
      },
    };
    dispatch(dropElement({ _id: flowId, newNode }));
  };

  const onLayout = useCallback(
    (direction) => {
      const layoutedElements = getLayoutedElements(elements, direction);

      dispatch(
        addLayoutElements({ layoutedElements, _id: params.get("flowId") })
      );
    },
    [dispatch, elements, params]
  );

  const nodeTypes = {
    conditional: ConditionalNode,
    conditionalEnd: ConditionalEndNode,
    parallel: ParallelNode,
    parallelEnd: ParallelEndNode,
    task: TaskNode,
    eventStart: EventStartNode,
    eventEnd: EventEndNode,
  };
  const edgeTypes = {
    customEdge: CustomEdge,
  };

  useEffect(() => {
    dispatch(fetchFlows());
    dispatch(addHandleClick({ handleClick, _id: params.get("flowId") }));
  }, [dispatch]);

  console.log(flowStatus);

  return flowStatus === "succeeded" ? (
    <div className="dndflow">
      <ReactFlowProvider>
        <div className="reactflow-wrapper" ref={reactFlowWrapper}>
          <ReactFlow
            elements={elements}
            onConnect={onConnect}
            onElementsRemove={onElementsRemove}
            onLoad={onLoad}
            onDrop={onDrop}
            onDragOver={onDragOver}
            nodeTypes={nodeTypes}
            edgeTypes={edgeTypes}
          >
            <Controls />
            <Background
              style={{ backgroundColor: "#fff4eb" }}
              size={1}
              variant="lines"
            />
            <MiniMap />
          </ReactFlow>
        </div>
        <Popover
          id={id}
          open={open}
          anchorEl={anchorEl}
          onClose={handleClose}
          anchorOrigin={{
            vertical: "bottom",
            horizontal: "center",
          }}
          transformOrigin={{
            vertical: "top",
            horizontal: "center",
          }}
        >
          <input
            value={text}
            onChange={(text) => setText(text.target.value)}
          ></input>
        </Popover>

        <Sidebar />
        <div className="column">
          <button
            onClick={() => {
              onLayout("TB");
              reactFlowInstance.fitView();
            }}
          >
            vertical layout
          </button>
          <button
            onClick={() => {
              onLayout("LR");
              reactFlowInstance.fitView();
            }}
          >
            horizontal layout
          </button>

          <h3>ALTERAR TITULO DO FLUXO</h3>
          <input
            value={flow.title}
            onChange={(text) =>
              dispatch(
                editFlowTitle({
                  title: text.target.value,
                  flowId: params.get("flowId"),
                })
              )
            }
          ></input>
          <p style={{ color: "red" }}>{error === "" ? null : error}</p>
          <button
            className="roww"
            onClick={() => {
              if (flow.title === "") {
                return setError("Necessário preencher o título");
              } else
                return dispatch(
                  handleEditModelFlow(
                    reactFlowInstance.getElements(),
                    flow.title,
                    flowId
                  )
                ).then(() => history.push("/"));
            }}
          >
            Editar flow
          </button>
        </div>
      </ReactFlowProvider>
    </div>
  ) : (
    <div>wait</div>
  );
};

export default FlowEditionPage;
moklick commented 3 years ago

Hey @CamboimGabriel

at which point do you want to call fitView exactly? In the FlowEditionPage this wouldn't be possible (because that's the provider component) but in all children it should work.

CamboimGabriel commented 3 years ago

Oh, I want to call fitView right after click in vertical/horizontal layout buttons, so what can I do to call fitView in same page?

moklick commented 3 years ago

You would need to put the buttons in their own component. In that component you can use the useZoomPanHelper hook.

CamboimGabriel commented 3 years ago

I put fit view in a component but doesnt seem to work

Before:

image

After click:

image

Here is the code:

import React from "react";
import { useZoomPanHelper } from "react-flow-renderer";

const LayoutButton = ({ title, onClick }) => {
  const { fitView } = useZoomPanHelper();

  return (
    <>
      <button
        onClick={() => {
          onClick();
          fitView();
        }}
      >
        {title}
      </button>
    </>
  );
};

export default LayoutButton;

Is this a problem with vertical/horizontal layout example?

moklick commented 3 years ago

I think the fitView gets called during/before the node positions are updated.

CamboimGabriel commented 3 years ago

I'm trying to call after but didnt get sucess about it, do u have an idea on how I can do this?

moklick commented 3 years ago

The layouting task is asynchronous. When you call fitView the new layout is not applied yet. I think it would be nice to have a getTransformByElements/ getViewportByElements helper function for this case.

moklick commented 3 years ago

Hey @CamboimGabriel Could you solve this?

CamboimGabriel commented 3 years ago

Hey @moklick, I removed the horizontal / vertical shift feature from my app, so I didn't have to worry about it anymore, at least atm

CamboimGabriel commented 3 years ago

I tried to resolve but didnt get success

stvncode commented 2 years ago

In case someone is asking the same question, you have to put the proivder in the parent file. Example: <ReactFlowProvider><FlowEditionPage /></ReactFlowProvider>

narekdanielyan19 commented 2 years ago

Hi, anybody knows how to use some function after fitView updates the layout?

stvncode commented 2 years ago

You can just use an useEffect and put fitView in the dependancies