Merge pull request #352 from wbkd/develop

Develop
This commit is contained in:
Moritz
2020-07-21 16:49:11 +02:00
committed by GitHub
25 changed files with 577 additions and 154 deletions
+1
View File
@@ -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
+5 -9
View File
@@ -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 })
+80
View File
@@ -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');
});
});
});
-10
View File
@@ -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);
});
});
+3 -21
View File
@@ -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);
});
});
});
+3 -1
View File
@@ -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);
+84 -1
View File
@@ -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);
});
});
});
+69
View File
@@ -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);
});
});
});
});
-17
View File
@@ -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');
});
});
-10
View File
@@ -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);
});
});
+1 -1
View File
@@ -70,7 +70,7 @@ const CustomNodeFlow = () => {
nodeTypes={{
selectorNode: ColorSelectorNode,
}}
connectionLineStyle={{ stroke: '#ddd', strokeWidth: 2 }}
connectionLineStyle={{ stroke: '#ddd' }}
snapToGrid={true}
snapGrid={[16, 16]}
>
+36
View File
@@ -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;
+104
View File
@@ -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;
}
+2 -2
View File
@@ -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
+9 -6
View File
@@ -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 />
+5
View File
@@ -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
View File
@@ -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
+7 -2
View File
@@ -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({
+8 -4
View File
@@ -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);
+5 -58
View File
@@ -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
View File
@@ -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 {
+4
View File
@@ -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
View File
@@ -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 });
}