Merge pull request #214 from wbkd/develop

add horizontal flow, custom edges, interactive control
This commit is contained in:
Moritz
2020-05-11 22:38:42 +02:00
committed by GitHub
14 changed files with 141 additions and 53 deletions

View File

@@ -199,6 +199,7 @@ edgeTypes={{
You can now use type `special` for an edge.
The `straight`, `default` and `step` types will be still available except you overwrite one of them.
There is an implementation of a custom edge in the [edges example](/example/src/Edges/index.js).
## Helper Functions
@@ -254,7 +255,7 @@ const GraphWithMiniMap = () => (
### Controls
The control panel contains a zoom-in, zoom-out and a fit-view button. You can use it by passing it as a children to your React Flow component:
The control panel contains a zoom-in, zoom-out, fit-view and a lock/unlock button. You can use it by passing it as a children to the React Flow component:
```javascript
import ReactFlow, { Controls } from 'react-flow-renderer';
@@ -272,6 +273,9 @@ const GraphWithControls = () => (
- `style`: css properties
- `className`: class name
- `showZoom`: boolean - default: true
- `showFitView`: boolean - default: true
- `showInteractive`: boolean - default: true
## Examples
@@ -280,6 +284,7 @@ You can find all examples in the [example](example) folder or check out the live
- [rich](https://react-flow.netlify.app/)
- [basic](https://react-flow.netlify.app/basic)
- [custom node](https://react-flow.netlify.app/custom-node)
- [horizontal](https://react-flow.netlify.app/horizontal)
- [stress](https://react-flow.netlify.app/stress)
- [edges](https://react-flow.netlify.app/edges)
- [empty](https://react-flow.netlify.app/empty)

1
assets/icons/lock.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M376 186h-20v-40c0-55-45-100-100-100S156 91 156 146v40h-20c-22.002 0-40 17.998-40 40v200c0 22.002 17.998 40 40 40h240c22.002 0 40-17.998 40-40V226c0-22.002-17.998-40-40-40zM256 368c-22.002 0-40-17.998-40-40s17.998-40 40-40 40 17.998 40 40-17.998 40-40 40zm62.002-182H193.998v-40c0-34.004 28.003-62.002 62.002-62.002 34.004 0 62.002 27.998 62.002 62.002v40z"/></svg>

After

Width:  |  Height:  |  Size: 437 B

1
assets/icons/unlock.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="m376,187l-20,0l0,-40c0,-55 -45,-100 -100,-100c-54.002,23.996 -17.999,27.998 4,31.998c21.999,4 58.002,33.998 58.002,68.002l-0.002,0l0,40l-182,0c-22.002,0 -40,17.998 -40,40l0,200c0,22.002 17.998,40 40,40l240,0c22.002,0 40,-17.998 40,-40l0,-200c0,-22.002 -17.998,-40 -40,-40zm-120,182c-22.002,0 -40,-17.998 -40,-40s17.998,-40 40,-40s40,17.998 40,40s-17.998,40 -40,40z"/></svg>

After

Width:  |  Height:  |  Size: 445 B

View File

@@ -13,7 +13,7 @@ describe('Empty Flow Rendering', () => {
.get('.react-flow__selectionpane')
.trigger('mousedown', 'topLeft', { which: 1, force: true })
.trigger('mousemove', 'bottomRight', { which: 1 })
.trigger('mouseup', 'bottomRight', { force: true })
.trigger('mouseup', 'bottomRight', { force: true });
});
it('renders a control panel', () => {
@@ -32,6 +32,10 @@ describe('Empty Flow Rendering', () => {
cy.get('.react-flow__controls-fitview').click();
});
it('uses lock view control', () => {
cy.get('.react-flow__controls-interactive').click();
});
it('renders an empty mini map', () => {
cy.get('.react-flow__minimap');
cy.get('.react-flow__minimap-node').should('not.exist');
@@ -43,10 +47,7 @@ describe('Empty Flow Rendering', () => {
});
it('connects nodes', () => {
cy.get('.react-flow__node')
.first()
.find('.react-flow__handle.source')
.trigger('mousedown', { which: 1 });
cy.get('.react-flow__node').first().find('.react-flow__handle.source').trigger('mousedown', { which: 1 });
cy.get('.react-flow__node')
.last()

View File

@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import ReactFlow, { removeElements, addEdge } from 'react-flow-renderer';
import Graph, { removeElements, addEdge } from 'react-flow-renderer';
const onNodeDragStop = node => console.log('drag stop', node);
const onLoad = graphInstance => console.log('graph loaded:', graphInstance);
@@ -11,7 +11,7 @@ const initialElements = [
{ 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 } },
{ id: 'e1-2', source: '1', target: '2', animated: true, label: 'edge text' },
{ id: 'e1-2', source: '1', target: '2', animated: true },
{ id: 'e1-3', source: '1', target: '3' },
];
@@ -22,7 +22,7 @@ const BasicGraph = () => {
const onConnect = (params) => setElements(els => addEdge(params, els));
return (
<ReactFlow
<Graph
elements={elements}
onLoad={onLoad}
onElementClick={onElementClick}

View File

@@ -0,0 +1,16 @@
import React from 'react';
export default function CustomEdge({
id, sourceX, sourceY, targetX, targetY, label, style = {}
}) {
return (
<>
<path id={id} style={style} className="react-flow__edge-path" d={`M ${sourceX},${sourceY}L ${targetX},${targetY}`} />
<text>
<textPath href={`#${id}`} style={{ fontSize: '12px' }} startOffset="50%" textAnchor="middle">
{label}
</textPath>
</text>
</>
);
};

View File

@@ -2,6 +2,8 @@ import React, { useState } from 'react';
import ReactFlow, { removeElements, addEdge, MiniMap, Controls } from 'react-flow-renderer';
import CustomEdge from './CustomEdge';
const onNodeDragStop = node => console.log('drag stop', node);
const onElementClick = element => console.log('click', element);
const onLoad = (graph) => {
@@ -9,14 +11,15 @@ const onLoad = (graph) => {
};
const initialElements = [
{ id: '1', type: 'input', data: { label: 'Input Node 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: '3', data: { label: 'Node 3' }, position: { x: 250, y: 200 } },
{ 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: 450, y: 400 } },
{ id: '6', type: 'output', data: { label: 'Output Node 5' }, position: { x: 250, y: 550 } },
{ id: '7', type: 'output', data: { label: 'Output Node 6' }, position: { x: 550, y: 550 } },
{ 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: 'e1-2', source: '1', target: '2', label: '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' },
@@ -24,6 +27,7 @@ const initialElements = [
{ id: 'e3-5', source: '4', target: '5', animated: true, label: 'animated styled edge', style: { stroke: 'red' } },
{ id: 'e5-6', source: '5', target: '6', label: 'styled label', labelStyle: { fill: 'red', fontWeight: 700 } },
{ id: 'e5-7', source: '5', target: '7', label: 'label with styled bg', labelBgStyle: { fill: '#eee', fillOpacity: 0.7 } },
{ id: 'e5-8', source: '5', target: '8', type: 'custom', label: 'custom edge' },
];
const EdgesFlow = () => {
@@ -42,6 +46,9 @@ const EdgesFlow = () => {
style={{ width: '100%', height: '100%' }}
onLoad={onLoad}
snapToGrid={true}
edgeTypes={{
custom: CustomEdge
}}
>
<MiniMap />
<Controls />

View File

@@ -0,0 +1,35 @@
import React, { useState } from 'react';
import ReactFlow, { removeElements, addEdge } from 'react-flow-renderer';
const onLoad = (graph) => {
graph.fitView();
};
const initialElements = [
{ id: '1', sourcePosition: 'right', type: 'input', data: { label: 'Input' }, position: { x: 0, y: 80 } },
{ id: '2', sourcePosition: 'right', targetPosition: 'left', data: { label: 'A Node' }, position: { x: 250, y: 0 } },
{ id: '3', sourcePosition: 'right', targetPosition: 'left', data: { label: 'Another node' }, position: { x: 250, y: 160 } },
{ id: '4', sourcePosition: 'right', targetPosition: 'left', data: { label: 'Node 4' }, position: { x: 500, y: 80 } },
{ id: 'e1-2', source: '1', target: '2', animated: true, },
{ id: 'e1-3', source: '1', target: '3', animated: true, },
{ id: 'e1-4', source: '2', target: '4', label: 'edge label' }
];
const HorizontalFlow = () => {
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}
onElementsRemove={onElementsRemove}
onConnect={onConnect}
onLoad={onLoad}
/>
);
}
export default HorizontalFlow;

View File

@@ -9,6 +9,7 @@ import Stress from './Stress';
import Inactive from './Inactive';
import Empty from './Empty';
import Edges from './Edges';
import Horizontal from './Horizontal';
import './index.css';
@@ -28,6 +29,10 @@ const routes = [{
path: '/custom-node',
component: CustomNode,
label: 'CustomNode'
}, {
path: '/horizontal',
component: Horizontal,
label: 'Horizontal'
}, {
path: '/stress',
component: Stress,

View File

@@ -9,6 +9,19 @@
"type": "git",
"url": "https://github.com/wbkd/react-flow.git"
},
"scripts": {
"build": "rollup -c --environment NODE_ENV:production",
"start": "rollup -w -c",
"dev": "npm run build && npm start & cd example && npm start",
"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",
"release": "release-it"
},
"dependencies": {
"@welldone-software/why-did-you-render": "^4.2.0",
"classnames": "^2.2.6",
@@ -56,18 +69,6 @@
"peerDependencies": {
"react": "^16.13.1"
},
"scripts": {
"build": "rollup -c --environment NODE_ENV:production",
"start": "rollup -w -c",
"dev": "npm run build && npm start & cd example && npm start",
"dev:wait": "start-server-and-test dev 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",
"release": "release-it"
},
"files": [
"dist"
]

View File

@@ -89,6 +89,11 @@ const GraphView = memo(
}
const size = getDimensions(rendererNode.current);
if (size.height === 0 || size.width === 0) {
throw new Error('The React Flow parent container needs a width and a height to render the graph.');
}
updateSize(size);
};

View File

@@ -49,7 +49,7 @@ export interface ReactFlowProps extends Omit<HTMLAttributes<HTMLDivElement>, 'on
const ReactFlow = ({
style,
onElementClick,
elements,
elements = [],
children,
nodeTypes,
edgeTypes,

View File

@@ -3,12 +3,7 @@ import ReactFlow from './container/ReactFlow';
export default ReactFlow;
export { default as Handle } from './components/Handle';
export { default as EdgeText } from './components/Edges/EdgeText';
export { MiniMap, Controls } from './plugins';
export {
isNode,
isEdge,
removeElements,
addEdge,
getOutgoers,
} from './utils/graph';
export { isNode, isEdge, removeElements, addEdge, getOutgoers } from './utils/graph';

View File

@@ -2,9 +2,13 @@ import React, { CSSProperties } from 'react';
import classnames from 'classnames';
import { fitView, zoomIn, zoomOut } from '../../utils/graph';
import { useStoreState, useStoreActions } from '../../store/hooks';
import PlusIcon from '../../../assets/icons/plus.svg';
import MinusIcon from '../../../assets/icons/minus.svg';
import FitviewIcon from '../../../assets/icons/fitview.svg';
import LockIcon from '../../../assets/icons/lock.svg';
import UnlockIcon from '../../../assets/icons/unlock.svg';
const baseStyle: CSSProperties = {
position: 'absolute',
@@ -13,11 +17,18 @@ const baseStyle: CSSProperties = {
left: 10,
};
interface ControlProps extends React.HTMLAttributes<HTMLDivElement> {}
interface ControlProps extends React.HTMLAttributes<HTMLDivElement> {
showZoom?: boolean;
showFitView?: boolean;
showInteractive?: boolean;
}
export default ({ style, className }: ControlProps) => {
export default ({ style, showZoom = true, showFitView = true, showInteractive = true, className }: ControlProps) => {
const mapClasses: string = classnames('react-flow__controls', className);
const setInteractive = useStoreActions((actions) => actions.setInteractive);
const { isInteractive } = useStoreState(({ isInteractive }) => ({ isInteractive }));
return (
<div
className={mapClasses}
@@ -26,24 +37,29 @@ export default ({ style, className }: ControlProps) => {
...style,
}}
>
<div
className="react-flow__controls-button react-flow__controls-zoomin"
onClick={zoomIn}
>
<PlusIcon />
</div>
<div
className="react-flow__controls-button react-flow__controls-zoomout"
onClick={zoomOut}
>
<MinusIcon />
</div>
<div
className="react-flow__controls-button react-flow__controls-fitview"
onClick={() => fitView()}
>
<FitviewIcon />
</div>
{showZoom && (
<>
<div className="react-flow__controls-button react-flow__controls-zoomin" onClick={zoomIn}>
<PlusIcon />
</div>
<div className="react-flow__controls-button react-flow__controls-zoomout" onClick={zoomOut}>
<MinusIcon />
</div>
</>
)}
{showFitView && (
<div className="react-flow__controls-button react-flow__controls-fitview" onClick={() => fitView()}>
<FitviewIcon />
</div>
)}
{showInteractive && (
<div
className="react-flow__controls-button react-flow__controls-interactive"
onClick={() => setInteractive(!isInteractive)}
>
{isInteractive ? <UnlockIcon /> : <LockIcon />}
</div>
)}
</div>
);
};