@@ -542,6 +542,7 @@ You can find all examples in the [example](example) folder or check out the live
|
||||
- [interaction](https://reactflow.dev/interaction)
|
||||
- [provider](https://reactflow.dev/provider)
|
||||
- [hidden](https://reactflow.dev/hidden)
|
||||
- [edge types](https://reactflow.dev/edge-types)
|
||||
|
||||
# Development
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
describe('Basic Flow Rendering', () => {
|
||||
it('renders a flow with three nodes', () => {
|
||||
before(() => {
|
||||
cy.visit('/basic');
|
||||
});
|
||||
|
||||
it('renders a flow with three nodes', () => {
|
||||
cy.get('.react-flow__renderer');
|
||||
cy.get('.react-flow-basic-example'); // check if className prop works
|
||||
cy.get('.react-flow__node').should('have.length', 4);
|
||||
@@ -115,16 +117,10 @@ describe('Basic Flow Rendering', () => {
|
||||
});
|
||||
|
||||
it('drags the pane', () => {
|
||||
// for d3 we have to pass the window to the event
|
||||
// https://github.com/cypress-io/cypress/issues/3441
|
||||
|
||||
const newPosition = {
|
||||
clientX: Cypress.config('viewportWidth') * 0.6,
|
||||
clientY: Cypress.config('viewportHeight') * 0.7,
|
||||
};
|
||||
|
||||
const styleBeforeDrag = Cypress.$('.react-flow__nodes').css('transform');
|
||||
|
||||
// for d3 we have to pass the window to the event
|
||||
// https://github.com/cypress-io/cypress/issues/3441
|
||||
cy.window().then((win) => {
|
||||
cy.get('.react-flow__zoompane')
|
||||
.trigger('mousedown', 'topLeft', { which: 1, view: win })
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
describe('Controls Testing', () => {
|
||||
before(() => {
|
||||
cy.visit('/');
|
||||
});
|
||||
|
||||
it('renders the control panel', () => {
|
||||
cy.get('.react-flow__controls');
|
||||
});
|
||||
|
||||
it('zooms in', () => {
|
||||
const styleBeforeZoom = Cypress.$('.react-flow__nodes').css('transform');
|
||||
|
||||
cy.get('.react-flow__controls-zoomin')
|
||||
.click()
|
||||
.then(() => {
|
||||
const styleAfterZoom = Cypress.$('.react-flow__nodes').css('transform');
|
||||
expect(styleBeforeZoom).to.not.equal(styleAfterZoom);
|
||||
});
|
||||
});
|
||||
|
||||
it('zooms out', () => {
|
||||
const styleBeforeZoom = Cypress.$('.react-flow__nodes').css('transform');
|
||||
|
||||
cy.get('.react-flow__controls-zoomout')
|
||||
.click()
|
||||
.then(() => {
|
||||
const styleAfterZoom = Cypress.$('.react-flow__nodes').css('transform');
|
||||
expect(styleBeforeZoom).to.not.equal(styleAfterZoom);
|
||||
});
|
||||
});
|
||||
|
||||
// view is already fitted so we drag the pane to un-fit it
|
||||
it('drags the pane', () => {
|
||||
const styleBeforeDrag = Cypress.$('.react-flow__nodes').css('transform');
|
||||
|
||||
// for d3 we have to pass the window to the event
|
||||
// https://github.com/cypress-io/cypress/issues/3441
|
||||
cy.window().then((win) => {
|
||||
cy.get('.react-flow__zoompane')
|
||||
.trigger('mousedown', 'topLeft', { which: 1, view: win })
|
||||
.trigger('mousemove', 'bottomLeft')
|
||||
.trigger('mouseup', { force: true, view: win })
|
||||
.then(() => {
|
||||
const styleAfterDrag = Cypress.$('.react-flow__nodes').css('transform');
|
||||
expect(styleBeforeDrag).to.not.equal(styleAfterDrag);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('fits view', () => {
|
||||
const styleBeforeZoom = Cypress.$('.react-flow__nodes').css('transform');
|
||||
|
||||
cy.get('.react-flow__controls-fitview')
|
||||
.click()
|
||||
.then(() => {
|
||||
const styleAfterZoom = Cypress.$('.react-flow__nodes').css('transform');
|
||||
expect(styleBeforeZoom).to.not.equal(styleAfterZoom);
|
||||
});
|
||||
});
|
||||
|
||||
it('uses interactive control - not interactive', () => {
|
||||
cy.get('.react-flow__node:first').click().should('have.class', 'selected');
|
||||
cy.get('.react-flow__renderer').click('bottomRight');
|
||||
cy.get('.react-flow__node:first').should('not.have.class', 'selected');
|
||||
|
||||
cy.get('.react-flow__controls-interactive')
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.get('.react-flow__node:first').should('not.have.class', 'selected');
|
||||
});
|
||||
});
|
||||
|
||||
it('uses interactive control - interactive', () => {
|
||||
cy.get('.react-flow__controls-interactive')
|
||||
.click()
|
||||
.then(() => {
|
||||
cy.get('.react-flow__node:first').click().should('have.class', 'selected');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,10 +0,0 @@
|
||||
describe('Custom Node Flow Rendering', () => {
|
||||
it('renders a flow', () => {
|
||||
cy.visit('/custom-node');
|
||||
|
||||
cy.get('.react-flow__renderer');
|
||||
|
||||
cy.get('.react-flow__node').should('have.length', 4);
|
||||
cy.get('.react-flow__edge').should('have.length', 3);
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,9 @@
|
||||
describe('Empty Flow Rendering', () => {
|
||||
it('renders an empty flow', () => {
|
||||
before(() => {
|
||||
cy.visit('/empty');
|
||||
});
|
||||
|
||||
it('renders an empty flow', () => {
|
||||
cy.get('.react-flow__renderer');
|
||||
cy.get('.react-flow__node').should('not.exist');
|
||||
cy.get('.react-flow__edge').should('not.exist');
|
||||
@@ -16,26 +18,6 @@ describe('Empty Flow Rendering', () => {
|
||||
.trigger('mouseup', 'bottomRight', { force: true });
|
||||
});
|
||||
|
||||
it('renders a control panel', () => {
|
||||
cy.get('.react-flow__controls');
|
||||
});
|
||||
|
||||
it('uses zoom in control', () => {
|
||||
cy.get('.react-flow__controls-zoomin').click();
|
||||
});
|
||||
|
||||
it('uses zoom out control', () => {
|
||||
cy.get('.react-flow__controls-zoomout').click();
|
||||
});
|
||||
|
||||
it('uses fit view control', () => {
|
||||
cy.get('.react-flow__controls-fitview').click();
|
||||
});
|
||||
|
||||
it('uses lock view control', () => {
|
||||
cy.get('.react-flow__controls-interactive');
|
||||
});
|
||||
|
||||
it('renders an empty mini map', () => {
|
||||
cy.get('.react-flow__minimap');
|
||||
cy.get('.react-flow__minimap-node').should('not.exist');
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import { isNode, isEdge, getOutgoers, removeElements, addEdge } from '../../../src/utils/graph.ts';
|
||||
|
||||
const nodes = [
|
||||
{ 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 = [
|
||||
{ id: 'e1-2', source: '1', target: '2', animated: true },
|
||||
{ id: 'e1-3', source: '1', target: '3' },
|
||||
];
|
||||
|
||||
const elements = [...nodes, ...edges];
|
||||
|
||||
describe('Graph Utils Testing', () => {
|
||||
it('tests isNode function', () => {
|
||||
expect(isNode(nodes[0])).to.be.true;
|
||||
expect(isNode(edges[0])).to.be.false;
|
||||
});
|
||||
|
||||
it('tests isEdge function', () => {
|
||||
expect(isEdge(edges[0])).to.be.true;
|
||||
expect(isEdge(nodes[0])).to.be.false;
|
||||
});
|
||||
|
||||
it('tests getOutgoers function', () => {
|
||||
const outgoers = getOutgoers(nodes[0], elements);
|
||||
expect(outgoers.length).to.be.equal(2);
|
||||
|
||||
const noOutgoers = getOutgoers(nodes[1], elements);
|
||||
expect(noOutgoers.length).to.be.equal(0);
|
||||
});
|
||||
|
||||
describe('tests addEdge function', () => {
|
||||
it('adds edge', () => {
|
||||
const newEdge = { source: '2', target: '3' };
|
||||
const nextElements = addEdge(newEdge, elements);
|
||||
|
||||
expect(nextElements.length).to.be.equal(elements.length + 1);
|
||||
});
|
||||
|
||||
it('tries to add invalid edge', () => {
|
||||
const newEdge = { nosource: '1', notarget: '3' };
|
||||
|
||||
try {
|
||||
addEdge(newEdge, elements);
|
||||
} catch (e) {
|
||||
console.log(e.message);
|
||||
|
||||
expect(e.message).to.be.equal("Can't create edge. An edge needs a source and a target.");
|
||||
}
|
||||
});
|
||||
|
||||
it('tries to add edge with id that does not exist', () => {
|
||||
const notExistingId = 'does-not-exist';
|
||||
const newEdge = { source: notExistingId, target: '3' };
|
||||
|
||||
try {
|
||||
addEdge(newEdge, elements);
|
||||
} catch (e) {
|
||||
console.log(e.message);
|
||||
expect(e.message).to.be.equal(`Can't create edge. Node with id=${notExistingId} does not exist.`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('tests removeElements function', () => {
|
||||
it('removes a node', () => {
|
||||
const nextElements = removeElements([nodes[0]], elements);
|
||||
|
||||
const nextNodes = nextElements.filter((e) => isNode(e));
|
||||
const nextEdges = nextElements.filter((e) => isEdge(e));
|
||||
|
||||
expect(nextNodes.length).to.be.equal(nodes.length - 1);
|
||||
expect(nextEdges.length).to.be.equal(edges.length - 2);
|
||||
});
|
||||
|
||||
it('removes multiple nodes', () => {
|
||||
const elementsToRemove = [nodes[0], nodes[1]];
|
||||
const nextElements = removeElements(elementsToRemove, elements);
|
||||
const nextNodes = nextElements.filter((e) => isNode(e));
|
||||
const nextEdges = nextElements.filter((e) => isEdge(e));
|
||||
|
||||
expect(nextNodes.length).to.be.equal(nodes.length - 2);
|
||||
expect(nextEdges.length).to.be.equal(0);
|
||||
});
|
||||
|
||||
it('removes no node', () => {
|
||||
const nextElementsNoRemove = removeElements([], elements);
|
||||
expect(nextElementsNoRemove.length).to.be.equal(elements.length);
|
||||
});
|
||||
|
||||
it('tries to removes node that does not exist', () => {
|
||||
const nextElementsNoRemove = removeElements([{ id: 'id-that-does-not-exist' }], elements);
|
||||
expect(nextElementsNoRemove.length).to.be.equal(elements.length);
|
||||
});
|
||||
|
||||
it('removes an edge', () => {
|
||||
const nextElements = removeElements([edges[0]], elements);
|
||||
|
||||
const nextNodes = nextElements.filter((e) => isNode(e));
|
||||
const nextEdges = nextElements.filter((e) => isEdge(e));
|
||||
|
||||
expect(nextNodes.length).to.be.equal(nodes.length);
|
||||
expect(nextEdges.length).to.be.equal(edges.length - 1);
|
||||
});
|
||||
|
||||
it('removes multiple edges', () => {
|
||||
const nextElements = removeElements([edges[0], edges[1]], elements);
|
||||
|
||||
const nextNodes = nextElements.filter((e) => isNode(e));
|
||||
const nextEdges = nextElements.filter((e) => isEdge(e));
|
||||
|
||||
expect(nextNodes.length).to.be.equal(nodes.length);
|
||||
expect(nextEdges.length).to.be.equal(edges.length - 2);
|
||||
});
|
||||
|
||||
it('removes node and edge', () => {
|
||||
const nextElements = removeElements([nodes[0], edges[0]], elements);
|
||||
|
||||
const nextNodes = nextElements.filter((e) => isNode(e));
|
||||
const nextEdges = nextElements.filter((e) => isEdge(e));
|
||||
|
||||
expect(nextNodes.length).to.be.equal(nodes.length - 1);
|
||||
expect(nextEdges.length).to.be.equal(edges.length - 2);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,9 @@
|
||||
describe('Hidden Flow Rendering', () => {
|
||||
it('renders the initial flow', () => {
|
||||
before(() => {
|
||||
cy.visit('/hidden');
|
||||
});
|
||||
|
||||
it('renders initial flow', () => {
|
||||
cy.get('.react-flow__renderer');
|
||||
cy.get('.react-flow__node').should('have.length', 4);
|
||||
cy.get('.react-flow__edge').should('have.length', 3);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
describe('Interaction Flow Rendering', () => {
|
||||
it('renders initial flow', () => {
|
||||
before(() => {
|
||||
cy.visit('/interaction');
|
||||
});
|
||||
|
||||
it('renders initial flow', () => {
|
||||
cy.get('.react-flow__renderer');
|
||||
cy.get('.react-flow__node').should('have.length', 4);
|
||||
cy.get('.react-flow__edge').should('have.length', 2);
|
||||
@@ -23,6 +25,38 @@ describe('Interaction Flow Rendering', () => {
|
||||
cy.get('body').type('{shift}', { release: true });
|
||||
});
|
||||
|
||||
it('tries to connect to nodes', () => {
|
||||
cy.get('.react-flow__node')
|
||||
.contains('Node 3')
|
||||
.find('.react-flow__handle.source')
|
||||
.then(($el) => {
|
||||
const pointerEvents = $el.css('pointer-events');
|
||||
expect(pointerEvents).to.equal('none');
|
||||
});
|
||||
});
|
||||
|
||||
it('tries to zoom by scroll', () => {
|
||||
const styleBeforeZoom = Cypress.$('.react-flow__nodes').css('transform');
|
||||
|
||||
cy.get('.react-flow__zoompane')
|
||||
.dblclick()
|
||||
.then(() => {
|
||||
const styleAfterZoom = Cypress.$('.react-flow__nodes').css('transform');
|
||||
expect(styleBeforeZoom).to.equal(styleAfterZoom);
|
||||
});
|
||||
});
|
||||
|
||||
it('tries to zoom by double click', () => {
|
||||
const styleBeforeZoom = Cypress.$('.react-flow__nodes').css('transform');
|
||||
|
||||
cy.get('.react-flow__zoompane')
|
||||
.trigger('wheel', 'topLeft', { deltaY: -200 })
|
||||
.then(() => {
|
||||
const styleAfterZoom = Cypress.$('.react-flow__nodes').css('transform');
|
||||
expect(styleBeforeZoom).to.equal(styleAfterZoom);
|
||||
});
|
||||
});
|
||||
|
||||
it('toggles draggable mode', () => {
|
||||
cy.get('.react-flow__draggable').click();
|
||||
});
|
||||
@@ -47,4 +81,53 @@ describe('Interaction Flow Rendering', () => {
|
||||
it('selects an edge by click', () => {
|
||||
cy.get('.react-flow__edge:first').click().should('have.class', 'selected');
|
||||
});
|
||||
|
||||
it('toggles connectable mode', () => {
|
||||
cy.get('.react-flow__connectable').click();
|
||||
});
|
||||
|
||||
it('connects two nodes', () => {
|
||||
cy.get('.react-flow__node')
|
||||
.contains('Node 3')
|
||||
.find('.react-flow__handle.source')
|
||||
.trigger('mousedown', { which: 1 });
|
||||
|
||||
cy.get('.react-flow__node')
|
||||
.contains('Node 4')
|
||||
.find('.react-flow__handle.target')
|
||||
.trigger('mousemove')
|
||||
.trigger('mouseup', { force: true });
|
||||
|
||||
cy.get('.react-flow__edge').should('have.length', 3);
|
||||
});
|
||||
|
||||
it('toggles zoom on scroll', () => {
|
||||
cy.get('.react-flow__zoomonscroll').click();
|
||||
});
|
||||
|
||||
it('zooms by scroll', () => {
|
||||
const styleBeforeZoom = Cypress.$('.react-flow__nodes').css('transform');
|
||||
|
||||
cy.get('.react-flow__zoompane')
|
||||
.trigger('wheel', 'topLeft', { deltaY: 200 })
|
||||
.then(() => {
|
||||
const styleAfterZoom = Cypress.$('.react-flow__nodes').css('transform');
|
||||
expect(styleBeforeZoom).not.to.equal(styleAfterZoom);
|
||||
});
|
||||
});
|
||||
|
||||
it('toggles zoom on double click', () => {
|
||||
cy.get('.react-flow__zoomondbl').click();
|
||||
});
|
||||
|
||||
it('zooms by double click', () => {
|
||||
const styleBeforeZoom = Cypress.$('.react-flow__nodes').css('transform');
|
||||
|
||||
cy.get('.react-flow__zoompane')
|
||||
.dblclick()
|
||||
.then(() => {
|
||||
const styleAfterZoom = Cypress.$('.react-flow__nodes').css('transform');
|
||||
expect(styleBeforeZoom).not.to.equal(styleAfterZoom);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
describe('Minimap Testing', () => {
|
||||
before(() => {
|
||||
cy.visit('/');
|
||||
});
|
||||
|
||||
it('renders the mini map', () => {
|
||||
cy.get('.react-flow__minimap');
|
||||
cy.get('.react-flow__minimap-mask');
|
||||
});
|
||||
|
||||
it('has same number of nodes as the pane', () => {
|
||||
const paneNodes = Cypress.$('.react-flow__node').length;
|
||||
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 maskPathBeforeZoom = Cypress.$('.react-flow__minimap-mask').attr('d');
|
||||
|
||||
cy.get('.react-flow__zoompane')
|
||||
.trigger('wheel', 'topLeft', { deltaY: -200 })
|
||||
.then(() => {
|
||||
const viewBoxAfterZoom = Cypress.$('.react-flow__minimap').attr('viewBox');
|
||||
const maskPathAfterZoom = Cypress.$('.react-flow__minimap-mask').attr('d');
|
||||
|
||||
expect(viewBoxBeforeZoom).to.not.equal(viewBoxAfterZoom);
|
||||
expect(maskPathBeforeZoom).to.not.equal(maskPathAfterZoom);
|
||||
});
|
||||
});
|
||||
|
||||
it('changes node position', () => {
|
||||
const xPosBeforeDrag = Cypress.$('.react-flow__minimap-node:first').attr('x');
|
||||
const yPosBeforeDrag = Cypress.$('.react-flow__minimap-node:first').attr('y');
|
||||
const maskPathBeforeDrag = Cypress.$('.react-flow__minimap-mask').attr('d');
|
||||
|
||||
cy.drag('.react-flow__node:first', { x: 500, y: 25 }).then(($el) => {
|
||||
const xPosAfterDrag = Cypress.$('.react-flow__minimap-node:first').attr('x');
|
||||
const yPosAfterDrag = Cypress.$('.react-flow__minimap-node:first').attr('y');
|
||||
const maskPathAfterDrag = Cypress.$('.react-flow__minimap-mask').attr('d');
|
||||
|
||||
expect(xPosBeforeDrag).to.not.equal(xPosAfterDrag);
|
||||
expect(yPosBeforeDrag).to.not.equal(yPosAfterDrag);
|
||||
expect(maskPathBeforeDrag).to.not.equal(maskPathAfterDrag);
|
||||
});
|
||||
});
|
||||
|
||||
it('changes node positions via pane drag', () => {
|
||||
const viewBoxBeforeDrag = Cypress.$('.react-flow__minimap').attr('viewBox');
|
||||
const maskPathBeforeDrag = Cypress.$('.react-flow__minimap-mask').attr('d');
|
||||
|
||||
// for d3 we have to pass the window to the event
|
||||
// https://github.com/cypress-io/cypress/issues/3441
|
||||
cy.window().then((win) => {
|
||||
cy.get('.react-flow__zoompane')
|
||||
.trigger('mousedown', 'topLeft', { which: 1, view: win })
|
||||
.trigger('mousemove', 'bottomLeft')
|
||||
.trigger('mouseup', { force: true, view: win })
|
||||
.then(() => {
|
||||
const viewBoxAfterDrag = Cypress.$('.react-flow__minimap').attr('viewBox');
|
||||
const maskPathAfterDrag = Cypress.$('.react-flow__minimap-mask').attr('d');
|
||||
|
||||
expect(viewBoxBeforeDrag).to.not.equal(viewBoxAfterDrag);
|
||||
expect(maskPathBeforeDrag).to.not.equal(maskPathAfterDrag);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
describe('Overview Flow Rendering', () => {
|
||||
it('renders a flow', () => {
|
||||
cy.visit('/');
|
||||
|
||||
cy.get('.react-flow__renderer');
|
||||
|
||||
cy.get('.react-flow__node').should('have.length', 7);
|
||||
cy.get('.react-flow__edge').should('have.length', 6);
|
||||
});
|
||||
|
||||
it('renders a grid', () => {
|
||||
cy.get('.react-flow__background');
|
||||
|
||||
const gridStroke = Cypress.$('.react-flow__background path').attr('fill');
|
||||
expect(gridStroke).to.equal('#888');
|
||||
});
|
||||
});
|
||||
@@ -1,10 +0,0 @@
|
||||
describe('Stress Flow Rendering', () => {
|
||||
it('renders initial flow', () => {
|
||||
cy.visit('/stress');
|
||||
|
||||
cy.get('.react-flow__renderer');
|
||||
|
||||
cy.get('.react-flow__node').should('have.length', 100);
|
||||
cy.get('.react-flow__edge').should('have.length', 99);
|
||||
});
|
||||
});
|
||||
@@ -70,7 +70,7 @@ const CustomNodeFlow = () => {
|
||||
nodeTypes={{
|
||||
selectorNode: ColorSelectorNode,
|
||||
}}
|
||||
connectionLineStyle={{ stroke: '#ddd', strokeWidth: 2 }}
|
||||
connectionLineStyle={{ stroke: '#ddd' }}
|
||||
snapToGrid={true}
|
||||
snapGrid={[16, 16]}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Example for checking the different edge types and source and target positions
|
||||
*/
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import ReactFlow, { removeElements, addEdge, MiniMap, Controls, Background } from 'react-flow-renderer';
|
||||
import { getElements } from './utils';
|
||||
|
||||
const onLoad = (reactFlowInstance) => {
|
||||
reactFlowInstance.fitView();
|
||||
console.log(reactFlowInstance.getElements());
|
||||
};
|
||||
|
||||
const initialElements = getElements();
|
||||
|
||||
const EdgeTypesFlow = () => {
|
||||
const [elements, setElements] = useState(initialElements);
|
||||
const onElementsRemove = (elementsToRemove) => setElements((els) => removeElements(elementsToRemove, els));
|
||||
const onConnect = (params) => setElements((els) => addEdge(params, els));
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
elements={elements}
|
||||
onLoad={onLoad}
|
||||
onElementsRemove={onElementsRemove}
|
||||
onConnect={onConnect}
|
||||
minZoom={0.2}
|
||||
>
|
||||
<MiniMap />
|
||||
<Controls />
|
||||
<Background />
|
||||
</ReactFlow>
|
||||
);
|
||||
};
|
||||
|
||||
export default EdgeTypesFlow;
|
||||
@@ -0,0 +1,104 @@
|
||||
const nodeWidth = 80;
|
||||
const nodeGapWidth = nodeWidth * 2;
|
||||
const nodeStyle = { width: nodeWidth, fontSize: 11, color: 'white' };
|
||||
|
||||
const sourceTargetPositions = [
|
||||
{ source: 'bottom', target: 'top' },
|
||||
{ source: 'right', target: 'left' },
|
||||
];
|
||||
const nodeColors = [
|
||||
['#1e9e99', '#4cb3ac', '#6ec9c0', '#8ddfd4'],
|
||||
['#0f4c75', '#1b5d8b', '#276fa1', '#3282b8'],
|
||||
];
|
||||
const edgeTypes = ['default', 'step', 'smoothstep', 'straight'];
|
||||
const offsets = [
|
||||
{
|
||||
x: 0,
|
||||
y: -nodeGapWidth,
|
||||
},
|
||||
{
|
||||
x: nodeGapWidth,
|
||||
y: -nodeGapWidth,
|
||||
},
|
||||
{
|
||||
x: nodeGapWidth,
|
||||
y: 0,
|
||||
},
|
||||
{
|
||||
x: nodeGapWidth,
|
||||
y: nodeGapWidth,
|
||||
},
|
||||
{
|
||||
x: 0,
|
||||
y: nodeGapWidth,
|
||||
},
|
||||
{
|
||||
x: -nodeGapWidth,
|
||||
y: nodeGapWidth,
|
||||
},
|
||||
{
|
||||
x: -nodeGapWidth,
|
||||
y: 0,
|
||||
},
|
||||
{
|
||||
x: -nodeGapWidth,
|
||||
y: -nodeGapWidth,
|
||||
},
|
||||
];
|
||||
|
||||
let id = 0;
|
||||
const getNodeId = () => (id++).toString();
|
||||
|
||||
export function getElements() {
|
||||
const initialElements = [];
|
||||
|
||||
for (let sourceTargetIndex = 0; sourceTargetIndex < sourceTargetPositions.length; sourceTargetIndex++) {
|
||||
const currSourceTargetPos = sourceTargetPositions[sourceTargetIndex];
|
||||
|
||||
for (let edgeTypeIndex = 0; edgeTypeIndex < edgeTypes.length; edgeTypeIndex++) {
|
||||
const currEdgeType = edgeTypes[edgeTypeIndex];
|
||||
|
||||
for (let offsetIndex = 0; offsetIndex < offsets.length; offsetIndex++) {
|
||||
const currOffset = offsets[offsetIndex];
|
||||
|
||||
const style = { ...nodeStyle, background: nodeColors[sourceTargetIndex][edgeTypeIndex] };
|
||||
const sourcePosition = {
|
||||
x: offsetIndex * nodeWidth * 4,
|
||||
y: edgeTypeIndex * 300 + sourceTargetIndex * edgeTypes.length * 300,
|
||||
};
|
||||
const sourceId = getNodeId();
|
||||
const sourceData = { label: `Source ${sourceId}` };
|
||||
const sourceNode = {
|
||||
id: sourceId,
|
||||
style,
|
||||
data: sourceData,
|
||||
position: sourcePosition,
|
||||
sourcePosition: currSourceTargetPos.source,
|
||||
targetPosition: currSourceTargetPos.target,
|
||||
};
|
||||
|
||||
const targetId = getNodeId();
|
||||
const targetData = { label: `Target ${targetId}` };
|
||||
const targetPosition = {
|
||||
x: sourcePosition.x + currOffset.x,
|
||||
y: sourcePosition.y + currOffset.y,
|
||||
};
|
||||
const targetNode = {
|
||||
id: targetId,
|
||||
style,
|
||||
data: targetData,
|
||||
position: targetPosition,
|
||||
sourcePosition: currSourceTargetPos.source,
|
||||
targetPosition: currSourceTargetPos.target,
|
||||
};
|
||||
|
||||
initialElements.push(sourceNode);
|
||||
initialElements.push(targetNode);
|
||||
|
||||
initialElements.push({ id: `${sourceId}-${targetId}`, source: sourceId, target: targetId, type: currEdgeType });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return initialElements;
|
||||
}
|
||||
@@ -22,8 +22,8 @@ const InteractionFlow = () => {
|
||||
const [isSelectable, setIsSelectable] = useState(false);
|
||||
const [isDraggable, setIsDraggable] = useState(false);
|
||||
const [isConnectable, setIsConnectable] = useState(false);
|
||||
const [zoomOnScroll, setZoomOnScroll] = useState(true);
|
||||
const [zoomOnDoubleClick, setZoomOnDoubleClick] = useState(true);
|
||||
const [zoomOnScroll, setZoomOnScroll] = useState(false);
|
||||
const [zoomOnDoubleClick, setZoomOnDoubleClick] = useState(false);
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
|
||||
@@ -95,8 +95,10 @@ const initialElements = [
|
||||
source: '5',
|
||||
target: '7',
|
||||
type: 'step',
|
||||
style: { stroke: '#f6ab6c' },
|
||||
label: 'a step edge',
|
||||
labelStyle: { fill: 'red', fontWeight: 700 },
|
||||
animated: true,
|
||||
labelStyle: { fill: '#f6ab6c', fontWeight: 700 },
|
||||
},
|
||||
];
|
||||
|
||||
@@ -126,17 +128,18 @@ const OverviewFlow = () => {
|
||||
onSelectionChange={onSelectionChange}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
onLoad={onLoad}
|
||||
connectionLineStyle={{ stroke: '#ddd', strokeWidth: 2 }}
|
||||
connectionLineStyle={{ stroke: '#ddd' }}
|
||||
snapToGrid={true}
|
||||
snapGrid={[16, 16]}
|
||||
>
|
||||
<MiniMap
|
||||
nodeColor={(n) => {
|
||||
if (n.type === 'input') return 'blue';
|
||||
if (n.type === 'output') return 'green';
|
||||
if (n.type === 'default') return 'red';
|
||||
if (n.style?.background) return n.style.background;
|
||||
if (n.type === 'input') return '#9999ff';
|
||||
if (n.type === 'output') return '#79c9b7';
|
||||
if (n.type === 'default') return '#ff6060';
|
||||
|
||||
return '#FFCC00';
|
||||
return '#eee';
|
||||
}}
|
||||
/>
|
||||
<Controls />
|
||||
|
||||
@@ -13,6 +13,7 @@ import Validation from './Validation';
|
||||
import Horizontal from './Horizontal';
|
||||
import Provider from './Provider';
|
||||
import Hidden from './Hidden';
|
||||
import EdgeTypes from './EdgeTypes';
|
||||
|
||||
import './index.css';
|
||||
|
||||
@@ -69,6 +70,10 @@ const routes = [
|
||||
path: '/hidden',
|
||||
component: Hidden,
|
||||
},
|
||||
{
|
||||
path: '/edge-types',
|
||||
component: EdgeTypes,
|
||||
},
|
||||
];
|
||||
|
||||
const navLinks = routes.filter((route) => route.label);
|
||||
|
||||
+4
-2
@@ -16,10 +16,12 @@
|
||||
"start:examples": "npm run build && cd example && npm start",
|
||||
"dev:wait": "start-server-and-test start:examples http-get://localhost:3000",
|
||||
"build:example": "npm install && npm run build && cd example && npm install && npm run build",
|
||||
"cy:run": "cypress run",
|
||||
"cy:open": "cypress open",
|
||||
"cypress": "npm run dev:wait cy:open",
|
||||
"test": "npm run dev:wait cy:run",
|
||||
"test:chrome": "cypress run --browser chrome",
|
||||
"test:firefox": "cypress run --browser firefox",
|
||||
"test:all": "npm run test:chrome && npm run test:firefox",
|
||||
"test": "npm run dev:wait test:chrome",
|
||||
"release": "release-it"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -15,7 +15,7 @@ const MiniMapNode = ({ node, color, borderRadius }: MiniMapNodeProps) => {
|
||||
height,
|
||||
} = node.__rf;
|
||||
const { background, backgroundColor } = node.style || {};
|
||||
const fill = (background || backgroundColor || color) as string;
|
||||
const fill = (color || background || backgroundColor) as string;
|
||||
|
||||
return (
|
||||
<rect
|
||||
|
||||
@@ -2,7 +2,6 @@ import React, { useEffect, useState, CSSProperties } from 'react';
|
||||
import cc from 'classcat';
|
||||
|
||||
import { getBezierPath } from '../Edges/BezierEdge';
|
||||
import { getStepPath } from '../Edges/StepEdge';
|
||||
import { getSmoothStepPath } from '../Edges/SmoothStepEdge';
|
||||
import { ElementId, Node, Transform, HandleElement, Position, ConnectionLineType, HandleType } from '../../types';
|
||||
|
||||
@@ -80,12 +79,18 @@ export default ({
|
||||
targetPosition,
|
||||
});
|
||||
} else if (connectionLineType === ConnectionLineType.Step) {
|
||||
dAttr = getStepPath({
|
||||
dAttr = getSmoothStepPath({
|
||||
xOffset,
|
||||
yOffset,
|
||||
centerX,
|
||||
centerY,
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition: sourceHandle?.position,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition,
|
||||
borderRadius: 0,
|
||||
});
|
||||
} else if (connectionLineType === ConnectionLineType.SmoothStep) {
|
||||
dAttr = getSmoothStepPath({
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { memo } from 'react';
|
||||
|
||||
import EdgeText from './EdgeText';
|
||||
import { getMarkerEnd } from './utils';
|
||||
import { EdgeBezierProps, Position } from '../../types';
|
||||
import { EdgeSmoothStepProps, Position } from '../../types';
|
||||
|
||||
// These are some helper methods for drawing the round corners
|
||||
// The name indicates the direction of the path. "bottomLeftCorner" goes
|
||||
@@ -44,6 +44,7 @@ interface GetSmoothStepPathParams {
|
||||
targetX: number;
|
||||
targetY: number;
|
||||
targetPosition?: Position;
|
||||
borderRadius?: number;
|
||||
}
|
||||
|
||||
export function getSmoothStepPath({
|
||||
@@ -57,9 +58,10 @@ export function getSmoothStepPath({
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition = Position.Top,
|
||||
borderRadius = 5,
|
||||
}: GetSmoothStepPathParams): string {
|
||||
const cornerWidth = Math.min(5, Math.abs(targetX - sourceX));
|
||||
const cornerHeight = Math.min(5, Math.abs(targetY - sourceY));
|
||||
const cornerWidth = Math.min(borderRadius, Math.abs(targetX - sourceX));
|
||||
const cornerHeight = Math.min(borderRadius, Math.abs(targetY - sourceY));
|
||||
const cornerSize = Math.min(cornerWidth, cornerHeight, xOffset, yOffset);
|
||||
|
||||
const leftAndRight = [Position.Left, Position.Right];
|
||||
@@ -141,7 +143,8 @@ export default memo(
|
||||
targetPosition = Position.Top,
|
||||
arrowHeadType,
|
||||
markerEndId,
|
||||
}: EdgeBezierProps) => {
|
||||
borderRadius = 5,
|
||||
}: EdgeSmoothStepProps) => {
|
||||
const yOffset = Math.abs(targetY - sourceY) / 2;
|
||||
const centerY = targetY < sourceY ? targetY + yOffset : targetY - yOffset;
|
||||
|
||||
@@ -159,6 +162,7 @@ export default memo(
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition,
|
||||
borderRadius,
|
||||
});
|
||||
|
||||
const markerEnd = getMarkerEnd(arrowHeadType, markerEndId);
|
||||
|
||||
@@ -1,61 +1,8 @@
|
||||
import React, { memo } from 'react';
|
||||
|
||||
import EdgeText from './EdgeText';
|
||||
import { getMarkerEnd } from './utils';
|
||||
import { EdgeProps } from '../../types';
|
||||
import { EdgeSmoothStepProps } from '../../types';
|
||||
import SmoothStepEdge from './SmoothStepEdge';
|
||||
|
||||
interface GetStepPathParams {
|
||||
centerY: number;
|
||||
sourceX: number;
|
||||
sourceY: number;
|
||||
targetX: number;
|
||||
targetY: number;
|
||||
}
|
||||
|
||||
export function getStepPath({ centerY, sourceX, sourceY, targetX, targetY }: GetStepPathParams): string {
|
||||
return `M ${sourceX},${sourceY}L ${sourceX},${centerY}L ${targetX},${centerY}L ${targetX},${targetY}`;
|
||||
}
|
||||
|
||||
export default memo(
|
||||
({
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
label,
|
||||
labelStyle,
|
||||
labelShowBg,
|
||||
labelBgStyle,
|
||||
style,
|
||||
arrowHeadType,
|
||||
markerEndId,
|
||||
}: EdgeProps) => {
|
||||
const yOffset = Math.abs(targetY - sourceY) / 2;
|
||||
const centerY = targetY < sourceY ? targetY + yOffset : targetY - yOffset;
|
||||
|
||||
const xOffset = Math.abs(targetX - sourceX) / 2;
|
||||
const centerX = targetX < sourceX ? targetX + xOffset : targetX - xOffset;
|
||||
|
||||
const path = getStepPath({ centerY, sourceX, sourceY, targetX, targetY });
|
||||
|
||||
const markerEnd = getMarkerEnd(arrowHeadType, markerEndId);
|
||||
|
||||
const text = label ? (
|
||||
<EdgeText
|
||||
x={centerX}
|
||||
y={centerY}
|
||||
label={label}
|
||||
labelStyle={labelStyle}
|
||||
labelShowBg={labelShowBg}
|
||||
labelBgStyle={labelBgStyle}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<path style={style} className="react-flow__edge-path" d={path} markerEnd={markerEnd} />
|
||||
{text}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
export default memo((props: EdgeSmoothStepProps) => {
|
||||
return <SmoothStepEdge {...props} borderRadius={0} />;
|
||||
});
|
||||
|
||||
+8
-8
@@ -63,7 +63,7 @@
|
||||
.react-flow__edge-path {
|
||||
fill: none;
|
||||
stroke: #bbb;
|
||||
stroke-width: 2;
|
||||
stroke-width: 1.5;
|
||||
}
|
||||
|
||||
.react-flow__edge-text {
|
||||
@@ -83,7 +83,7 @@
|
||||
.react-flow__connection-path {
|
||||
fill: none;
|
||||
stroke: #ddd;
|
||||
stroke-width: 2;
|
||||
stroke-width: 1.5;
|
||||
}
|
||||
|
||||
.react-flow__nodes {
|
||||
@@ -107,7 +107,7 @@
|
||||
.react-flow__node-input,
|
||||
.react-flow__node-output {
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
border-radius: 3px;
|
||||
width: 150px;
|
||||
font-size: 12px;
|
||||
color: #222;
|
||||
@@ -127,16 +127,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
.react-flow__node-default {
|
||||
background: #ff6060;
|
||||
}
|
||||
|
||||
.react-flow__node-input {
|
||||
background: #9999ff;
|
||||
}
|
||||
|
||||
.react-flow__node-default {
|
||||
background: #ff6060;
|
||||
}
|
||||
|
||||
.react-flow__node-output {
|
||||
background: #55dd99;
|
||||
background: #79c9b7;
|
||||
}
|
||||
|
||||
.react-flow__nodesselection {
|
||||
|
||||
@@ -99,6 +99,10 @@ export interface EdgeBezierProps extends EdgeProps {
|
||||
targetPosition: Position;
|
||||
}
|
||||
|
||||
export interface EdgeSmoothStepProps extends EdgeBezierProps {
|
||||
borderRadius?: number;
|
||||
}
|
||||
|
||||
export interface NodeProps {
|
||||
id: ElementId;
|
||||
type: string;
|
||||
|
||||
+8
-1
@@ -33,9 +33,16 @@ const getEdgeId = ({ source, target }: Connection): ElementId => `reactflow__edg
|
||||
|
||||
export const addEdge = (edgeParams: Edge | Connection, elements: Elements): Elements => {
|
||||
if (!edgeParams.source || !edgeParams.target) {
|
||||
throw new Error('Can not create edge. An edge needs a source and a target');
|
||||
throw new Error("Can't create edge. An edge needs a source and a target.");
|
||||
}
|
||||
|
||||
// make sure that there is node with the target and one with the source id
|
||||
[edgeParams.source, edgeParams.target].forEach((id) => {
|
||||
if (!elements.find((e) => isNode(e) && e.id === id)) {
|
||||
throw new Error(`Can't create edge. Node with id=${id} does not exist.`);
|
||||
}
|
||||
});
|
||||
|
||||
if (isEdge(edgeParams)) {
|
||||
return elements.concat({ ...edgeParams });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user