Merge pull request #2279 from wbkd/v11

Major Release v11
This commit is contained in:
Moritz Klack
2022-09-28 14:58:34 +02:00
committed by GitHub
287 changed files with 13338 additions and 61955 deletions

View File

@@ -1,13 +0,0 @@
{
"presets": [
[
"@babel/preset-env",
{
"modules": false
}
],
"@babel/preset-typescript",
"@babel/preset-react"
],
"plugins": ["@babel/plugin-transform-runtime"]
}

8
.changeset/README.md Normal file
View File

@@ -0,0 +1,8 @@
# Changesets
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
with multi-package repos, or single-package repos to help you version and publish your code. You can
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
We have a quick list of common questions to get you started engaging with this project in
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)

11
.changeset/config.json Normal file
View File

@@ -0,0 +1,11 @@
{
"$schema": "https://unpkg.com/@changesets/config@2.1.1/schema.json",
"changelog": ["@changesets/changelog-github", { "repo": "wbkd/react-flow" }],
"commit": false,
"fixed": [],
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}

3
.gitignore vendored
View File

@@ -9,3 +9,6 @@ dist
stats.html
.eslintcache
.idea
.log
.turbo
.rollup.cache

View File

@@ -1,10 +0,0 @@
.DS_Store
.cache
.vscode
.dependabot
node_modules
examples
cypress
.env
tsconfig.json
src

5
.npmrc
View File

@@ -1,2 +1,3 @@
registry = "https://registry.npmjs.com/"
legacy-peer-deps = true
registry="https://registry.npmjs.com/"
legacy-peer-deps=true
strict-peer-dependencies=false

View File

@@ -1,7 +0,0 @@
{
"plugins": {
"autoprefixer" : {},
"postcss-nested": {}
}
}

View File

@@ -1 +1,5 @@
*.md
*.md
dist
node_modules
.next
build

View File

@@ -1,12 +0,0 @@
{
"git": {
"commitMessage": "chore: release v${version}"
},
"github": {
"release": true
},
"hooks": {
"after:bump": "npm run build",
"after:release": "echo Successfully released ${name} v${version}."
}
}

View File

@@ -1,3 +0,0 @@
{
"typescript.tsdk": "node_modules/typescript/lib"
}

View File

@@ -25,7 +25,7 @@ The docs have its own [react-flow-docs repo](https://github.com/wbkd/react-flow-
If you want to contribute improvements or new features we are happy to review your PR :)
Please use a meaningful commit message and add a little description of your changes.
1. Install dependencies `npm install` and example dependencies `cd example && npm install`
2. Run library start task `npm start` and example start task `cd example && npm start`
3. Now you can test your changes with the existing examples. Feel free to add a new one if it's needed for your changes
4. Run tests `npm test` and add new ones if you are introducing a new feature
1. Install dependencies `pnpm install`
2. Start dev server `pnpm dev`
3. Test your changes with the existing examples or add a new one if it's needed for your changes
4. Run tests `pnpm test` and add new new tests if you are introducing a new feature

100
README.md
View File

@@ -3,19 +3,18 @@
<div align="center">
![GitHub License MIT](https://img.shields.io/github/license/wbkd/react-flow?color=%23ff0072)
![npm downloads](https://img.shields.io/npm/dt/react-flow-renderer?color=%23FF0072&label=downloads)
![GitHub Repo stars](https://img.shields.io/github/stars/wbkd/react-flow?color=%23FF0072)
![GitHub release (latest by date)](https://img.shields.io/github/v/release/wbkd/react-flow?color=%23FF0072)
![GitHub License MIT](https://img.shields.io/github/license/wbkd/react-flow?color=%23ff0072)
![npm downloads](https://img.shields.io/npm/dt/reactflow?color=%23FF0072&label=downloads)
![GitHub Repo stars](https://img.shields.io/github/stars/wbkd/react-flow?color=%23FF0072)
![GitHub release (latest by date)](https://img.shields.io/github/v/release/wbkd/react-flow?color=%23FF0072)
### Wire Your Ideas With React Flow!
A highly customizable React component for building interactive graphs and node-based editors.
[🚀 Getting Started](https://reactflow.dev/docs/getting-started/installation) | [📖 Documentation](https://reactflow.dev/docs/api/react-flow-props) | [📺 Examples](https://reactflow.dev/docs/examples/overview) | [☎️ Discord](https://discord.gg/RVmnytFmGW) | [💎 React Flow Pro](https://pro.reactflow.dev/pricing)
[🚀 Getting Started](https://reactflow.dev/docs/quickstart/) | [📖 Documentation](https://reactflow.dev/docs/api/react-flow-props) | [📺 Examples](https://reactflow.dev/docs/examples/overview) | [☎️ Discord](https://discord.gg/RVmnytFmGW) | [💎 React Flow Pro](https://pro.reactflow.dev)
</div>
----
---
## Key Features
@@ -26,28 +25,52 @@ A highly customizable React component for building interactive graphs and node-b
- **Plugin Components:** [Background](https://reactflow.dev/docs/api/plugin-components/background), [MiniMap](https://reactflow.dev/docs/api/plugin-components/minimap) and [Controls](https://reactflow.dev/docs/api/plugin-components/controls)
- **Reliable**: Written in [Typescript](https://www.typescriptlang.org/) and tested with [cypress](https://www.cypress.io/)
## Commercial Usage / Attribution
## Commercial Usage
React Flow includes a small attribution that links to the React Flow website. **We expect companies who are using React Flow commercially to subscribe to [React Flow Pro](https://pro.reactflow.dev/pricing) if they want to remove the attribution.** By subscribing you get access to other exclusive services like advanced examples, individual support or prioritized bug reports. In non-commercial applications you may hide the attribution without subscribing but are welcome to [sponsor us on Github](https://github.com/sponsors/wbkd).
**Are you using React Flow for a personal project?** Great! No sponsorship needed, you can support us by reporting any bugs you find, sending us screenshots of your projects, and starring us on Github 🌟
You can find more information in our [React Flow Pro FAQs](https://pro.reactflow.dev/faq).
**Are you using React Flow at your organization and making money from it?** Awesome! We rely on your support to keep React Flow developed and maintained under an MIT License, just how we like it. You can do that on the [React Flow Pro website](https://pro.reactflow.dev) or through [Github Sponsors](https://github.com/sponsors/wbkd).
You can find more information in our [React Flow Pro FAQs](https://pro.reactflow.dev/info).
## Installation
The easiest way to get the latest version of React Flow is to install it via npm:
The easiest way to get the latest version of React Flow is to install it via npm, yarn or pnpm:
```bash
npm install react-flow-renderer
npm install reactflow
```
## Quick Start
## Quickstart
This is only a very basic usage example of React Flow. To see everything that is possible with the library, please refer to the [website](https://reactflow.dev) for [guides](https://reactflow.dev/docs/guides/custom-nodes), [examples](https://reactflow.dev/docs/examples/overview) and [API reference](https://reactflow.dev/docs/api/react-flow-props).
This is only a very basic usage example of React Flow. To see everything that is possible with the library, please refer to the [website](https://reactflow.dev) for [guides](https://reactflow.dev/docs/guides/custom-nodes), [examples](https://reactflow.dev/docs/examples/overview) and the full [API reference](https://reactflow.dev/docs/api/react-flow-props).
```jsx
import ReactFlow, { MiniMap, Controls } from 'react-flow-renderer';
import { useCallback } from 'react';
import ReactFlow, {
MiniMap,
Controls,
Background,
useNodesState,
useEdgesState,
addEdge,
} from 'reactflow';
import 'reactflow/dist/style.css';
const initialNodes = [
{ id: '1', position: { x: 0, y: 0 }, data: { label: '1' } },
{ id: '2', position: { x: 0, y: 100 }, data: { label: '2' } },
];
const initialEdges = [{ id: 'e1-2', source: '1', target: '2' }];
function Flow() {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = useCallback((params) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
function Flow({ nodes, edges, onNodesChange, onEdgesChange, onConnect }) {
return (
<ReactFlow
nodes={nodes}
@@ -58,6 +81,7 @@ function Flow({ nodes, edges, onNodesChange, onEdgesChange, onConnect }) {
>
<MiniMap />
<Controls />
<Background />
</ReactFlow>
);
}
@@ -65,52 +89,40 @@ function Flow({ nodes, edges, onNodesChange, onEdgesChange, onConnect }) {
## Development
Before you start you need to build the project using `npm run build`. Then install the React Flow dependencies via `npm install` and the ones of the examples `cd example && npm install`.
Before you can start developing please make sure that you have [pnpm](https://pnpm.io/) installed (`npm i -g pnpm`). Then install the dependencies using pnpm: `pnpm install`.
If you want to contribute or develop custom features the easiest way is to start the dev server:
```sh
npm start
```
and the example app via:
```sh
cd example && npm start
```
The example app serves the content of the `example` folder and watches changes inside the `src` folder. The examples are using the source of the `src` folder.
For local development, you can use `pnpm dev`.
## Testing
Testing is done with cypress. You can find the tests in the [`integration/flow`](/cypress/integration/flow) folder. In order to run the tests do:
Testing is done with cypress. You can find the tests in the [`examples/cypress`](/examples/cypress/) folder. In order to run the tests do:
```sh
npm run test
pnpm test
```
## Maintainers
React Flow is developed and maintained by [webkid](https://webkid.io), a web development agency with focus on data driven applications from Berlin. If you need help or want to talk to us about a collaboration, feel free to contact us:
React Flow is the full-time project of Moritz and Christopher of [webkid](https://webkid.io/), based in Berlin. If you need help or want to talk to us about a collaboration, reach out through our [contact form](https://pro.reactflow.dev/contact) or by joining the [React Flow Discord Server](https://discord.gg/Bqt6xrs).
* Moritz Klack • [Twitter](https://twitter.com/moklick)[Github](https://github.com/moklick)
* Christopher Möller • [Twitter](https://twitter.com/chrtze)[Github](https://github.com/chrtze)
- Moritz Klack • [Twitter](https://twitter.com/moklick) • [Github](https://github.com/moklick)
- Christopher Möller • [Twitter](https://twitter.com/chrtze) • [Github](https://github.com/chrtze)
You can also use our [contact form](https://pro.reactflow.dev/contact) or join the [React Flow Discord Server](https://discord.gg/Bqt6xrs).
Any support you provide goes directly towards the development and maintenance of React Flow, allowing us to continue to operate as an independent company, working on what we think is best for React Flow as an open-source library.
## Community Packages
* [useUndoable](https://github.com/Infinium8/useUndoable) - Hook for undo/redo functionality with an explicit React Flow example
* [react-flow-smart-edge](https://github.com/tisoap/react-flow-smart-edge) - Custom edge that doesn't intersect with nodes
* [Feliz.ReactFlow](https://github.com/tforkmann/Feliz.ReactFlow) - Feliz React Bindings for React Flow
- [useUndoable](https://github.com/Infinium8/useUndoable) - Hook for undo/redo functionality with an explicit React Flow example
- [react-flow-smart-edge](https://github.com/tisoap/react-flow-smart-edge) - Custom edge that doesn't intersect with nodes
- [Feliz.ReactFlow](https://github.com/tforkmann/Feliz.ReactFlow) - Feliz React Bindings for React Flow
## Credits
React Flow was initially developed for [datablocks](https://datablocks.pro), a graph-based editor for transforming, analyzing and visualizing data in your browser. Under the hood, React Flow depends on these great libraries:
React Flow was initially developed for [datablocks](https://datablocks.pro), a graph-based editor for transforming, analyzing and visualizing data in the browser. Under the hood, React Flow depends on these great libraries:
* [d3-zoom](https://github.com/d3/d3-zoom) - used for zoom, pan and drag interactions with the graph canvas
* [d3-drag](https://github.com/d3/d3-drag) - used for making the nodes draggable
* [zustand](https://github.com/pmndrs/zustand) - internal state management
- [d3-zoom](https://github.com/d3/d3-zoom) - used for zoom, pan and drag interactions with the graph canvas
- [d3-drag](https://github.com/d3/d3-drag) - used for making the nodes draggable
- [zustand](https://github.com/pmndrs/zustand) - internal state management
## License

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 30">
<path d="M3.692 4.63c0-.53.4-.938.939-.938h5.215V0H4.708C2.13 0 0 2.054 0 4.63v5.216h3.692V4.631zM27.354 0h-5.2v3.692h5.17c.53 0 .984.4.984.939v5.215H32V4.631A4.624 4.624 0 0027.354 0zm.954 24.83c0 .532-.4.94-.939.94h-5.215v3.768h5.215c2.577 0 4.631-2.13 4.631-4.707v-5.139h-3.692v5.139zm-23.677.94c-.531 0-.939-.4-.939-.94v-5.138H0v5.139c0 2.577 2.13 4.707 4.708 4.707h5.138V25.77H4.631z" />
</svg>

Before

Width:  |  Height:  |  Size: 463 B

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 25 32">
<path d="M21.333 10.667H19.81V7.619C19.81 3.429 16.38 0 12.19 0 8 0 4.571 3.429 4.571 7.619v3.048H3.048A3.056 3.056 0 000 13.714v15.238A3.056 3.056 0 003.048 32h18.285a3.056 3.056 0 003.048-3.048V13.714a3.056 3.056 0 00-3.048-3.047zM12.19 24.533a3.056 3.056 0 01-3.047-3.047 3.056 3.056 0 013.047-3.048 3.056 3.056 0 013.048 3.048 3.056 3.056 0 01-3.048 3.047zm4.724-13.866H7.467V7.619c0-2.59 2.133-4.724 4.723-4.724 2.591 0 4.724 2.133 4.724 4.724v3.048z" />
</svg>

Before

Width:  |  Height:  |  Size: 530 B

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 5">
<path d="M0 0h32v4.2H0z"/>
</svg>

Before

Width:  |  Height:  |  Size: 96 B

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<path d="M32 18.133H18.133V32h-4.266V18.133H0v-4.266h13.867V0h4.266v13.867H32z"/>
</svg>

Before

Width:  |  Height:  |  Size: 152 B

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 25 32">
<path d="M21.333 10.667H19.81V7.619C19.81 3.429 16.38 0 12.19 0c-4.114 1.828-1.37 2.133.305 2.438 1.676.305 4.42 2.59 4.42 5.181v3.048H3.047A3.056 3.056 0 000 13.714v15.238A3.056 3.056 0 003.048 32h18.285a3.056 3.056 0 003.048-3.048V13.714a3.056 3.056 0 00-3.048-3.047zM12.19 24.533a3.056 3.056 0 01-3.047-3.047 3.056 3.056 0 013.047-3.048 3.056 3.056 0 013.048 3.048 3.056 3.056 0 01-3.048 3.047z" />
</svg>

Before

Width:  |  Height:  |  Size: 472 B

View File

@@ -1,7 +0,0 @@
{
"baseUrl": "http://localhost:3000",
"viewportWidth": 1280,
"viewportHeight": 720,
"video": false,
"pluginsFile": false
}

View File

@@ -1,5 +0,0 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}

View File

@@ -1,38 +0,0 @@
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add("login", (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This is will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
Cypress.Commands.add('drag', (selector, { x, y }) => {
return cy
.window()
.then((window) =>
cy
.get(selector)
.trigger('mousedown', { which: 1, view: window })
.trigger('mousemove', { clientX: x, clientY: y, force: true })
.wait(50)
.trigger('mouseup', { view: window, force: true })
);
});

25
example/.gitignore vendored
View File

@@ -1,25 +0,0 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
/src_oldapi

View File

@@ -1,23 +0,0 @@
# React Flow Examples
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Installation
```sh
npm install
```
## Start
Starts a dev server at http://localhost:3000
```sh
npm start
```
## Build
```sh
npm run build
```

39423
example/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,43 +0,0 @@
{
"name": "react-flow-examples",
"version": "0.1.0",
"private": true,
"dependencies": {
"dagre": "^0.8.5",
"localforage": "^1.10.0",
"react": "file:../node_modules/react",
"react-dom": "file:../node_modules/react-dom",
"react-flow-renderer": "file:../",
"react-router-dom": "^6.3.0",
"react-scripts": "5.0.1",
"typescript": "^4.7.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build"
},
"eslintConfig": {
"extends": [
"react-app"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@types/dagre": "^0.7.47",
"@types/localforage": "0.0.34",
"@types/react": "file:../node_modules/@types/react",
"@types/react-dom": "file:../node_modules/@types/react-dom",
"@types/react-router-dom": "^5.3.3"
}
}

View File

@@ -1 +0,0 @@
/* /index.html 200

View File

@@ -1,12 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>React Flow Examples</title>
</head>
<body>
<div id="root"></div>
</body>
</html>

View File

@@ -1,3 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@@ -1,25 +0,0 @@
import React, { DragEvent } from 'react';
const onDragStart = (event: DragEvent, nodeType: string) => {
event.dataTransfer.setData('application/reactflow', nodeType);
event.dataTransfer.effectAllowed = 'move';
};
const Sidebar = () => {
return (
<aside>
<div className="description">You can drag these nodes to the pane on the left.</div>
<div className="react-flow__node-input" onDragStart={(event: DragEvent) => onDragStart(event, 'input')} draggable>
Input Node
</div>
<div className="react-flow__node-default" onDragStart={(event: DragEvent) => onDragStart(event, 'default')} draggable>
Default Node
</div>
<div className="react-flow__node-output" onDragStart={(event: DragEvent) => onDragStart(event, 'output')} draggable>
Output Node
</div>
</aside>
);
};
export default Sidebar;

View File

@@ -1,76 +0,0 @@
import { Node, Edge, XYPosition, MarkerType } from 'react-flow-renderer';
const position: XYPosition = { x: 0, y: 0 };
const nodes: Node[] = [
{
id: '1',
type: 'input',
data: { label: 'input' },
position,
},
{
id: '2',
data: { label: 'node 2' },
position,
},
{
id: '2a',
data: { label: 'node 2a' },
position,
},
{
id: '2b',
data: { label: 'node 2b' },
position,
},
{
id: '2c',
data: { label: 'node 2c' },
position,
},
{
id: '2d',
data: { label: 'node 2d' },
position,
},
{
id: '3',
data: { label: 'node 3' },
position,
},
{
id: '4',
data: { label: 'node 4' },
position,
},
{
id: '5',
data: { label: 'node 5' },
position,
},
{
id: '6',
type: 'output',
data: { label: 'output' },
position,
},
{ id: '7', type: 'output', data: { label: 'output' }, position: { x: 400, y: 450 } },
];
const edges: Edge[] = [
{ id: 'e12', source: '1', target: '2', type: 'smoothstep', markerEnd: { type: MarkerType.Arrow } },
{ id: 'e13', source: '1', target: '3', type: 'smoothstep', markerEnd: { type: MarkerType.Arrow } },
{ id: 'e22a', source: '2', target: '2a', type: 'smoothstep', markerEnd: { type: MarkerType.Arrow } },
{ id: 'e22b', source: '2', target: '2b', type: 'smoothstep', markerEnd: { type: MarkerType.Arrow } },
{ id: 'e22c', source: '2', target: '2c', type: 'smoothstep', markerEnd: { type: MarkerType.Arrow } },
{ id: 'e2c2d', source: '2c', target: '2d', type: 'smoothstep', markerEnd: { type: MarkerType.Arrow } },
{ id: 'e45', source: '4', target: '5', type: 'smoothstep', markerEnd: { type: MarkerType.Arrow } },
{ id: 'e56', source: '5', target: '6', type: 'smoothstep', markerEnd: { type: MarkerType.Arrow } },
{ id: 'e57', source: '5', target: '7', type: 'smoothstep', markerEnd: { type: MarkerType.Arrow } },
];
const nodesAndEdges = { nodes, edges };
export default nodesAndEdges;

View File

@@ -1,13 +0,0 @@
.react-flow__example-multiflows {
display: flex;
height: 100%;
}
.react-flow__example-multiflows .react-flow {
width: 100%;
height: 100%;
}
.react-flow__example-multiflows .react-flow:first-child {
border-right: 2px solid #333;
}

View File

@@ -1,73 +0,0 @@
import { MouseEvent } from 'react';
import ReactFlow, { addEdge, Node, Connection, Edge, useNodesState, useEdgesState } from 'react-flow-renderer';
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
const nodesA: Node[] = [
{ id: '1a', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 }, className: 'light' },
{ id: '2a', data: { label: 'Node 2' }, position: { x: 100, y: 100 }, className: 'light' },
{ id: '3a', data: { label: 'Node 3' }, position: { x: 400, y: 100 }, className: 'light' },
{ id: '4a', data: { label: 'Node 4' }, position: { x: 400, y: 200 }, className: 'light' },
];
const edgesA: Edge[] = [
{ id: 'e1-2', source: '1a', target: '2a' },
{ id: 'e1-3', source: '1a', target: '3a' },
];
const nodesB: Node[] = [
{ id: 'inputb', type: 'input', data: { label: 'Input' }, position: { x: 300, y: 5 }, className: 'light' },
{ id: '1b', data: { label: 'Node 1' }, position: { x: 0, y: 100 }, className: 'light' },
{ id: '2b', data: { label: 'Node 2' }, position: { x: 200, y: 100 }, className: 'light' },
{ id: '3b', data: { label: 'Node 3' }, position: { x: 400, y: 100 }, className: 'light' },
{ id: '4b', data: { label: 'Node 4' }, position: { x: 600, y: 100 }, className: 'light' },
];
const edgesB: Edge[] = [
{ id: 'e1b', source: 'inputb', target: '1b' },
{ id: 'e2b', source: 'inputb', target: '2b' },
{ id: 'e3b', source: 'inputb', target: '3b' },
{ id: 'e4b', source: 'inputb', target: '4b' },
];
const BasicFlow = () => {
const [nodes, setNodes, onNodesChange] = useNodesState(nodesA);
const [edges, setEdges, onEdgesChange] = useEdgesState(edgesA);
const onConnect = (params: Connection | Edge) => setEdges((eds) => addEdge(params, eds));
return (
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onNodeClick={onNodeClick}
onConnect={onConnect}
onNodeDragStop={onNodeDragStop}
>
<div style={{ position: 'absolute', right: 10, top: 10, zIndex: 4 }}>
<button
onClick={() => {
setNodes(nodesA);
setEdges(edgesA);
}}
style={{ marginRight: 5 }}
>
flow a
</button>
<button
onClick={() => {
setNodes(nodesB);
setEdges(edgesB);
}}
>
flow b
</button>
</div>
</ReactFlow>
);
};
export default BasicFlow;

View File

@@ -1,31 +0,0 @@
.validationflow .react-flow__node {
width: 150px;
border-radius: 5px;
padding: 10px;
color: #555;
border: 1px solid #ddd;
text-align: center;
font-size: 12px;
}
.validationflow .react-flow__node-customnode {
background: #e6e6e9;
border: 1px solid #ddd;
}
.react-flow__node-custominput .react-flow__handle {
background: #e6e6e9;
}
.validationflow .react-flow__node-custominput {
background: #fff;
}
.validationflow .react-flow__handle-connecting {
background: #ff6060;
}
.validationflow .react-flow__handle-valid {
background: #55dd99;
}

View File

@@ -1 +0,0 @@
/// <reference types="react-scripts" />

View File

@@ -1,21 +0,0 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": ["src"],
"exclude": ["src_oldapi"]
}

24
examples/vite-app/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -0,0 +1,19 @@
import { defineConfig } from 'cypress';
export default defineConfig({
e2e: {
baseUrl: 'http://localhost:3000',
viewportWidth: 1280,
viewportHeight: 720,
video: false,
screenshotOnRunFailure: false,
},
component: {
devServer: {
framework: 'react',
bundler: 'vite',
},
screenshotOnRunFailure: false,
video: false,
},
});

View File

@@ -0,0 +1,42 @@
import React, { useEffect } from 'react';
import ReactFlow, { Edge, useEdges } from 'reactflow';
import { nodes as initialNodes, edges as initialEdges } from '../../fixtures/simpleflow';
describe('useEdges.cy.tsx', () => {
it('handles edges', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(
<ReactFlow nodes={initialNodes} edges={initialEdges}>
<HookHelperComponent onChange={onChangeSpy} />
</ReactFlow>
);
cy.get('@onChangeSpy').should('have.been.calledWith', []);
cy.get('@onChangeSpy').should('have.been.calledWith', initialEdges);
});
it('handles defaultEdges', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(
<ReactFlow defaultNodes={initialNodes} defaultEdges={initialEdges}>
<HookHelperComponent onChange={onChangeSpy} />
</ReactFlow>
);
cy.get('@onChangeSpy').should('have.been.calledWith', []);
cy.get('@onChangeSpy').should('have.been.calledWith', initialEdges);
});
});
const HookHelperComponent = ({ onChange }: { onChange: (edges: Edge[]) => void }) => {
const edges = useEdges();
useEffect(() => {
onChange(edges);
}, [edges]);
return null;
};

View File

@@ -0,0 +1,58 @@
import { useEffect } from 'react';
import ReactFlow, { Node, useNodes } from 'reactflow';
import { nodes } from '../../fixtures/simpleflow';
const nodeDimensions = {
width: 100,
height: 50,
};
const initialNodes: Node[] = nodes.map((n) => ({
...n,
style: nodeDimensions,
}));
const expectedNodes: Node[] = initialNodes.map((n) => ({
...n,
positionAbsolute: n.position,
...nodeDimensions,
}));
describe('useNodes.cy.tsx', () => {
it('handles nodes', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(
<ReactFlow nodes={initialNodes}>
<HookHelperComponent onChange={onChangeSpy} />
</ReactFlow>
);
cy.get('@onChangeSpy').should('have.been.calledWith', []);
cy.get('@onChangeSpy').should('have.been.calledWith', expectedNodes);
});
it('handles defaultNodes', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(
<ReactFlow defaultNodes={initialNodes}>
<HookHelperComponent onChange={onChangeSpy} />
</ReactFlow>
);
cy.get('@onChangeSpy').should('have.been.calledWith', []);
cy.get('@onChangeSpy').should('have.been.calledWith', expectedNodes);
});
});
const HookHelperComponent = ({ onChange }: { onChange: (nodes: Node[]) => void }) => {
const nodes = useNodes();
useEffect(() => {
onChange(nodes);
}, [nodes]);
return null;
};

View File

@@ -0,0 +1,68 @@
import ReactFlow, { useNodesInitialized } from 'reactflow';
import { nodes } from '../../fixtures/simpleflow';
import ControlledFlow from '../../support/ControlledFlow';
describe('useNodesInitialized.cy.tsx', () => {
it('returns false for no nodes', () => {
const initSpy = cy.spy().as('initSpy');
cy.mount(
<ReactFlow nodes={[]}>
<HookHelperComponent onChange={initSpy} />
</ReactFlow>
);
// cy.get('@initSpy').should('to.be.calledOnce');
cy.get('@initSpy').should('have.been.calledWith', false);
});
it('returns false without onNodesChange', () => {
const initSpy = cy.spy().as('initSpy');
cy.mount(
<ReactFlow nodes={nodes}>
<HookHelperComponent onChange={initSpy} />
</ReactFlow>
);
cy.get('@initSpy').should('to.be.calledOnce');
cy.get('@initSpy').should('have.be.calledWith', false);
});
it('returns true for defaultNodes', () => {
const initSpy = cy.spy().as('initSpy');
cy.mount(
<ReactFlow defaultNodes={nodes}>
<HookHelperComponent onChange={initSpy} />
</ReactFlow>
);
cy.get('@initSpy').should('have.been.calledWith', false);
cy.get('@initSpy').should('have.been.calledWith', true);
});
it('returns true for nodes + onNodesChange', () => {
const initSpy = cy.spy().as('initSpy');
cy.mount(
<ControlledFlow initialNodes={nodes}>
<HookHelperComponent onChange={initSpy} />
</ControlledFlow>
);
cy.get('@initSpy').should('have.been.calledWith', false);
cy.get('@initSpy').should('have.been.calledWith', true);
});
});
// test specific helpers
function HookHelperComponent({ onChange }: { onChange: (init: boolean) => void }) {
const initialized = useNodesInitialized();
onChange(initialized);
return null;
}

View File

@@ -0,0 +1,96 @@
import ReactFlow, { useOnViewportChange, Viewport } from 'reactflow';
describe('useOnViewportChange.cy.tsx', () => {
it('listen to viewport drag', () => {
const onStartSpy = cy.spy().as('onStartSpy');
const onChangeSpy = cy.spy().as('onChangeSpy');
const onEndSpy = cy.spy().as('onEndSpy');
cy.mount(
<ReactFlow>
<HookHelperComponent onStart={onStartSpy} onChange={onChangeSpy} onEnd={onEndSpy} />
</ReactFlow>
);
cy.dragPane({ from: { x: 50, y: 50 }, to: { x: 50, y: 100 } }).then(() => {
cy.get('@onStartSpy').should('have.been.calledWith', { x: 0, y: 0, zoom: 1 });
cy.get('@onChangeSpy').should('have.been.calledWith', { x: 0, y: 50, zoom: 1 });
cy.get('@onEndSpy').should('have.been.calledWith', { x: 0, y: 50, zoom: 1 });
});
});
it('handles zoom in', () => {
const onStartSpy = cy.spy().as('onStartSpy');
const onChangeSpy = cy.spy().as('onChangeSpy');
const onEndSpy = cy.spy().as('onEndSpy');
cy.mount(
<ReactFlow>
<HookHelperComponent onStart={onStartSpy} onChange={onChangeSpy} onEnd={onEndSpy} />
</ReactFlow>
);
cy.zoomPane(-200).then(() => {
cy.get('@onStartSpy').should('have.been.callCount', 1);
cy.get('@onChangeSpy').should('have.been.callCount', 1);
cy.get('@onEndSpy').should('have.been.callCount', 1);
expect(getLatestViewport(onChangeSpy).zoom).to.be.gt(1);
});
});
it('handles zoom out', () => {
const onStartSpy = cy.spy().as('onStartSpy');
const onChangeSpy = cy.spy().as('onChangeSpy');
const onEndSpy = cy.spy().as('onEndSpy');
cy.mount(
<ReactFlow>
<HookHelperComponent onStart={onStartSpy} onChange={onChangeSpy} onEnd={onEndSpy} />
</ReactFlow>
);
cy.zoomPane(200).then(() => {
cy.get('@onStartSpy').should('have.been.callCount', 1);
cy.get('@onChangeSpy').should('have.been.callCount', 1);
cy.get('@onEndSpy').should('have.been.callCount', 1);
expect(getLatestViewport(onChangeSpy).zoom).to.be.lt(1);
});
});
it('handles default viewport', () => {
const defaultViewport = { x: 100, y: 100, zoom: 0.5 };
const onStartSpy = cy.spy().as('onStartSpy');
cy.mount(
<ReactFlow defaultViewport={defaultViewport}>
<HookHelperComponent onStart={onStartSpy} />
</ReactFlow>
);
cy.dragPane({ from: { x: 50, y: 50 }, to: { x: 50, y: 100 } }).then(() => {
cy.get('@onStartSpy').should('have.been.calledWith', defaultViewport);
});
});
});
// test specific helpers
function HookHelperComponent({
onStart,
onChange,
onEnd,
}: {
onStart?: (viewport: Viewport) => void;
onChange?: (viewport: Viewport) => void;
onEnd?: (viewport: Viewport) => void;
}) {
useOnViewportChange({ onStart, onChange, onEnd });
return null;
}
function getLatestViewport(onChangeSpy: any) {
return onChangeSpy.lastCall.args[0] as unknown as Viewport;
}

View File

@@ -0,0 +1,79 @@
import ReactFlow, { useViewport, Viewport } from 'reactflow';
describe('useViewport.cy.tsx', () => {
it('handles drag', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(
<ReactFlow>
<HookHelperComponent onChange={onChangeSpy} />
</ReactFlow>
);
cy.get('@onChangeSpy').should('have.been.calledWith', { x: 0, y: 0, zoom: 1 });
cy.dragPane({ from: { x: 50, y: 0 }, to: { x: 50, y: 400 } }).then(() => {
cy.get('@onChangeSpy').should('have.been.callCount', 2);
cy.get('@onChangeSpy').should('have.been.calledWith', { x: 0, y: 0, zoom: 1 });
expect(getLatestViewport(onChangeSpy)).to.eql({ x: 0, y: 400, zoom: 1 });
});
});
it('handles zoom in', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(
<ReactFlow>
<HookHelperComponent onChange={onChangeSpy} />
</ReactFlow>
);
cy.zoomPane(-200).then(() => {
cy.get('@onChangeSpy').should('have.been.callCount', 2);
expect(getLatestViewport(onChangeSpy).zoom).to.be.gt(1);
});
});
it('handles zoom out', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(
<ReactFlow>
<HookHelperComponent onChange={onChangeSpy} />
</ReactFlow>
);
cy.zoomPane(200).then(() => {
cy.get('@onChangeSpy').should('have.been.callCount', 2);
expect(getLatestViewport(onChangeSpy).zoom).to.be.lt(1);
});
});
it('handles default viewport', () => {
const defaultViewport = { x: 100, y: 100, zoom: 0.5 };
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(
<ReactFlow defaultViewport={defaultViewport}>
<HookHelperComponent onChange={onChangeSpy} />
</ReactFlow>
);
cy.get('@onChangeSpy').should('have.been.calledWith', defaultViewport);
});
});
// test specific helpers
function HookHelperComponent({ onChange }: { onChange: (vp: Viewport) => void }) {
const viewport = useViewport();
onChange(viewport);
return null;
}
function getLatestViewport(onChangeSpy: any) {
return onChangeSpy.lastCall.args[0] as unknown as Viewport;
}

View File

@@ -0,0 +1,197 @@
import ReactFlow, { EdgeProps } from 'reactflow';
import ControlledFlow from '../../support/ControlledFlow';
import * as simpleflow from '../../fixtures/simpleflow';
describe('<ReactFlow />: Basic Props', () => {
describe('uses defaultNodes and defaultEdges', () => {
beforeEach(() => {
cy.mount(<ReactFlow defaultNodes={simpleflow.nodes} defaultEdges={simpleflow.edges} />);
});
it('mounts nodes and edges', () => {
cy.get('.react-flow__node').should('have.length', simpleflow.nodes.length);
cy.get('.react-flow__edge').should('have.length', simpleflow.edges.length);
});
it('drags a node', () => {
const styleBeforeDrag = Cypress.$('.react-flow__node:first').css('transform');
cy.drag('.react-flow__node:first', { x: 200, y: 25 }).then(($el: JQuery<HTMLElement>) => {
const styleAfterDrag = $el.css('transform');
expect(styleBeforeDrag).to.not.equal(styleAfterDrag);
});
});
});
describe('uses nodes and edges', () => {
beforeEach(() => {
cy.mount(
<ControlledFlow
addOnNodeChangeHandler={false}
initialNodes={simpleflow.nodes}
initialEdges={simpleflow.edges}
/>
);
});
it('mounts nodes and edges', () => {
cy.get('.react-flow__node').should('have.length', simpleflow.nodes.length);
cy.get('.react-flow__edge').should('have.length', simpleflow.edges.length);
});
it('can not drag a node', () => {
const styleBeforeDrag = Cypress.$('.react-flow__node:first').css('transform');
cy.drag('.react-flow__node:first', { x: 200, y: 25 }).then(($el: JQuery<HTMLElement>) => {
const styleAfterDrag = $el.css('transform');
expect(styleBeforeDrag).to.equal(styleAfterDrag);
});
});
});
describe('uses onNodesChange handler', () => {
beforeEach(() => {
cy.mount(<ControlledFlow addOnConnectHandler={false} initialNodes={simpleflow.nodes} />);
});
it('mounts nodes', () => {
cy.get('.react-flow__node').should('have.length', simpleflow.nodes.length);
cy.get('.react-flow__edge').should('have.length', 0);
});
it('drags a node', () => {
const styleBeforeDrag = Cypress.$('.react-flow__node:first').css('transform');
cy.drag('.react-flow__node:first', { x: 200, y: 25 }).then(($el: JQuery<HTMLElement>) => {
const styleAfterDrag = $el.css('transform');
expect(styleBeforeDrag).to.not.equal(styleAfterDrag);
});
});
it('can not connect nodes', () => {
cy.get('.react-flow__node').first().find('.react-flow__handle.source').trigger('mousedown', { button: 0 });
cy.get('.react-flow__node')
.last()
.find('.react-flow__handle.target')
.trigger('mousemove')
.trigger('mouseup', { force: true });
cy.get('.react-flow__edge').should('have.length', 0);
});
});
describe('uses onEdgeChange handler', () => {
beforeEach(() => {
cy.mount(<ControlledFlow initialNodes={simpleflow.nodes} initialEdges={simpleflow.edges} />);
});
it('mounts nodes and edges', () => {
cy.get('.react-flow__node').should('have.length', simpleflow.nodes.length);
cy.get('.react-flow__edge').should('have.length', simpleflow.edges.length);
});
it('selects an edge', () => {
cy.get('.react-flow__edge').first().click().should('have.class', 'selected');
});
});
describe('uses onConnect handlers', () => {
beforeEach(() => {
cy.mount(<ControlledFlow initialNodes={simpleflow.nodes} />);
});
it('mounts nodes', () => {
cy.get('.react-flow__node').should('have.length', simpleflow.nodes.length);
cy.get('.react-flow__edge').should('have.length', 0);
});
it('connects nodes', () => {
cy.get('.react-flow__node').first().find('.react-flow__handle.source').trigger('mousedown', { button: 0 });
cy.get('.react-flow__node')
.last()
.find('.react-flow__handle.target')
.trigger('mousemove')
.trigger('mouseup', { force: true });
cy.get('.react-flow__edge').should('have.length', 1);
});
});
describe('uses nodeTypes', () => {
it('renders custom nodes', () => {
const nodeTypes = {
custom: () => <div className="customnode">custom node</div>,
};
const initialNodes = simpleflow.nodes.map((n) => ({
...n,
type: 'custom',
}));
cy.mount(<ControlledFlow nodeTypes={nodeTypes} initialNodes={initialNodes} />);
cy.get('.react-flow__node-custom').should('have.length', simpleflow.nodes.length);
cy.get('.react-flow').contains('custom node');
});
it('tries invalid node type - should fallback to default', () => {
const initialNodes = simpleflow.nodes.map((n) => ({
...n,
type: 'invalid',
}));
cy.mount(<ControlledFlow initialNodes={initialNodes} />);
cy.get('.react-flow__node-invalid').should('have.length', 0);
cy.get('.react-flow__node-default').should('have.length', simpleflow.nodes.length);
});
});
describe('uses edgeTypes', () => {
it('renders custom edge', () => {
const edgeTypes = {
custom: ({ sourceX, sourceY, targetX, targetY }: EdgeProps) => (
<path d={`M${sourceX} ${sourceY} L${targetX} ${targetY}`} stroke="black" />
),
};
const initialEdges = simpleflow.edges.map((e) => ({
...e,
type: 'custom',
}));
cy.mount(<ControlledFlow edgeTypes={edgeTypes} initialNodes={simpleflow.nodes} initialEdges={initialEdges} />);
cy.get('.react-flow__edge-custom').should('have.length', simpleflow.edges.length);
});
it('tries invalid edge type - should fallback to default', () => {
const initialEdges = simpleflow.edges.map((e) => ({
...e,
type: 'invalid',
}));
cy.mount(<ControlledFlow initialNodes={simpleflow.nodes} initialEdges={initialEdges} />);
cy.get('.react-flow__edge-invalid').should('have.length', 0);
cy.get('.react-flow__edge-default').should('have.length', simpleflow.edges.length);
});
});
it('uses style', () => {
const styles = {
backgroundColor: 'red',
};
cy.mount(<ControlledFlow style={styles} />);
cy.get('.react-flow').should('have.css', 'background-color', 'rgb(255, 0, 0)');
});
it('uses classname', () => {
cy.mount(<ControlledFlow className="custom" />);
cy.get('.react-flow').should('have.class', 'custom');
});
});

View File

@@ -0,0 +1,210 @@
import { skipOn } from '@cypress/skip-test';
import { nodes, edges } from '../../fixtures/simpleflow';
import ControlledFlow from '../../support/ControlledFlow';
describe('<ReactFlow />: Event handlers', () => {
describe('Node event handlers', () => {
it('handles onInit', () => {
const onInitSpy = cy.spy().as('onInitSpy');
cy.mount(<ControlledFlow initialNodes={nodes} initialEdges={edges} onInit={onInitSpy} />)
.wait(100)
.then(() => {
expect(onInitSpy.callCount).to.be.eq(1);
});
});
it('handles onNodeClick', () => {
const onNodeClickSpy = cy.spy().as('onNodeClickSpy');
cy.mount(<ControlledFlow initialNodes={nodes} initialEdges={edges} onNodeClick={onNodeClickSpy} />).then(() => {
expect(onNodeClickSpy.callCount).to.be.eq(0);
cy.get('.react-flow__node:first')
.click()
.then(() => {
expect(onNodeClickSpy.callCount).to.be.eq(1);
});
});
});
it('handles onNodeDrag handlers', () => {
const onNodeDragStart = cy.spy().as('onNodeDragStart');
const onNodeDrag = cy.spy().as('onNodeDrag');
const onNodeDragStop = cy.spy().as('onNodeDragStop');
cy.mount(
<ControlledFlow
initialNodes={nodes}
initialEdges={edges}
onNodeDragStart={onNodeDragStart}
onNodeDrag={onNodeDrag}
onNodeDragStop={onNodeDragStop}
/>
).then(() => {
expect(onNodeDragStart.callCount).to.be.eq(0);
expect(onNodeDrag.callCount).to.be.eq(0);
expect(onNodeDragStop.callCount).to.be.eq(0);
cy.drag('.react-flow__node:first', { x: 200, y: 0 }).then(() => {
expect(onNodeDragStart.callCount).to.be.gt(0);
expect(onNodeDrag.callCount).to.be.gt(0);
expect(onNodeDragStop.callCount).to.be.gt(0);
});
});
});
it('handles onNodeMouse handlers', () => {
const onNodeMouseEnter = cy.spy().as('onNodeMouseEnter');
const onNodeMouseMove = cy.spy().as('onNodeMouseMove');
const onNodeMouseLeave = cy.spy().as('onNodeMouseLeave');
const onNodeContextMenu = cy.spy().as('onNodeContextMenu');
const onNodeDoubleClick = cy.spy().as('onNodeDoubleClick');
cy.mount(
<ControlledFlow
initialNodes={nodes}
initialEdges={edges}
onNodeMouseEnter={onNodeMouseEnter}
onNodeMouseMove={onNodeMouseMove}
onNodeMouseLeave={onNodeMouseLeave}
onNodeContextMenu={onNodeContextMenu}
onNodeDoubleClick={onNodeDoubleClick}
/>
)
.wait(200)
.then(() => {
expect(onNodeMouseEnter.callCount).to.be.eq(0);
expect(onNodeMouseMove.callCount).to.be.eq(0);
expect(onNodeMouseLeave.callCount).to.be.eq(0);
expect(onNodeContextMenu.callCount).to.be.eq(0);
expect(onNodeDoubleClick.callCount).to.be.eq(0);
const node = cy.get('.react-flow__node').contains('Node 1');
node
.rightclick()
.dblclick()
.then(() => {
expect(onNodeContextMenu.callCount).to.be.eq(1);
expect(onNodeDoubleClick.callCount).to.be.eq(1);
});
skipOn('firefox');
node
.realHover()
.realMouseMove(100, 100)
.then(() => {
expect(onNodeMouseEnter.callCount).to.be.gt(0);
expect(onNodeMouseMove.callCount).to.be.gt(0);
expect(onNodeMouseLeave.callCount).to.be.gt(0);
});
});
});
});
describe('Edge event handlers', () => {
it('handles onEdgeClick', () => {
const onEdgeClick = cy.spy().as('onEdgeClick');
cy.mount(<ControlledFlow initialNodes={nodes} initialEdges={edges} onEdgeClick={onEdgeClick} />).then(() => {
expect(onEdgeClick.callCount).to.be.eq(0);
cy.get('.react-flow__edge:first')
.click()
.then(() => {
expect(onEdgeClick.callCount).to.be.eq(1);
});
});
});
it('handles onEdgeMouse handlers', () => {
const onEdgeMouseEnter = cy.spy().as('onEdgeMouseEnter');
const onEdgeMouseMove = cy.spy().as('onEdgeMouseMove');
const onEdgeMouseLeave = cy.spy().as('onEdgeMouseLeave');
const onEdgeContextMenu = cy.spy().as('onEdgeContextMenu');
const onEdgeDoubleClick = cy.spy().as('onEdgedoubleClick');
cy.mount(
<ControlledFlow
initialNodes={nodes}
initialEdges={edges}
onEdgeMouseEnter={onEdgeMouseEnter}
onEdgeMouseMove={onEdgeMouseMove}
onEdgeMouseLeave={onEdgeMouseLeave}
onEdgeContextMenu={onEdgeContextMenu}
onEdgeDoubleClick={onEdgeDoubleClick}
/>
)
.wait(200)
.then(() => {
expect(onEdgeMouseEnter.callCount).to.be.eq(0);
expect(onEdgeMouseMove.callCount).to.be.eq(0);
expect(onEdgeMouseLeave.callCount).to.be.eq(0);
expect(onEdgeContextMenu.callCount).to.be.eq(0);
expect(onEdgeDoubleClick.callCount).to.be.eq(0);
const edge = cy.get('.react-flow__edge:first');
edge
.rightclick()
.dblclick()
.then(() => {
expect(onEdgeContextMenu.callCount).to.be.eq(1);
});
skipOn('firefox');
edge
.realHover()
.realMouseMove(100, 100)
.then(() => {
expect(onEdgeMouseEnter.callCount).to.be.gt(0);
expect(onEdgeMouseMove.callCount).to.be.gt(0);
expect(onEdgeMouseLeave.callCount).to.be.gt(0);
});
});
});
});
describe('Pane event handlers', () => {
it('handles onMove handlers', () => {
const onMoveStart = cy.spy().as('onMoveStart');
const onMove = cy.spy().as('onMove');
const onMoveEnd = cy.spy().as('onMoveEnd');
cy.mount(<ControlledFlow onMoveStart={onMoveStart} onMove={onMove} onMoveEnd={onMoveEnd} />).then(() => {
expect(onMoveStart.callCount).to.be.eq(0);
expect(onMove.callCount).to.be.eq(0);
expect(onMoveEnd.callCount).to.be.eq(0);
cy.dragPane({ from: { x: 10, y: 200 }, to: { x: 100, y: 200 } })
.wait(100)
.then(() => {
expect(onMoveStart.callCount).to.be.eq(1);
expect(onMove.callCount).to.be.gt(0);
expect(onMoveEnd.callCount).to.be.eq(1);
});
});
});
it('handles click handlers', () => {
const onPaneClick = cy.spy().as('onPaneClick');
const onPaneContextMenu = cy.spy().as('onPaneContextMenu');
cy.mount(<ControlledFlow onPaneClick={onPaneClick} onPaneContextMenu={onPaneContextMenu} />).then(() => {
expect(onPaneClick.callCount).to.be.eq(0);
expect(onPaneContextMenu.callCount).to.be.eq(0);
cy.get('.react-flow__pane')
.rightclick()
.click()
.wait(100)
.then(() => {
expect(onPaneClick.callCount).to.be.eq(1);
expect(onPaneContextMenu.callCount).to.be.eq(1);
});
});
});
});
});

View File

@@ -0,0 +1,35 @@
import ReactFlow from 'reactflow';
import { nodes, edges } from '../../fixtures/simpleflow';
describe('<ReactFlow />: Uncontrolled Flow', () => {
beforeEach(() => {
cy.mount(<ReactFlow defaultNodes={nodes} defaultEdges={edges} />);
});
it('mounts nodes and edges', () => {
cy.get('.react-flow__node').should('have.length', nodes.length);
cy.get('.react-flow__edge').should('have.length', edges.length);
});
it('selects a node', () => {
cy.get('.react-flow__node').first().click().should('have.class', 'selected');
cy.get('.react-flow__pane').click();
cy.get('.react-flow__node').first().should('not.have.class', 'selected');
});
it('drags a node', () => {
const styleBeforeDrag = Cypress.$('.react-flow__node:first').css('transform');
cy.drag('.react-flow__node:first', { x: 200, y: 25 }).then(($el: JQuery<HTMLElement>) => {
const styleAfterDrag = $el.css('transform');
expect(styleBeforeDrag).to.not.equal(styleAfterDrag);
});
});
it('selects an edge', () => {
cy.get('.react-flow__edge').first().click().should('have.class', 'selected');
cy.get('.react-flow__pane').click();
cy.get('.react-flow__edge').first().should('not.have.class', 'selected');
});
});

View File

@@ -0,0 +1,312 @@
import ReactFlow, { ReactFlowProps, Viewport, useViewport, SnapGrid, CoordinateExtent, Node } from 'reactflow';
import ControlledFlow from '../../support/ControlledFlow';
import * as simpleflow from '../../fixtures/simpleflow';
const nodesOutsideOfView = [
{
...simpleflow.nodes[0],
position: {
x: -500,
y: 0,
},
},
{
...simpleflow.nodes[1],
position: {
x: 500,
y: 0,
},
},
];
describe('<ReactFlow />: View Props', () => {
it('uses fitView', () => {
cy.mount(<Comp nodes={nodesOutsideOfView} fitView minZoom={0.2} />).wait(200);
cy.get('.react-flow__node:first').isWithinViewport();
});
it('uses fitViewOptions', () => {
const fitViewOptions = { padding: 0.5 };
cy.mount(<ReactFlow nodes={[nodesOutsideOfView[0]]} fitView fitViewOptions={fitViewOptions} />).wait(200);
cy.get('.react-flow__node:first').isWithinViewport();
});
describe('uses minZoom', () => {
it('minZoom: 0.2', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(<Comp minZoom={0.2} onChange={onChangeSpy} />);
cy.zoomPane(10000).then(() => {
expect(getLatestViewport(onChangeSpy).zoom).to.be.eq(0.2);
});
});
it('minZoom: 1', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(<Comp minZoom={1} onChange={onChangeSpy} />);
cy.zoomPane(10000).then(() => {
expect(getLatestViewport(onChangeSpy).zoom).to.be.eq(1);
});
});
it('sets invalid defaultZoom', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
const defaultViewport = { x: 0, y: 0, zoom: 0.2 };
cy.mount(<Comp minZoom={1} defaultViewport={defaultViewport} onChange={onChangeSpy} />);
cy.zoomPane(10000).then(() => {
expect(getLatestViewport(onChangeSpy).zoom).to.be.eq(1);
});
});
});
describe('uses maxZoom', () => {
it('maxZoom: 1', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(<Comp maxZoom={1} onChange={onChangeSpy} />);
cy.zoomPane(-10000).then(() => {
expect(getLatestViewport(onChangeSpy).zoom).to.be.eq(1);
});
});
it('maxZoom: 2', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
cy.mount(<Comp maxZoom={2} onChange={onChangeSpy} />);
cy.zoomPane(-10000).then(() => {
expect(getLatestViewport(onChangeSpy).zoom).to.be.eq(2);
});
});
it('sets invalid defaultZoom', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
const defaultViewport = { x: 0, y: 0, zoom: 2 };
cy.mount(<Comp maxZoom={1.5} defaultViewport={defaultViewport} onChange={onChangeSpy} />);
cy.zoomPane(-10000).then(() => {
expect(getLatestViewport(onChangeSpy).zoom).to.be.eq(1.5);
});
});
});
describe('uses defaultViewport', () => {
it('defaultViewport: { x: 0, y: 0, zoom: 1 }', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
const defaultViewport = { x: 0, y: 0, zoom: 1 };
cy.mount(<Comp nodes={simpleflow.nodes} defaultViewport={defaultViewport} onChange={onChangeSpy} />).then(() => {
expect(getLatestViewport(onChangeSpy)).to.be.eql(defaultViewport);
});
});
it('defaultViewport: { x: 0, y: 0, zoom: 1.5 }', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
const defaultViewport = { x: 0, y: 0, zoom: 1.5 };
cy.mount(<Comp nodes={simpleflow.nodes} defaultViewport={defaultViewport} onChange={onChangeSpy} />).then(() => {
expect(getLatestViewport(onChangeSpy)).to.be.eql(defaultViewport);
});
});
it('defaultViewport: { x: 100, y: 100, zoom: 1.5 }', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
const defaultViewport = { x: 100, y: 100, zoom: 1.5 };
cy.mount(<Comp nodes={simpleflow.nodes} defaultViewport={defaultViewport} onChange={onChangeSpy} />).then(() => {
expect(getLatestViewport(onChangeSpy)).to.be.eql(defaultViewport);
});
});
it('defaultViewport: invalid { x: 0, y: 0, zoom: -1 }', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
const defaultViewport = { x: 0, y: 0, zoom: -1 };
cy.mount(<Comp nodes={simpleflow.nodes} defaultViewport={defaultViewport} onChange={onChangeSpy} />).then(() => {
// this should be min zoom because zoom is clamped
expect(getLatestViewport(onChangeSpy).zoom).to.be.eql(0.5);
});
});
it('defaultViewport: invalid { x: 0, y: 0, zoom: 100 }', () => {
const onChangeSpy = cy.spy().as('onChangeSpy');
const defaultViewport = { x: 0, y: 0, zoom: 100 };
cy.mount(<Comp nodes={simpleflow.nodes} defaultViewport={defaultViewport} onChange={onChangeSpy} />).then(() => {
// this should be max zoom because zoom is clamped
expect(getLatestViewport(onChangeSpy).zoom).to.be.eql(2);
});
});
});
it('uses snapGrid and snapToGrid', () => {
const snapGrid: SnapGrid = [100, 100];
cy.mount(<ControlledFlow initialNodes={simpleflow.nodes} snapGrid={snapGrid} snapToGrid />).then(() => {
cy.drag('.react-flow__node:first', { x: 125, y: 125 }).then(($el: any) => {
const transformAfterDrag = $el.css('transform');
const { m41: nodeX, m42: nodeY } = new DOMMatrixReadOnly(transformAfterDrag);
expect([nodeX, nodeY]).to.eql(snapGrid);
});
});
});
describe('uses onlyRenderVisibleElements', () => {
it('displays no nodes', () => {
cy.mount(<ControlledFlow initialNodes={nodesOutsideOfView} onlyRenderVisibleElements />).then(() => {
cy.get('.react-flow__node').should('have.length', 0);
});
});
it('displays one node', () => {
const nodes = nodesOutsideOfView.map((n) => {
if (n.id === '1') {
return { ...n, position: { x: 0, y: 0 } };
}
return n;
});
cy.mount(<ControlledFlow initialNodes={nodes} onlyRenderVisibleElements />).then(() => {
cy.get('.react-flow__node').should('have.length', 1);
});
});
});
it('uses translateExtent', () => {
const translateExtent: CoordinateExtent = [
[0, 0],
[1000, 1000],
];
cy.mount(<ControlledFlow translateExtent={translateExtent} />).then(() => {
const transformBeforeDrag = Cypress.$('.react-flow__viewport').css('transform');
cy.dragPane({ from: { x: 0, y: 0 }, to: { x: 100, y: 100 } }).then(() => {
const transformAfterDrag = Cypress.$('.react-flow__viewport').css('transform');
expect(transformBeforeDrag).to.eql(transformAfterDrag);
});
});
});
it('uses nodeExtent', () => {
const nodes: Node[] = [
{
id: '1',
position: { x: 200, y: 200 },
data: { label: '1' },
},
];
const nodeExtent: CoordinateExtent = [
[0, 0],
[50, 50],
];
cy.mount(<ControlledFlow nodes={nodes} nodeExtent={nodeExtent} />)
.wait(500)
.then(() => {
const transform = Cypress.$('.react-flow__node:first').css('transform');
const { m41: nodeX, m42: nodeY } = new DOMMatrixReadOnly(transform);
expect(nodeX).to.equal(50);
expect(nodeY).to.equal(50);
});
});
describe('uses preventScrolling', () => {
function ScrollFlow({ preventScrolling }: { preventScrolling: boolean }) {
return (
<div className="wrapper" style={{ height: 2000 }}>
<div className="inner" style={{ height: 500 }}>
<ControlledFlow preventScrolling={preventScrolling} />
</div>
</div>
);
}
it('lets user scroll', () => {
cy.mount(<ScrollFlow preventScrolling={false} />).then(() => {
cy.window().then((window) => {
cy.get('.react-flow__pane').trigger('wheel', {
wheelDelta: 240,
wheelDeltaX: 0,
wheelDeltaY: 240,
eventConstructor: 'WheelEvent',
view: window,
});
// @TODO: why is this not working?
// it seems that the event is somehow broken. This works fine when using the mouse.
// cy.window().its('scrollY').should('be.gt', 0);
});
});
});
it('does not user scroll', () => {
cy.mount(<ScrollFlow preventScrolling={true} />).then(() => {
cy.window().then((window) => {
cy.get('.react-flow__pane').trigger('wheel', {
wheelDelta: 240,
wheelDeltaX: 0,
wheelDeltaY: 240,
eventConstructor: 'WheelEvent',
view: window,
});
cy.window().its('scrollY').should('be.equal', 0);
});
});
});
});
describe('uses attributionPosition', () => {
it('displays it on the bottom right', () => {
cy.mount(<ControlledFlow />).then(() => {
cy.get('.react-flow__attribution').then(($el) => {
const { left, top, width, height } = $el[0].getBoundingClientRect();
expect(left).equal(window.innerWidth - width);
expect(top).equal(window.innerHeight - height);
});
});
});
it('displays it on the top left', () => {
cy.mount(<ControlledFlow attributionPosition="top-left" />).then(() => {
cy.get('.react-flow__attribution').then(($el) => {
const { left, top } = $el[0].getBoundingClientRect();
expect(left).equal(0);
expect(top).equal(0);
});
});
});
});
});
// test specific helpers
type CompProps = {
onChange?: (vp: Viewport) => void;
};
function InnerComp({ onChange }: CompProps) {
const viewport = useViewport();
onChange?.(viewport);
return null;
}
function Comp({ onChange, ...rest }: CompProps & ReactFlowProps) {
return (
<ReactFlow {...rest}>
<InnerComp onChange={onChange} />
</ReactFlow>
);
}
function getLatestViewport(onChangeSpy: any) {
return onChangeSpy.lastCall.args[0] as unknown as Viewport;
}

View File

@@ -1,13 +1,18 @@
import { isNode, isEdge, getOutgoers, getIncomers, addEdge } from '../../../dist/esm/index.js';
import { Node, Edge, isNode, isEdge, getOutgoers, getIncomers, addEdge } from 'reactflow';
const nodes = [
{ id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 } },
const nodes: Node[] = [
{
id: '1',
type: 'input',
data: { label: 'Node 1' },
position: { x: 250, y: 5 },
},
{ id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 } },
{ id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 } },
{ id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 } },
];
const edges = [
const edges: Edge[] = [
{ id: 'e1-2', source: '1', target: '2', animated: true },
{ id: 'e1-3', source: '1', target: '3' },
{ id: 'e2-3', source: '2', target: '3' },
@@ -43,25 +48,26 @@ describe('Graph Utils Testing', () => {
describe('tests addEdge function', () => {
it('adds edge', () => {
const newEdge = { source: '1', target: '4' };
const newEdge: Edge = { source: '1', target: '4', id: 'new-edge-1-4' };
const nextEdges = addEdge(newEdge, edges);
expect(nextEdges.length).to.be.equal(edges.length + 1);
});
it('tries to add existing edge', () => {
const newEdge = { source: '2', target: '3' };
const newEdge: Edge = { source: '2', target: '3', id: 'new-edge-2-3' };
const nextEdges = addEdge(newEdge, edges);
expect(nextEdges.length).to.be.equal(edges.length);
});
it('tries to add invalid edge', () => {
const newEdge = { nosource: '1', notarget: '3' };
// @ts-ignore
const newEdge: Edge = { nosource: '1', notarget: '3' };
try {
addEdge(newEdge, edges);
} catch (e) {
} catch (e: any) {
console.log(e.message);
expect(e.message).to.be.equal("Can't create edge. An edge needs a source and a target.");
@@ -69,3 +75,5 @@ describe('Graph Utils Testing', () => {
});
});
});
export {};

View File

@@ -1,6 +1,6 @@
describe('Basic Flow Rendering', () => {
before(() => {
cy.visit('/basic');
cy.visit('/');
});
it('renders a flow with three nodes', () => {
@@ -95,7 +95,7 @@ describe('Basic Flow Rendering', () => {
it('drags a node', () => {
const styleBeforeDrag = Cypress.$('.react-flow__node:first').css('transform');
cy.drag('.react-flow__node:first', { x: 500, y: 25 }).then(($el) => {
cy.drag('.react-flow__node:first', { x: 500, y: 25 }).then(($el: any) => {
const styleAfterDrag = $el.css('transform');
expect(styleBeforeDrag).to.not.equal(styleAfterDrag);
});
@@ -163,3 +163,5 @@ describe('Basic Flow Rendering', () => {
});
});
});
export {};

View File

@@ -79,3 +79,5 @@ describe('Controls Testing', () => {
});
});
});
export {};

View File

@@ -1,6 +1,6 @@
describe('DragHandle Flow Rendering', () => {
before(() => {
cy.visit('/draghandle');
cy.visit('/DragHandle');
});
it('renders a flow with a node', () => {
@@ -28,3 +28,5 @@ describe('DragHandle Flow Rendering', () => {
});
});
});
export {};

View File

@@ -1,6 +1,6 @@
describe('Empty Flow Rendering', () => {
before(() => {
cy.visit('/empty');
cy.visit('/Empty');
});
it('renders an empty flow', () => {
@@ -10,12 +10,15 @@ describe('Empty Flow Rendering', () => {
});
it('renders empty selection', () => {
cy.get('.react-flow__renderer').click();
cy.get('body')
.type('{shift}', { release: false })
.wait(50)
.get('.react-flow__selectionpane')
.trigger('mousedown', 'topLeft', { which: 1, force: true })
.trigger('mousemove', 'bottomLeft', { which: 1 })
.trigger('mouseup', 'bottomLeft', { force: true });
.trigger('mousedown', 400, 50, { which: 1, force: true })
.trigger('mousemove', 200, 200, { which: 1 })
.wait(50)
.trigger('mouseup', 200, 200, { force: true });
cy.get('body').type('{shift}', { release: true });
});
@@ -37,3 +40,5 @@ describe('Empty Flow Rendering', () => {
cy.get('.react-flow__edge').should('have.length', 1);
});
});
export {};

View File

@@ -1,6 +1,6 @@
describe('Hidden Flow Rendering', () => {
before(() => {
cy.visit('/hidden');
cy.visit('/Hidden');
});
it('renders empty flow', () => {
@@ -30,3 +30,5 @@ describe('Hidden Flow Rendering', () => {
cy.get('.react-flow__minimap-node').should('not.exist');
});
});
export {};

View File

@@ -1,6 +1,6 @@
describe('Interaction Flow Rendering', () => {
before(() => {
cy.visit('/interaction');
cy.visit('/Interaction');
});
it('renders initial flow', () => {
@@ -149,3 +149,5 @@ describe('Interaction Flow Rendering', () => {
});
});
});
export {};

View File

@@ -10,22 +10,23 @@ describe('Minimap Testing', () => {
it('has same number of nodes as the pane', () => {
const paneNodes = Cypress.$('.react-flow__node').length;
const minimapNodes = Cypress.$('.react-flow__minimap-node').length;
cy.wait(100);
cy.wait(200);
const minimapNodes = Cypress.$('.react-flow__minimap-node').length;
expect(paneNodes).equal(minimapNodes);
});
it('changes zoom level', () => {
const viewBoxBeforeZoom = Cypress.$('.react-flow__minimap').attr('viewBox');
const viewBoxBeforeZoom = Cypress.$('.react-flow__minimap svg').attr('viewBox');
const maskPathBeforeZoom = Cypress.$('.react-flow__minimap-mask').attr('d');
cy.get('.react-flow__pane')
.trigger('wheel', 'topLeft', { deltaY: -200 })
.wait(50)
.then(() => {
const viewBoxAfterZoom = Cypress.$('.react-flow__minimap').attr('viewBox');
const viewBoxAfterZoom = Cypress.$('.react-flow__minimap svg').attr('viewBox');
const maskPathAfterZoom = Cypress.$('.react-flow__minimap-mask').attr('d');
expect(viewBoxBeforeZoom).to.not.equal(viewBoxAfterZoom);
@@ -49,7 +50,7 @@ describe('Minimap Testing', () => {
});
it('changes node positions via pane drag', () => {
const viewBoxBeforeDrag = Cypress.$('.react-flow__minimap').attr('viewBox');
const viewBoxBeforeDrag = Cypress.$('.react-flow__minimap svg').attr('viewBox');
const maskPathBeforeDrag = Cypress.$('.react-flow__minimap-mask').attr('d');
// for d3 we have to pass the window to the event
@@ -61,7 +62,7 @@ describe('Minimap Testing', () => {
.wait(50)
.trigger('mouseup', { force: true, view: win })
.then(() => {
const viewBoxAfterDrag = Cypress.$('.react-flow__minimap').attr('viewBox');
const viewBoxAfterDrag = Cypress.$('.react-flow__minimap svg').attr('viewBox');
const maskPathAfterDrag = Cypress.$('.react-flow__minimap-mask').attr('d');
expect(viewBoxBeforeDrag).to.not.equal(viewBoxAfterDrag);
@@ -70,3 +71,5 @@ describe('Minimap Testing', () => {
});
});
});
export {};

View File

@@ -0,0 +1,22 @@
import { Node, Edge } from 'reactflow';
export const nodes: Node[] = [
{
id: '1',
data: { label: 'Node 1' },
position: { x: 0, y: 0 },
},
{
id: '2',
data: { label: 'Node 2' },
position: { x: 200, y: 200 },
},
];
export const edges: Edge[] = [
{
id: 'e1',
source: '1',
target: '2',
},
];

View File

@@ -0,0 +1,65 @@
import { useCallback, useState } from 'react';
import {
ReactFlow,
Node,
Edge,
NodeChange,
EdgeChange,
applyNodeChanges,
applyEdgeChanges,
Connection,
addEdge,
ReactFlowProps,
} from 'reactflow';
function ControlledFlow({
addOnNodeChangeHandler = true,
addOnEdgeChangeHandler = true,
addOnConnectHandler = true,
initialNodes = [],
initialEdges = [],
...rest
}: {
initialNodes?: Node[];
initialEdges?: Edge[];
addOnNodeChangeHandler?: boolean;
addOnEdgeChangeHandler?: boolean;
addOnConnectHandler?: boolean;
} & Partial<ReactFlowProps>) {
const [nodes, setNodes] = useState<Node[]>(initialNodes);
const [edges, setEdges] = useState<Edge[]>(initialEdges);
const onNodesChange = useCallback(
(changes: NodeChange[]) => setNodes((nds) => applyNodeChanges(changes, nds)),
[setNodes]
);
const onEdgesChange = useCallback(
(changes: EdgeChange[]) => setEdges((eds) => applyEdgeChanges(changes, eds)),
[setEdges]
);
const onConnect = useCallback((params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
const handlers: {
onNodesChange?: (changes: NodeChange[]) => void;
onEdgesChange?: (changes: EdgeChange[]) => void;
onConnect?: (params: Connection | Edge) => void;
} = {};
if (addOnNodeChangeHandler) {
handlers.onNodesChange = onNodesChange;
}
if (addOnEdgeChangeHandler) {
handlers.onEdgesChange = onEdgesChange;
}
if (addOnConnectHandler) {
handlers.onConnect = onConnect;
}
return <ReactFlow nodes={nodes} edges={edges} {...handlers} {...rest} />;
}
export default ControlledFlow;

View File

@@ -0,0 +1,58 @@
Cypress.Commands.add('drag', (selector, { x, y }) =>
cy.window().then((window) => {
const elementToDrag = cy.get(selector as string);
return elementToDrag.then(($el) => {
const { left, top, width, height } = $el[0].getBoundingClientRect();
const centerX = left + width / 2;
const centerY = top + height / 2;
const nextX: number = centerX + x;
const nextY: number = centerY + y;
return elementToDrag
.trigger('mousedown', { view: window })
.trigger('mousemove', nextX, nextY, { force: true })
.wait(50)
.trigger('mouseup', { view: window, force: true });
});
})
);
Cypress.Commands.add('dragPane', ({ from, to }) =>
cy
.window()
.then((window) =>
cy
.get('.react-flow__pane')
.trigger('mousedown', from.x, from.y, { view: window })
.trigger('mousemove', to.x, to.y)
.trigger('mouseup', { force: true, view: window })
)
);
Cypress.Commands.add('zoomPane', (wheelDelta: number) =>
cy.get('.react-flow__pane').trigger('wheel', 'center', { deltaY: wheelDelta }).wait(250)
);
Cypress.Commands.add('isWithinViewport', { prevSubject: true }, (subject) => {
const rect = subject[0].getBoundingClientRect();
expect(rect.top).to.be.within(0, window.innerHeight);
expect(rect.right).to.be.within(0, window.innerWidth);
expect(rect.bottom).to.be.within(0, window.innerHeight);
expect(rect.left).to.be.within(0, window.innerWidth);
return subject;
});
Cypress.Commands.add('isOutsideViewport', { prevSubject: true }, (subject) => {
const rect = subject[0].getBoundingClientRect();
expect(rect.top).not.to.be.within(0, window.innerHeight);
expect(rect.right).not.to.be.within(0, window.innerWidth);
expect(rect.bottom).not.to.be.within(0, window.innerHeight);
expect(rect.left).not.to.be.within(0, window.innerWidth);
return subject;
});
export {};

View File

@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title>Components App</title>
<style>
html,
body,
#root {
margin: 0;
height: 100%;
}
</style>
</head>
<body>
<div data-cy-root id="root"></div>
</body>
</html>

View File

@@ -0,0 +1,27 @@
///<reference types="cypress" />
///<reference types="@cypress/skip-test" />
///<reference types="cypress-real-events" />
import './commands';
import 'cypress-real-events/support';
import { mount } from 'cypress/react18';
import { XYPosition } from 'reactflow';
import 'reactflow/dist/style.css';
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
interface Chainable {
mount: typeof mount;
drag: (selector: string, toPosition: XYPosition) => Cypress.Chainable<JQuery<HTMLElement>>;
dragPane: ({ from, to }: { from: XYPosition; to: XYPosition }) => Cypress.Chainable<JQuery<HTMLElement>>;
zoomPane: (wheelDelta: number) => Cypress.Chainable<JQuery<HTMLElement>>;
isWithinViewport: () => Cypress.Chainable<JQuery<HTMLElement>>;
isOutsideViewport: () => Cypress.Chainable<JQuery<HTMLElement>>;
}
}
}
Cypress.Commands.add('mount', mount);

View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@@ -0,0 +1,34 @@
{
"name": "@reactflow/examples",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite --port 3000 --open",
"serve": "vite serve --port 3000",
"test:dev": "cypress open",
"test": "pnpm test-component && pnpm test-e2e",
"test-component": "cypress run --component",
"test-e2e-cypress": "cypress run --e2e --headless",
"test-e2e": "start-server-and-test 'pnpm serve' http-get://localhost:3000 'pnpm test-e2e-cypress'"
},
"dependencies": {
"dagre": "^0.8.5",
"localforage": "^1.10.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.3.0",
"reactflow": "workspace:*"
},
"devDependencies": {
"@cypress/skip-test": "^2.6.1",
"@types/react": "^18.0.17",
"@types/react-dom": "^18.0.6",
"@vitejs/plugin-react": "^2.1.0",
"cypress": "^10.6.0",
"cypress-real-events": "^1.7.1",
"start-server-and-test": "^1.14.0",
"typescript": "^4.8.3",
"vite": "^3.1.0"
}
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,53 +1,53 @@
import { ChangeEvent } from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter, Route, Routes, useLocation, useNavigate } from 'react-router-dom';
import Basic from './Basic';
import ControlledUncontrolled from './ControlledUncontrolled';
import CustomConnectionLine from './CustomConnectionLine';
import CustomNode from './CustomNode';
import DefaultNodes from './DefaultNodes';
import DragHandle from './DragHandle';
import DragNDrop from './DragNDrop';
import Edges from './Edges';
import EdgeTypes from './EdgeTypes';
import Empty from './Empty';
import FloatingEdges from './FloatingEdges';
import Hidden from './Hidden';
import './index.css';
import Interaction from './Interaction';
import Layouting from './Layouting';
import MultiFlows from './MultiFlows';
import NestedNodes from './NestedNodes';
import NodeTypeChange from './NodeTypeChange';
import NodeTypesObjectChange from './NodeTypesObjectChange';
import Overview from './Overview';
import Provider from './Provider';
import SaveRestore from './SaveRestore';
import Stress from './Stress';
import Subflow from './Subflow';
import SwitchFlow from './Switch';
import TouchDevice from './TouchDevice';
import Undirectional from './Undirectional';
import UpdatableEdge from './UpdatableEdge';
import UpdateNode from './UpdateNode';
import UseUpdateNodeInternals from './UseUpdateNodeInternals';
import UseReactFlow from './UseReactFlow';
import Validation from './Validation';
import UseKeyPress from './UseKeyPress';
import Basic from '../examples/Basic';
import Backgrounds from '../examples/Backgrounds';
import ControlledUncontrolled from '../examples/ControlledUncontrolled';
import CustomConnectionLine from '../examples/CustomConnectionLine';
import CustomNode from '../examples/CustomNode';
import DefaultNodes from '../examples/DefaultNodes';
import DragHandle from '../examples/DragHandle';
import DragNDrop from '../examples/DragNDrop';
import Edges from '../examples/Edges';
import EdgeTypes from '../examples/EdgeTypes';
import Empty from '../examples/Empty';
import FloatingEdges from '../examples/FloatingEdges';
import Hidden from '../examples/Hidden';
import Interaction from '../examples/Interaction';
import Layouting from '../examples/Layouting';
import MultiFlows from '../examples/MultiFlows';
import NestedNodes from '../examples/NestedNodes';
import NodeTypeChange from '../examples/NodeTypeChange';
import NodeTypesObjectChange from '../examples/NodeTypesObjectChange';
import Overview from '../examples/Overview';
import Provider from '../examples/Provider';
import SaveRestore from '../examples/SaveRestore';
import Stress from '../examples/Stress';
import Subflow from '../examples/Subflow';
import SwitchFlow from '../examples/Switch';
import TouchDevice from '../examples/TouchDevice';
import Undirectional from '../examples/Undirectional';
import UpdatableEdge from '../examples/UpdatableEdge';
import UpdateNode from '../examples/UpdateNode';
import UseUpdateNodeInternals from '../examples/UseUpdateNodeInternals';
import UseReactFlow from '../examples/UseReactFlow';
import Validation from '../examples/Validation';
import UseKeyPress from '../examples/UseKeyPress';
import EdgeRouting from '../examples/EdgeRouting';
const routes = [
{
path: '/',
component: Overview,
},
{
path: '/basic',
component: Basic,
},
{
path: '/default-nodes',
component: DefaultNodes,
path: '/backgrounds',
component: Backgrounds,
},
{
path: '/controlled-uncontrolled',
component: ControlledUncontrolled,
},
{
path: '/custom-connectionline',
@@ -57,6 +57,10 @@ const routes = [
path: '/custom-node',
component: CustomNode,
},
{
path: '/default-nodes',
component: DefaultNodes,
},
{
path: '/draghandle',
component: DragHandle,
@@ -73,6 +77,10 @@ const routes = [
path: '/edge-types',
component: EdgeTypes,
},
{
path: '/edge-routing',
component: EdgeRouting,
},
{
path: '/empty',
component: Empty,
@@ -109,6 +117,10 @@ const routes = [
path: '/nodetypesobject-change',
component: NodeTypesObjectChange,
},
{
path: '/overview',
component: Overview,
},
{
path: '/provider',
component: Provider,
@@ -157,10 +169,6 @@ const routes = [
path: '/validation',
component: Validation,
},
{
path: '/controlled-uncontrolled',
component: ControlledUncontrolled,
},
{
path: '/use-key-press',
component: UseKeyPress,
@@ -180,7 +188,7 @@ const Header = () => {
<select defaultValue={location.pathname} onChange={onChange}>
{routes.map((route) => (
<option value={route.path} key={route.path}>
{route.path === '/' ? 'overview' : route.path.substring(1, route.path.length)}
{route.path === '/' ? 'basic' : route.path.substring(1, route.path.length)}
</option>
))}
</select>
@@ -188,10 +196,7 @@ const Header = () => {
);
};
const container = document.getElementById('root');
const root = createRoot(container!);
root.render(
export default () => (
<BrowserRouter>
<Header />
<Routes>

View File

@@ -0,0 +1,42 @@
import React, { FC } from 'react';
import ReactFlow, {
Node,
ReactFlowProvider,
useNodesState,
Background,
BackgroundProps,
BackgroundVariant,
} from 'reactflow';
import styles from './style.module.css';
const initialNodes: Node[] = [
{
id: '1',
data: { label: 'Node 1' },
position: { x: 50, y: 50 },
},
];
const Flow: FC<{ id: string; bgProps: BackgroundProps }> = ({ id, bgProps }) => {
const [nodes, , onNodesChange] = useNodesState(initialNodes);
return (
<ReactFlowProvider>
<ReactFlow nodes={nodes} onNodesChange={onNodesChange} id={id}>
<Background {...bgProps} />
</ReactFlow>
</ReactFlowProvider>
);
};
const Backgrounds: FC = () => (
<div className={styles.wrapper}>
<Flow id="flow-a" bgProps={{ variant: BackgroundVariant.Dots }} />
<Flow id="flow-b" bgProps={{ variant: BackgroundVariant.Lines, gap: [50, 50] }} />
<Flow id="flow-c" bgProps={{ variant: BackgroundVariant.Cross, gap: [100, 50] }} />
</div>
);
export default Backgrounds;

View File

@@ -0,0 +1,13 @@
.wrapper {
display: flex;
height: 100%;
}
.wrapper :global .react-flow {
width: 100%;
height: 100%;
}
.wrapper :global .react-flow {
border-right: 1px solid #ddd;
}

View File

@@ -1,23 +1,45 @@
import { MouseEvent } from 'react';
import React, { MouseEvent } from 'react';
import ReactFlow, {
ReactFlowProvider,
MiniMap,
Background,
BackgroundVariant,
Controls,
ReactFlowProvider,
Node,
Edge,
useReactFlow,
} from 'react-flow-renderer';
} from 'reactflow';
const onNodeDrag = (_: MouseEvent, node: Node) => console.log('drag', node);
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
const initialNodes: Node[] = [
{ id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 }, className: 'light' },
{ id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 }, className: 'light' },
{ id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 }, className: 'light' },
{ id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 }, className: 'light' },
{
id: '1',
type: 'input',
data: { label: 'Node 1' },
position: { x: 250, y: 5 },
className: 'light',
},
{
id: '2',
data: { label: 'Node 2' },
position: { x: 100, y: 100 },
className: 'light',
},
{
id: '3',
data: { label: 'Node 3' },
position: { x: 400, y: 100 },
className: 'light',
},
{
id: '4',
data: { label: 'Node 4' },
position: { x: 400, y: 200 },
className: 'light',
},
];
const initialEdges: Edge[] = [
@@ -71,6 +93,8 @@ const BasicFlow = () => {
selectNodesOnDrag={false}
>
<Background variant={BackgroundVariant.Lines} />
<MiniMap />
<Controls />
<div style={{ position: 'absolute', right: 10, top: 10, zIndex: 4 }}>
<button onClick={resetTransform} style={{ marginRight: 5 }}>

View File

@@ -1,19 +1,41 @@
import React from 'react';
import ReactFlow, {
useReactFlow,
Background,
BackgroundVariant,
Node,
Edge,
ReactFlowProvider,
useNodesState,
useEdgesState,
} from 'react-flow-renderer';
Background,
BackgroundVariant,
} from 'reactflow';
const defaultNodes: Node[] = [
{ id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 }, className: 'light' },
{ id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 }, className: 'light' },
{ id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 }, className: 'light' },
{ id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 }, className: 'light' },
{
id: '1',
type: 'input',
data: { label: 'Node 1' },
position: { x: 250, y: 5 },
className: 'light',
},
{
id: '2',
data: { label: 'Node 2' },
position: { x: 100, y: 100 },
className: 'light',
},
{
id: '3',
data: { label: 'Node 3' },
position: { x: 400, y: 100 },
className: 'light',
},
{
id: '4',
data: { label: 'Node 4' },
position: { x: 400, y: 200 },
className: 'light',
},
];
const defaultEdges: Edge[] = [

View File

@@ -1,7 +1,7 @@
import React, { FC } from 'react';
import { ConnectionLineComponentProps } from 'react-flow-renderer';
import { ConnectionLineComponentProps } from 'reactflow';
const ConnectionLine: FC<ConnectionLineComponentProps> = ({ sourceX, sourceY, targetX, targetY, toX, toY }) => {
const ConnectionLine: FC<ConnectionLineComponentProps> = ({ fromX, fromY, toX, toY }) => {
return (
<g>
<path
@@ -9,7 +9,7 @@ const ConnectionLine: FC<ConnectionLineComponentProps> = ({ sourceX, sourceY, ta
stroke="#222"
strokeWidth={1.5}
className="animated"
d={`M${sourceX},${sourceY} C ${sourceX} ${targetY} ${sourceX} ${targetY} ${targetX},${targetY}`}
d={`M${fromX},${fromY} C ${fromX} ${toY} ${fromX} ${toY} ${toX},${toY}`}
/>
<circle cx={toX} cy={toY} fill="#fff" r={3} stroke="#222" strokeWidth={1.5} />
</g>

View File

@@ -1,18 +1,25 @@
import { useCallback } from 'react';
import React, { useCallback } from 'react';
import ReactFlow, {
Node,
addEdge,
Background,
BackgroundVariant,
Connection,
Edge,
useNodesState,
useEdgesState,
} from 'react-flow-renderer';
Background,
BackgroundVariant,
} from 'reactflow';
import ConnectionLine from './ConnectionLine';
const initialNodes: Node[] = [{ id: '1', type: 'default', data: { label: 'Node 1' }, position: { x: 250, y: 5 } }];
const initialNodes: Node[] = [
{
id: '1',
type: 'default',
data: { label: 'Node 1' },
position: { x: 250, y: 5 },
},
];
const initialEdges: Edge[] = [];
const ConnectionLineFlow = () => {

View File

@@ -1,14 +1,27 @@
import React, { memo, FC, CSSProperties } from 'react';
import { Handle, Position, NodeProps, Connection, Edge } from 'react-flow-renderer';
import React, { memo, FC, CSSProperties, useCallback } from 'react';
import { Handle, Position, NodeProps, Connection, Edge, useOnViewportChange, Viewport } from 'reactflow';
const targetHandleStyle: CSSProperties = { background: '#555' };
const sourceHandleStyleA: CSSProperties = { ...targetHandleStyle, top: 10 };
const sourceHandleStyleB: CSSProperties = { ...targetHandleStyle, bottom: 10, top: 'auto' };
const sourceHandleStyleB: CSSProperties = {
...targetHandleStyle,
bottom: 10,
top: 'auto',
};
const onConnect = (params: Connection | Edge) => console.log('handle onConnect', params);
const ColorSelectorNode: FC<NodeProps> = ({ data, isConnectable }) => {
const onStart = useCallback((viewport: Viewport) => console.log('onStart', viewport), []);
const onChange = useCallback((viewport: Viewport) => console.log('onChange', viewport), []);
const onEnd = useCallback((viewport: Viewport) => console.log('onEnd', viewport), []);
useOnViewportChange({
onStart,
onChange,
onEnd,
});
return (
<>
<Handle type="target" position={Position.Left} style={targetHandleStyle} onConnect={onConnect} />

View File

@@ -1,10 +1,8 @@
import { useState, useEffect, MouseEvent } from 'react';
import { ChangeEvent } from 'react';
import React, { useState, useEffect, MouseEvent, ChangeEvent, useCallback } from 'react';
import ReactFlow, {
addEdge,
MiniMap,
Controls,
addEdge,
Node,
ReactFlowInstance,
Position,
@@ -12,7 +10,7 @@ import ReactFlow, {
Connection,
useNodesState,
useEdgesState,
} from 'react-flow-renderer';
} from 'reactflow';
import ColorSelectorNode from './ColorSelectorNode';
@@ -93,14 +91,37 @@ const CustomNodeFlow = () => {
]);
setEdges([
{ id: 'e1-2', source: '1', target: '2', animated: true, style: { stroke: '#fff' } },
{ id: 'e2a-3', source: '2', sourceHandle: 'a', target: '3', animated: true, style: { stroke: '#fff' } },
{ id: 'e2b-4', source: '2', sourceHandle: 'b', target: '4', animated: true, style: { stroke: '#fff' } },
{
id: 'e1-2',
source: '1',
target: '2',
animated: true,
style: { stroke: '#fff' },
},
{
id: 'e2a-3',
source: '2',
sourceHandle: 'a',
target: '3',
animated: true,
style: { stroke: '#fff' },
},
{
id: 'e2b-4',
source: '2',
sourceHandle: 'b',
target: '4',
animated: true,
style: { stroke: '#fff' },
},
]);
}, []);
const onConnect = (connection: Connection) =>
setEdges((eds) => addEdge({ ...connection, animated: true, style: { stroke: '#fff' } }, eds));
const onConnect = useCallback(
(connection: Connection) =>
setEdges((eds) => addEdge({ ...connection, animated: true, style: { stroke: '#fff' } }, eds)),
[setEdges]
);
return (
<ReactFlow

View File

@@ -1,17 +1,32 @@
import ReactFlow, {
useReactFlow,
Background,
BackgroundVariant,
Node,
Edge,
ReactFlowProvider,
} from 'react-flow-renderer';
import React from 'react';
import ReactFlow, { useReactFlow, Node, Edge, ReactFlowProvider, Background, BackgroundVariant } from 'reactflow';
const defaultNodes: Node[] = [
{ id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 }, className: 'light' },
{ id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 }, className: 'light' },
{ id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 }, className: 'light' },
{ id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 }, className: 'light' },
{
id: '1',
type: 'input',
data: { label: 'Node 1' },
position: { x: 250, y: 5 },
className: 'light',
},
{
id: '2',
data: { label: 'Node 2' },
position: { x: 100, y: 100 },
className: 'light',
},
{
id: '3',
data: { label: 'Node 3' },
position: { x: 400, y: 100 },
className: 'light',
},
{
id: '4',
data: { label: 'Node 4' },
position: { x: 400, y: 200 },
className: 'light',
},
];
const defaultEdges: Edge[] = [

View File

@@ -1,6 +1,6 @@
import { memo, FC } from 'react';
import React, { memo, FC } from 'react';
import { Handle, Position, NodeProps, Connection, Edge } from 'react-flow-renderer';
import { Handle, Position, NodeProps, Connection, Edge } from 'reactflow';
const onConnect = (params: Connection | Edge) => console.log('handle onConnect', params);

View File

@@ -1,5 +1,5 @@
import { MouseEvent } from 'react';
import ReactFlow, { Node, Edge, useNodesState, useEdgesState } from 'react-flow-renderer';
import React, { MouseEvent } from 'react';
import ReactFlow, { Node, Edge, useNodesState, useEdgesState } from 'reactflow';
import DragHandleNode from './DragHandleNode';

View File

@@ -0,0 +1,40 @@
import React, { DragEvent } from 'react';
import styles from './dnd.module.css';
const onDragStart = (event: DragEvent, nodeType: string) => {
event.dataTransfer.setData('application/reactflow', nodeType);
event.dataTransfer.effectAllowed = 'move';
};
const Sidebar = () => {
return (
<aside className={styles.aside}>
<div className={styles.description}>
You can drag these nodes to the pane on the left.
</div>
<div
className='react-flow__node-input'
onDragStart={(event: DragEvent) => onDragStart(event, 'input')}
draggable
>
Input Node
</div>
<div
className='react-flow__node-default'
onDragStart={(event: DragEvent) => onDragStart(event, 'default')}
draggable
>
Default Node
</div>
<div
className='react-flow__node-output'
onDragStart={(event: DragEvent) => onDragStart(event, 'output')}
draggable
>
Output Node
</div>
</aside>
);
};
export default Sidebar;

View File

@@ -4,34 +4,34 @@
height: 100%;
}
.dndflow aside {
.aside {
border-right: 1px solid #eee;
padding: 15px 10px;
font-size: 12px;
background: #fcfcfc;
}
.dndflow aside > * {
.aside > * {
margin-bottom: 10px;
cursor: grab;
}
.dndflow aside .description {
.description {
margin-bottom: 10px;
}
.dndflow .reactflow-wrapper {
.wrapper {
flex-grow: 1;
height: 100%;
}
@media screen and (min-width: 768px) {
.dndflow {
flex-direction: row;
}
.dndflow aside {
.aside {
width: 20%;
max-width: 180px;
}
.dndflow {
flex-direction: row;
}
}

View File

@@ -1,21 +1,29 @@
import { useState, DragEvent } from 'react';
import React, { useState, DragEvent } from 'react';
import ReactFlow, {
ReactFlowProvider,
addEdge,
Controls,
ReactFlowInstance,
Connection,
Edge,
Node,
useNodesState,
useEdgesState,
} from 'react-flow-renderer';
Controls,
NodeOrigin,
} from 'reactflow';
import Sidebar from './Sidebar';
import './dnd.css';
import styles from './dnd.module.css';
const initialNodes: Node[] = [{ id: '1', type: 'input', data: { label: 'input node' }, position: { x: 250, y: 5 } }];
const initialNodes: Node[] = [
{
id: '1',
type: 'input',
data: { label: 'input node' },
position: { x: 250, y: 5 },
},
];
const onDragOver = (event: DragEvent) => {
event.preventDefault();
@@ -25,6 +33,8 @@ const onDragOver = (event: DragEvent) => {
let id = 0;
const getId = () => `dndnode_${id++}`;
const nodeOrigin: NodeOrigin = [0.5, 0.5];
const DnDFlow = () => {
const [reactFlowInstance, setReactFlowInstance] = useState<ReactFlowInstance>();
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
@@ -38,7 +48,10 @@ const DnDFlow = () => {
if (reactFlowInstance) {
const type = event.dataTransfer.getData('application/reactflow');
const position = reactFlowInstance.project({ x: event.clientX, y: event.clientY - 40 });
const position = reactFlowInstance.project({
x: event.clientX,
y: event.clientY - 40,
});
const newNode: Node = {
id: getId(),
type,
@@ -51,9 +64,9 @@ const DnDFlow = () => {
};
return (
<div className="dndflow">
<div className={styles.dndflow}>
<ReactFlowProvider>
<div className="reactflow-wrapper">
<div className={styles.wrapper}>
<ReactFlow
nodes={nodes}
edges={edges}
@@ -63,6 +76,7 @@ const DnDFlow = () => {
onInit={onInit}
onDrop={onDrop}
onDragOver={onDragOver}
nodeOrigin={nodeOrigin}
>
<Controls />
</ReactFlow>

View File

@@ -0,0 +1,123 @@
import React from 'react';
import ReactFlow, { Node, Edge, Position, MarkerType } from 'reactflow';
const nodes: Node[] = [
// LTR
{
id: '1',
position: { x: 50, y: -100 },
data: { label: 'Source' },
sourcePosition: Position.Right,
targetPosition: Position.Right,
style: { background: 'rgba(255,255,255,0.5)' },
},
{
id: '2',
position: { x: -100, y: 0 },
data: { label: 'Target' },
sourcePosition: Position.Left,
targetPosition: Position.Left,
style: { background: 'rgba(255,255,255,0.5)' },
},
// Right Right
{
id: '3',
position: { x: -100, y: 250 },
data: { label: 'Source' },
sourcePosition: Position.Right,
targetPosition: Position.Right,
style: { background: 'rgba(255,255,255,0.5)' },
},
{
id: '4',
position: { x: 50, y: 150 },
data: { label: 'Target' },
sourcePosition: Position.Right,
targetPosition: Position.Right,
style: { background: 'rgba(255,255,255,0.5)' },
},
// Right Top
{
id: '5',
position: { x: -100, y: 450 },
data: { label: 'Source' },
sourcePosition: Position.Right,
targetPosition: Position.Right,
style: { background: 'rgba(255,255,255,0.5)' },
},
{
id: '6',
position: { x: 100, y: 400 },
data: { label: 'Target' },
sourcePosition: Position.Top,
targetPosition: Position.Top,
style: { background: 'rgba(255,255,255,0.5)' },
},
// Right Bottom
{
id: '7',
position: { x: 100, y: 700 },
data: { label: 'Source' },
sourcePosition: Position.Right,
targetPosition: Position.Right,
style: { background: 'rgba(255,255,255,0.5)' },
},
{
id: '8',
position: { x: -100, y: 600 },
data: { label: 'Target' },
sourcePosition: Position.Bottom,
targetPosition: Position.Bottom,
style: { background: 'rgba(255,255,255,0.5)' },
},
];
const edges: Edge[] = [
{
id: 'e1-2',
source: '1',
target: '2',
pathOptions: {
offset: 30,
},
interactionWidth: 0,
},
{
id: 'e3-4',
source: '3',
target: '4',
pathOptions: {
borderRadius: 2,
},
interactionWidth: 0,
},
{
id: 'e4-5',
source: '5',
target: '6',
},
{
id: 'e7-8',
source: '7',
target: '8',
},
];
const defaultEdgeOptions = {
label: 'Edge Label',
type: 'smoothstep',
markerEnd: {
type: MarkerType.ArrowClosed,
},
style: {
strokeWidth: 1,
},
};
const SimpleEdge = () => {
return <ReactFlow defaultNodes={nodes} defaultEdges={edges} fitView defaultEdgeOptions={defaultEdgeOptions} />;
};
export default SimpleEdge;

View File

@@ -1,18 +1,20 @@
/**
* Example for checking the different edge types and source and target positions
*/
import React, { useCallback } from 'react';
import ReactFlow, {
addEdge,
MiniMap,
Controls,
Background,
Controls,
MiniMap,
addEdge,
ReactFlowInstance,
Connection,
Edge,
useNodesState,
useEdgesState,
} from 'react-flow-renderer';
} from 'reactflow';
import { getElements } from './utils';
const onInit = (reactFlowInstance: ReactFlowInstance) => {
@@ -28,7 +30,7 @@ const deleteKeyCode = ['AltLeft+KeyD', 'Backspace'];
const EdgeTypesFlow = () => {
const [nodes, , onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = (params: Connection | Edge) => setEdges((eds) => addEdge(params, eds));
const onConnect = useCallback((params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
return (
<ReactFlow
@@ -39,7 +41,6 @@ const EdgeTypesFlow = () => {
onInit={onInit}
onConnect={onConnect}
minZoom={0.2}
zoomOnScroll={false}
selectionKeyCode="a+s"
multiSelectionKeyCode={multiSelectionKeyCode}
deleteKeyCode={deleteKeyCode}

View File

@@ -1,4 +1,4 @@
import { Edge, Node, Position } from 'react-flow-renderer';
import { Edge, Node, Position } from 'reactflow';
const nodeWidth = 80;
const nodeGapWidth = nodeWidth * 2;
@@ -63,7 +63,10 @@ export function getElements(): { nodes: Node[]; edges: Edge[] } {
for (let offsetIndex = 0; offsetIndex < offsets.length; offsetIndex++) {
const currOffset = offsets[offsetIndex];
const style = { ...nodeStyle, background: nodeColors[sourceTargetIndex][edgeTypeIndex] };
const style = {
...nodeStyle,
background: nodeColors[sourceTargetIndex][edgeTypeIndex],
};
const sourcePosition = {
x: offsetIndex * nodeWidth * 4,
y: edgeTypeIndex * 300 + sourceTargetIndex * edgeTypes.length * 300,

View File

@@ -1,5 +1,5 @@
import { FC } from 'react';
import { EdgeProps, getBezierPath } from 'react-flow-renderer';
import { EdgeProps, getBezierPath } from 'reactflow';
const CustomEdge: FC<EdgeProps> = ({
id,
@@ -11,7 +11,14 @@ const CustomEdge: FC<EdgeProps> = ({
targetPosition,
data,
}) => {
const edgePath = getBezierPath({ sourceX, sourceY, sourcePosition, targetX, targetY, targetPosition });
const [edgePath] = getBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
});
return (
<>

View File

@@ -1,5 +1,5 @@
import { FC } from 'react';
import { EdgeProps, getBezierPath, EdgeText, getBezierEdgeCenter } from 'react-flow-renderer';
import { EdgeProps, getBezierPath, EdgeText } from 'reactflow';
const CustomEdge: FC<EdgeProps> = ({
id,
@@ -11,20 +11,21 @@ const CustomEdge: FC<EdgeProps> = ({
targetPosition,
data,
}) => {
const edgePath = getBezierPath({ sourceX, sourceY, sourcePosition, targetX, targetY, targetPosition });
const [centerX, centerY] = getBezierEdgeCenter({
const [edgePath, labelX, labelY] = getBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
});
return (
<>
<path id={id} className="react-flow__edge-path" d={edgePath} />
<EdgeText
x={centerX}
y={centerY}
x={labelX}
y={labelY}
label={data.text}
labelStyle={{ fill: 'white' }}
labelShowBg

View File

@@ -1,22 +1,21 @@
import { MouseEvent } from 'react';
import React, { MouseEvent, useCallback } from 'react';
import ReactFlow, {
addEdge,
Background,
Connection,
Controls,
Background,
MiniMap,
addEdge,
Connection,
Edge,
EdgeTypes,
MarkerType,
MiniMap,
Node,
ReactFlowInstance,
useEdgesState,
useNodesState,
} from 'react-flow-renderer';
} from 'reactflow';
import CustomEdge from './CustomEdge';
import CustomEdge2 from './CustomEdge2';
const onInit = (reactFlowInstance: ReactFlowInstance) => reactFlowInstance.fitView();
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
const onEdgeClick = (_: MouseEvent, edge: Edge) => console.log('click', edge);
@@ -26,7 +25,12 @@ const onEdgeMouseMove = (_: MouseEvent, edge: Edge) => console.log('move', edge)
const onEdgeMouseLeave = (_: MouseEvent, edge: Edge) => console.log('leave', edge);
const initialNodes: Node[] = [
{ id: '1', type: 'input', data: { label: 'Input 1' }, position: { x: 250, y: 0 } },
{
id: '1',
type: 'input',
data: { label: 'Input 1' },
position: { x: 250, y: 0 },
},
{ id: '2', data: { label: 'Node 2' }, position: { x: 150, y: 100 } },
{ id: '2a', data: { label: 'Node 2a' }, position: { x: 0, y: 180 } },
{ id: '2b', data: { label: 'Node 2b' }, position: { x: -40, y: 300 } },
@@ -34,15 +38,47 @@ const initialNodes: Node[] = [
{ id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 300 } },
{ id: '3a', data: { label: 'Node 3a' }, position: { x: 150, y: 300 } },
{ id: '5', data: { label: 'Node 5' }, position: { x: 250, y: 400 } },
{ id: '6', type: 'output', data: { label: 'Output 6' }, position: { x: 50, y: 550 } },
{ id: '7', type: 'output', data: { label: 'Output 7' }, position: { x: 250, y: 550 } },
{ id: '8', type: 'output', data: { label: 'Output 8' }, position: { x: 525, y: 600 } },
{ id: '9', type: 'output', data: { label: 'Output 9' }, position: { x: 675, y: 500 } },
{
id: '6',
type: 'output',
data: { label: 'Output 6' },
position: { x: 50, y: 550 },
},
{
id: '7',
type: 'output',
data: { label: 'Output 7' },
position: { x: 250, y: 550 },
},
{
id: '8',
type: 'output',
data: { label: 'Output 8' },
position: { x: 525, y: 600 },
},
{
id: '9',
type: 'output',
data: { label: 'Output 9' },
position: { x: 675, y: 500 },
},
];
const initialEdges: Edge[] = [
{ id: 'e1-2', source: '1', target: '2', label: 'bezier edge (default)', className: 'normal-edge' },
{ id: 'e2-2a', source: '2', target: '2a', type: 'smoothstep', label: 'smoothstep edge' },
{
id: 'e1-2',
source: '1',
target: '2',
label: 'bezier edge (default)',
className: 'normal-edge',
},
{
id: 'e2-2a',
source: '2',
target: '2a',
type: 'smoothstep',
label: 'smoothstep edge',
},
{
id: 'e2a-2b',
source: '2a',
@@ -51,9 +87,29 @@ const initialEdges: Edge[] = [
label: 'simple bezier edge',
},
{ id: 'e2-3', source: '2', target: '3', type: 'step', label: 'step edge' },
{ id: 'e3-4', source: '3', target: '4', type: 'straight', label: 'straight edge' },
{ id: 'e3-3a', source: '3', target: '3a', type: 'straight', label: 'label only edge', style: { stroke: 'none' } },
{ id: 'e3-5', source: '4', target: '5', animated: true, label: 'animated styled edge', style: { stroke: 'red' } },
{
id: 'e3-4',
source: '3',
target: '4',
type: 'straight',
label: 'straight edge',
},
{
id: 'e3-3a',
source: '3',
target: '3a',
type: 'straight',
label: 'label only edge',
style: { stroke: 'none' },
},
{
id: 'e3-5',
source: '4',
target: '5',
animated: true,
label: 'animated styled edge',
style: { stroke: 'red' },
},
{
id: 'e5-7',
source: '5',
@@ -121,7 +177,7 @@ const edgeTypes: EdgeTypes = {
const EdgesFlow = () => {
const [nodes, , onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = (params: Connection | Edge) => setEdges((eds) => addEdge(params, eds));
const onConnect = useCallback((params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
return (
<ReactFlow
@@ -132,7 +188,6 @@ const EdgesFlow = () => {
onNodeClick={onNodeClick}
onConnect={onConnect}
onNodeDragStop={onNodeDragStop}
onInit={onInit}
snapToGrid={true}
edgeTypes={edgeTypes}
onEdgeClick={onEdgeClick}

View File

@@ -1,35 +1,43 @@
import { MouseEvent, CSSProperties } from 'react';
import React, { MouseEvent, CSSProperties, useCallback } from 'react';
import ReactFlow, {
addEdge,
Controls,
Background,
Node,
BackgroundVariant,
Controls,
addEdge,
Node,
Connection,
Edge,
useNodesState,
useEdgesState,
ReactFlowInstance,
} from 'react-flow-renderer';
} from 'reactflow';
const onInit = (reactFlowInstance: ReactFlowInstance) => console.log('flow loaded:', reactFlowInstance);
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
const buttonStyle: CSSProperties = { position: 'absolute', left: 10, top: 10, zIndex: 4 };
const buttonStyle: CSSProperties = {
position: 'absolute',
left: 10,
top: 10,
zIndex: 4,
};
const EmptyFlow = () => {
const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const onConnect = (params: Connection | Edge) => setEdges((els) => addEdge(params, els));
const onConnect = useCallback((params: Connection | Edge) => setEdges((els) => addEdge(params, els)), [setEdges]);
const addRandomNode = () => {
const nodeId = (nodes.length + 1).toString();
const newNode: Node = {
id: nodeId,
data: { label: `Node: ${nodeId}` },
position: { x: Math.random() * window.innerWidth, y: Math.random() * window.innerHeight },
position: {
x: Math.random() * window.innerWidth,
y: Math.random() * window.innerHeight,
},
};
setNodes((nds) => nds.concat(newNode));
};

View File

@@ -1,5 +1,5 @@
import { FC } from 'react';
import { getBezierPath, ConnectionLineComponentProps, Node } from 'react-flow-renderer';
import React, { FC } from 'react';
import { getBezierPath, ConnectionLineComponentProps, Node } from 'reactflow';
import { getEdgeParams } from './utils';

View File

@@ -1,5 +1,5 @@
import { FC, useMemo, CSSProperties } from 'react';
import { EdgeProps, useStore, getBezierPath, ReactFlowState } from 'react-flow-renderer';
import React, { FC, useMemo, CSSProperties } from 'react';
import { EdgeProps, useStore, getBezierPath, ReactFlowState } from 'reactflow';
import { getEdgeParams } from './utils';

View File

@@ -1,25 +1,25 @@
import { useCallback } from 'react';
import React, { useCallback } from 'react';
import ReactFlow, {
addEdge,
Background,
addEdge,
ReactFlowInstance,
EdgeTypes,
Connection,
useNodesState,
useEdgesState,
} from 'react-flow-renderer';
} from 'reactflow';
import './style.css';
import styles from './style.module.css';
import FloatingEdge from './FloatingEdge';
import FloatingConnectionLine from './FloatingConnectionLine';
import FloatingEdge from './FloatingEdge';
import { createElements } from './utils';
const onInit = (reactFlowInstance: ReactFlowInstance) => reactFlowInstance.fitView();
const { nodes: initialNodes, edges: initialEdges } = createElements();
const { nodes: initialNodes, edges: initialEdges } =
typeof window !== 'undefined' ? createElements() : { nodes: [], edges: [] };
const edgeTypes: EdgeTypes = {
floating: FloatingEdge,
};
@@ -33,7 +33,7 @@ const FloatingEdges = () => {
}, []);
return (
<div className="floatingedges">
<div className={styles.floatingedges}>
<ReactFlow
nodes={nodes}
edges={edges}

View File

@@ -4,6 +4,6 @@
height: 100%;
}
.floatingedges .react-flow__handle {
.floatingedges :global .react-flow__handle {
opacity: 0;
}

View File

@@ -1,4 +1,4 @@
import { Position, XYPosition, Node, Edge } from 'react-flow-renderer';
import { Position, XYPosition, Node, Edge } from 'reactflow';
// this helper function returns the intersection point
// of the line between the center of the intersectionNode and the target node

View File

@@ -1,22 +1,33 @@
import { useState, useCallback } from 'react';
import React, { useState, useEffect, useCallback } from 'react';
import { useEffect } from 'react';
import ReactFlow, {
addEdge,
MiniMap,
Controls,
Connection,
Edge,
Node,
useNodesState,
useEdgesState,
} from 'react-flow-renderer';
import ReactFlow, { addEdge, Connection, Edge, Node, useNodesState, useEdgesState, MiniMap, Controls } from 'reactflow';
const initialNodes: Node[] = [
{ id: '1', type: 'input', hidden: true, data: { label: 'Node 1' }, position: { x: 250, y: 5 } },
{ id: '2', hidden: true, data: { label: 'Node 2' }, position: { x: 100, y: 100 } },
{ id: '3', hidden: true, data: { label: 'Node 3' }, position: { x: 400, y: 100 } },
{ id: '4', hidden: true, data: { label: 'Node 4' }, position: { x: 400, y: 200 } },
{
id: '1',
type: 'input',
hidden: true,
data: { label: 'Node 1' },
position: { x: 250, y: 5 },
},
{
id: '2',
hidden: true,
data: { label: 'Node 2' },
position: { x: 100, y: 100 },
},
{
id: '3',
hidden: true,
data: { label: 'Node 3' },
position: { x: 400, y: 100 },
},
{
id: '4',
hidden: true,
data: { label: 'Node 4' },
position: { x: 400, y: 200 },
},
];
const initialEdges: Edge[] = [
@@ -49,8 +60,6 @@ const HiddenFlow = () => {
setEdges(setHidden(isHidden));
}, [isHidden, setEdges, setNodes]);
console.log(nodes);
return (
<ReactFlow
nodes={nodes}

View File

@@ -1,8 +1,6 @@
import { useState, MouseEvent as ReactMouseEvent, WheelEvent } from 'react';
import React, { useState, MouseEvent as ReactMouseEvent, WheelEvent } from 'react';
import ReactFlow, {
addEdge,
MiniMap,
Controls,
Node,
Connection,
Edge,
@@ -10,10 +8,17 @@ import ReactFlow, {
Viewport,
useNodesState,
useEdgesState,
} from 'react-flow-renderer';
Controls,
MiniMap,
} from 'reactflow';
const initialNodes: Node[] = [
{ id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 } },
{
id: '1',
type: 'input',
data: { label: 'Node 1' },
position: { x: 250, y: 5 },
},
{ id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 } },
{ id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 } },
{ id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 } },

View File

@@ -1,8 +1,8 @@
import { useCallback } from 'react';
import React, { useCallback } from 'react';
import ReactFlow, {
Controls,
ReactFlowProvider,
addEdge,
Controls,
Connection,
CoordinateExtent,
Position,
@@ -10,12 +10,13 @@ import ReactFlow, {
useEdgesState,
MarkerType,
EdgeMarker,
} from 'react-flow-renderer';
} from 'reactflow';
import dagre from 'dagre';
import initialItems from './initial-elements';
import './layouting.css';
import styles from './layouting.module.css';
const dagreGraph = new dagre.graphlib.Graph();
dagreGraph.setDefaultEdgeLabel(() => ({}));
@@ -56,7 +57,10 @@ const LayoutFlow = () => {
node.sourcePosition = isHorizontal ? Position.Right : Position.Bottom;
// we need to pass a slightly different position in order to notify react flow about the change
// @TODO how can we change the position handling so that we dont need this hack?
node.position = { x: nodeWithPosition.x + Math.random() / 1000, y: nodeWithPosition.y };
node.position = {
x: nodeWithPosition.x + Math.random() / 1000,
y: nodeWithPosition.y,
};
return node;
});
@@ -80,7 +84,7 @@ const LayoutFlow = () => {
};
return (
<div className="layoutflow">
<div className={styles.layoutflow}>
<ReactFlowProvider>
<ReactFlow
nodes={nodes}
@@ -93,7 +97,7 @@ const LayoutFlow = () => {
>
<Controls />
</ReactFlow>
<div className="controls">
<div className={styles.controls}>
<button onClick={() => onLayout('TB')} style={{ marginRight: 10 }}>
vertical layout
</button>

View File

@@ -0,0 +1,135 @@
import { Node, Edge, XYPosition, MarkerType } from 'reactflow';
const position: XYPosition = { x: 0, y: 0 };
const nodes: Node[] = [
{
id: '1',
type: 'input',
data: { label: 'input' },
position,
},
{
id: '2',
data: { label: 'node 2' },
position,
},
{
id: '2a',
data: { label: 'node 2a' },
position,
},
{
id: '2b',
data: { label: 'node 2b' },
position,
},
{
id: '2c',
data: { label: 'node 2c' },
position,
},
{
id: '2d',
data: { label: 'node 2d' },
position,
},
{
id: '3',
data: { label: 'node 3' },
position,
},
{
id: '4',
data: { label: 'node 4' },
position,
},
{
id: '5',
data: { label: 'node 5' },
position,
},
{
id: '6',
type: 'output',
data: { label: 'output' },
position,
},
{
id: '7',
type: 'output',
data: { label: 'output' },
position: { x: 400, y: 450 },
},
];
const edges: Edge[] = [
{
id: 'e12',
source: '1',
target: '2',
type: 'smoothstep',
markerEnd: { type: MarkerType.Arrow },
},
{
id: 'e13',
source: '1',
target: '3',
type: 'smoothstep',
markerEnd: { type: MarkerType.Arrow },
},
{
id: 'e22a',
source: '2',
target: '2a',
type: 'smoothstep',
markerEnd: { type: MarkerType.Arrow },
},
{
id: 'e22b',
source: '2',
target: '2b',
type: 'smoothstep',
markerEnd: { type: MarkerType.Arrow },
},
{
id: 'e22c',
source: '2',
target: '2c',
type: 'smoothstep',
markerEnd: { type: MarkerType.Arrow },
},
{
id: 'e2c2d',
source: '2c',
target: '2d',
type: 'smoothstep',
markerEnd: { type: MarkerType.Arrow },
},
{
id: 'e45',
source: '4',
target: '5',
type: 'smoothstep',
markerEnd: { type: MarkerType.Arrow },
},
{
id: 'e56',
source: '5',
target: '6',
type: 'smoothstep',
markerEnd: { type: MarkerType.Arrow },
},
{
id: 'e57',
source: '5',
target: '7',
type: 'smoothstep',
markerEnd: { type: MarkerType.Arrow },
},
];
const nodesAndEdges = { nodes, edges };
export default nodesAndEdges;

View File

@@ -3,7 +3,7 @@
position: relative;
}
.layoutflow .controls {
.controls {
position: absolute;
right: 10px;
top: 10px;

View File

@@ -1,8 +1,8 @@
import { FC } from 'react';
import React, { FC } from 'react';
import ReactFlow, {
addEdge,
Background,
addEdge,
Node,
Edge,
Connection,
@@ -10,19 +10,47 @@ import ReactFlow, {
useNodesState,
useEdgesState,
MarkerType,
} from 'react-flow-renderer';
MiniMap,
} from 'reactflow';
import './multiflows.css';
import styles from './multiflows.module.css';
const initialNodes: Node[] = [
{ id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 }, className: 'light' },
{ id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 }, className: 'light' },
{ id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 }, className: 'light' },
{ id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 }, className: 'light' },
{
id: '1',
type: 'input',
data: { label: 'Node 1' },
position: { x: 250, y: 5 },
className: 'light',
},
{
id: '2',
data: { label: 'Node 2' },
position: { x: 100, y: 100 },
className: 'light',
},
{
id: '3',
data: { label: 'Node 3' },
position: { x: 400, y: 100 },
className: 'light',
},
{
id: '4',
data: { label: 'Node 4' },
position: { x: 400, y: 200 },
className: 'light',
},
];
const initialEdges: Edge[] = [
{ id: 'e1-2', source: '1', target: '2', animated: true, markerEnd: { type: MarkerType.Arrow } },
{
id: 'e1-2',
source: '1',
target: '2',
animated: true,
markerEnd: { type: MarkerType.Arrow },
},
{ id: 'e1-3', source: '1', target: '3' },
];
@@ -43,13 +71,14 @@ const Flow: FC<{ id: string }> = ({ id }) => {
id={id}
>
<Background />
<MiniMap />
</ReactFlow>
</ReactFlowProvider>
);
};
const MultiFlows: FC = () => (
<div className="react-flow__example-multiflows">
<div className={styles.multiflows}>
<Flow id="flow-a" />
<Flow id="flow-b" />
</div>

View File

@@ -0,0 +1,13 @@
.multiflows {
display: flex;
height: 100%;
}
.multiflows :global .react-flow {
width: 100%;
height: 100%;
}
.multiflows :global .react-flow:first-child {
border-right: 2px solid #333;
}

View File

@@ -1,24 +1,29 @@
import { useState, MouseEvent, useCallback } from 'react';
import React, { useState, MouseEvent, useCallback } from 'react';
import ReactFlow, {
addEdge,
Controls,
MiniMap,
Background,
addEdge,
useNodesState,
useEdgesState,
MiniMap,
Controls,
Node,
Edge,
ReactFlowInstance,
Connection,
} from 'react-flow-renderer';
} from 'reactflow';
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
const onEdgeClick = (_: MouseEvent, edge: Edge) => console.log('click', edge);
const initialNodes: Node[] = [
{ id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 }, className: 'light' },
{
id: '1',
type: 'input',
data: { label: 'Node 1' },
position: { x: 250, y: 5 },
className: 'light',
},
{
id: '2',
data: { label: 'Node 2' },
@@ -32,7 +37,12 @@ const initialNodes: Node[] = [
position: { x: 10, y: 50 },
parentNode: '2',
},
{ id: '3', data: { label: 'Node 3' }, position: { x: 320, y: 100 }, className: 'light' },
{
id: '3',
data: { label: 'Node 3' },
position: { x: 320, y: 100 },
className: 'light',
},
{
id: '4',
data: { label: 'Node 4' },
@@ -53,7 +63,11 @@ const initialNodes: Node[] = [
data: { label: 'Node 4b' },
position: { x: 15, y: 120 },
className: 'light',
style: { backgroundColor: 'rgba(255, 0, 255, 0.7)', height: 150, width: 270 },
style: {
backgroundColor: 'rgba(255, 0, 255, 0.7)',
height: 150,
width: 270,
},
parentNode: '4',
},
{
@@ -88,9 +102,12 @@ const NestedFlow = () => {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = useCallback((connection: Connection) => {
setEdges((eds) => addEdge(connection, eds));
}, []);
const onConnect = useCallback(
(connection: Connection) => {
setEdges((eds) => addEdge(connection, eds));
},
[setEdges]
);
const onInit = useCallback((reactFlowInstance: ReactFlowInstance) => setRfInstance(reactFlowInstance), []);
const updatePos = () => {
@@ -139,7 +156,6 @@ const NestedFlow = () => {
onConnect={onConnect}
onNodeDragStop={onNodeDragStop}
className="react-flow-basic-example"
defaultZoom={1.5}
minZoom={0.2}
maxZoom={4}
onlyRenderVisibleElements={false}

View File

@@ -1,14 +1,6 @@
import { CSSProperties } from 'react';
import React, { CSSProperties, useCallback } from 'react';
import ReactFlow, {
addEdge,
Node,
Position,
Connection,
Edge,
useNodesState,
useEdgesState,
} from 'react-flow-renderer';
import ReactFlow, { addEdge, Node, Position, Connection, Edge, useNodesState, useEdgesState } from 'reactflow';
const initialNodes: Node[] = [
{
@@ -30,12 +22,17 @@ const initialNodes: Node[] = [
const initialEdges: Edge[] = [{ id: 'e1-2', source: '1', type: 'smoothstep', target: '2', animated: true }];
const buttonStyle: CSSProperties = { position: 'absolute', right: 10, top: 30, zIndex: 4 };
const buttonStyle: CSSProperties = {
position: 'absolute',
right: 10,
top: 30,
zIndex: 4,
};
const NodeTypeChangeFlow = () => {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = (params: Connection | Edge) => setEdges((eds) => addEdge(params, eds));
const onConnect = useCallback((params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
const changeType = () => {
setNodes((nds) =>
nds.map((node) => {

View File

@@ -1,5 +1,4 @@
import { useState, CSSProperties, FC } from 'react';
import React, { useState, CSSProperties, FC, useCallback } from 'react';
import ReactFlow, {
addEdge,
Node,
@@ -10,7 +9,7 @@ import ReactFlow, {
NodeTypes,
useNodesState,
useEdgesState,
} from 'react-flow-renderer';
} from 'reactflow';
const initialNodes: Node[] = [
{
@@ -30,9 +29,17 @@ const initialNodes: Node[] = [
},
];
const buttonStyle: CSSProperties = { position: 'absolute', right: 10, top: 30, zIndex: 4 };
const buttonStyle: CSSProperties = {
position: 'absolute',
right: 10,
top: 30,
zIndex: 4,
};
const nodeStyles: CSSProperties = { padding: '10px 15px', border: '1px solid #ddd' };
const nodeStyles: CSSProperties = {
padding: '10px 15px',
border: '1px solid #ddd',
};
const NodeA: FC<NodeProps> = () => {
return <div style={nodeStyles}>A</div>;
@@ -57,9 +64,9 @@ const nodeTypesObjects: NodeTypesObject = {
const NodeTypeChangeFlow = () => {
const [nodeTypesId, setNodeTypesId] = useState<string>('a');
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [nodes, , onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const onConnect = (params: Connection | Edge) => setEdges((eds) => addEdge(params, eds));
const onConnect = useCallback((params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
const changeType = () => setNodeTypesId((nt) => (nt === 'a' ? 'b' : 'a'));
return (

View File

@@ -1,9 +1,6 @@
import { MouseEvent as ReactMouseEvent, CSSProperties, useCallback } from 'react';
import React, { MouseEvent as ReactMouseEvent, CSSProperties, useCallback } from 'react';
import ReactFlow, {
addEdge,
MiniMap,
Controls,
Background,
Node,
Viewport,
SnapGrid,
@@ -13,7 +10,10 @@ import ReactFlow, {
useNodesState,
useEdgesState,
OnSelectionChangeParams,
} from 'react-flow-renderer';
Controls,
Background,
MiniMap,
} from 'reactflow';
const onNodeDragStart = (_: ReactMouseEvent, node: Node, nodes: Node[]) => console.log('drag start', node, nodes);
const onNodeDrag = (_: ReactMouseEvent, node: Node, nodes: Node[]) => console.log('drag', node, nodes);
@@ -45,6 +45,7 @@ const onEdgeMouseLeave = (_: ReactMouseEvent, edge: Edge) => console.log('edge m
const onEdgeDoubleClick = (_: ReactMouseEvent, edge: Edge) => console.log('edge double click', edge);
const onNodesDelete = (nodes: Node[]) => console.log('nodes delete', nodes);
const onEdgesDelete = (edges: Edge[]) => console.log('edges delete', edges);
const onPaneMouseMove = (e: ReactMouseEvent) => console.log('pane move', e.clientX, e.clientY);
const initialNodes: Node[] = [
{
@@ -80,7 +81,12 @@ const initialNodes: Node[] = [
),
},
position: { x: 400, y: 100 },
style: { background: '#D6D5E6', color: '#333', border: '1px solid #222138', width: 180 },
style: {
background: '#D6D5E6',
color: '#333',
border: '1px solid #222138',
width: 180,
},
},
{
id: '4',
@@ -113,21 +119,40 @@ const initialNodes: Node[] = [
data: {
label: (
<>
An <strong>output node</strong>
An <strong>output node (not deletable)</strong>
</>
),
},
position: { x: 100, y: 480 },
deletable: false,
},
{
id: '7',
type: 'output',
data: { label: 'Another output node' },
position: { x: 400, y: 450 },
},
{ id: '7', type: 'output', data: { label: 'Another output node' }, position: { x: 400, y: 450 } },
];
const initialEdges: Edge[] = [
{ id: 'e1-2', source: '1', target: '2', label: 'this is an edge label' },
{ id: 'e1-3', source: '1', target: '3' },
{ id: 'e3-4', source: '3', target: '4', animated: true, label: 'animated edge' },
{
id: 'e3-4',
source: '3',
target: '4',
animated: true,
label: 'animated edge',
},
{ id: 'e4-5', source: '4', target: '5', label: 'edge with arrow head' },
{ id: 'e5-6', source: '5', target: '6', type: 'smoothstep', label: 'smooth step edge' },
{
id: 'e5-6',
source: '5',
target: '6',
type: 'smoothstep',
deletable: false,
label: 'smooth step edge (not deletable)',
},
{
id: 'e5-7',
source: '5',
@@ -200,6 +225,7 @@ const OverviewFlow = () => {
maxZoom={Infinity}
onNodesDelete={onNodesDelete}
onEdgesDelete={onEdgesDelete}
onPaneMouseMove={onPaneMouseMove}
>
<MiniMap nodeStrokeColor={nodeStrokeColor} nodeColor={nodeColor} nodeBorderRadius={2} />
<Controls />

Some files were not shown because too many files have changed in this diff Show More