From e5e64bb490ee209eb38d1470d8eb7d7c113613a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Alvarez?= Date: Mon, 11 May 2020 02:33:52 -0300 Subject: [PATCH 01/11] feat(lock): add lock and unlock svg icons --- assets/icons/lock.svg | 1 + assets/icons/unlock.svg | 1 + 2 files changed, 2 insertions(+) create mode 100644 assets/icons/lock.svg create mode 100644 assets/icons/unlock.svg diff --git a/assets/icons/lock.svg b/assets/icons/lock.svg new file mode 100644 index 00000000..72c66dc7 --- /dev/null +++ b/assets/icons/lock.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/unlock.svg b/assets/icons/unlock.svg new file mode 100644 index 00000000..b33b6698 --- /dev/null +++ b/assets/icons/unlock.svg @@ -0,0 +1 @@ + From f500d52f2fc8dec20bae23f186522d28c5917f26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Alvarez?= Date: Mon, 11 May 2020 02:42:50 -0300 Subject: [PATCH 02/11] feat(lock): button enabling and disabling interactivity on Controls plugin --- src/plugins/Controls/index.tsx | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/plugins/Controls/index.tsx b/src/plugins/Controls/index.tsx index e2c38712..8d528136 100644 --- a/src/plugins/Controls/index.tsx +++ b/src/plugins/Controls/index.tsx @@ -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', @@ -18,6 +22,9 @@ interface ControlProps extends React.HTMLAttributes {} export default ({ style, className }: ControlProps) => { const mapClasses: string = classnames('react-flow__controls', className); + const setInteractive = useStoreActions(actions => actions.setInteractive); + const { isInteractive } = useStoreState(({ isInteractive }) => ({ isInteractive })); + return (
{ ...style, }} > +
setInteractive(!isInteractive)} + > + { isInteractive ? : } +
Date: Mon, 11 May 2020 03:14:09 -0300 Subject: [PATCH 03/11] feat(lock): update className of lock button based on interactivity state --- src/plugins/Controls/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/Controls/index.tsx b/src/plugins/Controls/index.tsx index 8d528136..2730c61c 100644 --- a/src/plugins/Controls/index.tsx +++ b/src/plugins/Controls/index.tsx @@ -34,7 +34,7 @@ export default ({ style, className }: ControlProps) => { }} >
setInteractive(!isInteractive)} > { isInteractive ? : } From e2a825c100c230d740da3f50e783392ce4085a4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Alvarez?= Date: Mon, 11 May 2020 03:14:43 -0300 Subject: [PATCH 04/11] feat(lock): tests for lock condition before and after button click --- cypress/integration/flow/empty.spec.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/cypress/integration/flow/empty.spec.js b/cypress/integration/flow/empty.spec.js index 54f514a0..8fffb924 100644 --- a/cypress/integration/flow/empty.spec.js +++ b/cypress/integration/flow/empty.spec.js @@ -32,6 +32,14 @@ describe('Empty Flow Rendering', () => { cy.get('.react-flow__controls-fitview').click(); }); + it('uses lock view control', () => { + cy.get('.react-flow__controls-unlocked').click(); + cy.get('.react-flow__controls-unlocked').should('not.exist'); + + cy.get('.react-flow__controls-locked').click(); + cy.get('.react-flow__controls-locked').should('not.exist'); + }) + it('renders an empty mini map', () => { cy.get('.react-flow__minimap'); cy.get('.react-flow__minimap-node').should('not.exist'); From c3e8bfb34b6d7d910583e8e5b80e32bde00566a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Alvarez?= Date: Mon, 11 May 2020 13:38:16 -0300 Subject: [PATCH 05/11] feat(lock): updated unlock icon --- assets/icons/unlock.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/icons/unlock.svg b/assets/icons/unlock.svg index b33b6698..22ed8018 100644 --- a/assets/icons/unlock.svg +++ b/assets/icons/unlock.svg @@ -1 +1 @@ - + From 6dcb9f51204919dc890414271a085cdb7fde4c09 Mon Sep 17 00:00:00 2001 From: moklick Date: Mon, 11 May 2020 18:50:09 +0200 Subject: [PATCH 06/11] refactor(renderer): show error when no width or height is set #196 --- src/container/GraphView/index.tsx | 5 +++++ src/container/ReactFlow/index.tsx | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/container/GraphView/index.tsx b/src/container/GraphView/index.tsx index aa78cef8..3423c334 100644 --- a/src/container/GraphView/index.tsx +++ b/src/container/GraphView/index.tsx @@ -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); }; diff --git a/src/container/ReactFlow/index.tsx b/src/container/ReactFlow/index.tsx index d7c86841..371a0deb 100644 --- a/src/container/ReactFlow/index.tsx +++ b/src/container/ReactFlow/index.tsx @@ -49,7 +49,7 @@ export interface ReactFlowProps extends Omit, 'on const ReactFlow = ({ style, onElementClick, - elements, + elements = [], children, nodeTypes, edgeTypes, From c0b80f2d894502faad6d32ea73cec3a1ddb87d32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Alvarez?= Date: Mon, 11 May 2020 13:55:44 -0300 Subject: [PATCH 07/11] feat(lock): Lock/unlock button at bottom --- src/plugins/Controls/index.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/plugins/Controls/index.tsx b/src/plugins/Controls/index.tsx index 2730c61c..79519e43 100644 --- a/src/plugins/Controls/index.tsx +++ b/src/plugins/Controls/index.tsx @@ -33,12 +33,6 @@ export default ({ style, className }: ControlProps) => { ...style, }} > -
setInteractive(!isInteractive)} - > - { isInteractive ? : } -
{ >
+
setInteractive(!isInteractive)} + > + { isInteractive ? : } +
); }; From 6ea206e3be2f4c8024cce73e2c15da563cb0bb30 Mon Sep 17 00:00:00 2001 From: moklick Date: Mon, 11 May 2020 19:08:07 +0200 Subject: [PATCH 08/11] feat(controls): add showZoom, showFitView, showInteractive options #211 --- README.md | 5 ++- src/plugins/Controls/index.tsx | 57 ++++++++++++++++++---------------- 2 files changed, 34 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 094af382..cf3a2550 100644 --- a/README.md +++ b/README.md @@ -254,7 +254,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 +272,9 @@ const GraphWithControls = () => ( - `style`: css properties - `className`: class name +- `showZoom`: boolean - default: true +- `showFitView`: boolean - default: true +- `showInteractive`: boolean - default: true ## Examples diff --git a/src/plugins/Controls/index.tsx b/src/plugins/Controls/index.tsx index 79519e43..12e81c38 100644 --- a/src/plugins/Controls/index.tsx +++ b/src/plugins/Controls/index.tsx @@ -17,12 +17,16 @@ const baseStyle: CSSProperties = { left: 10, }; -interface ControlProps extends React.HTMLAttributes {} +interface ControlProps extends React.HTMLAttributes { + 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 setInteractive = useStoreActions((actions) => actions.setInteractive); const { isInteractive } = useStoreState(({ isInteractive }) => ({ isInteractive })); return ( @@ -33,30 +37,29 @@ export default ({ style, className }: ControlProps) => { ...style, }} > -
- -
-
- -
-
fitView()} - > - -
-
setInteractive(!isInteractive)} - > - { isInteractive ? : } -
+ {showZoom && ( + <> +
+ +
+
+ +
+ + )} + {showFitView && ( +
fitView()}> + +
+ )} + {showInteractive && ( +
setInteractive(!isInteractive)} + > + {isInteractive ? : } +
+ )}
); }; From 4af6261771ee3d8252a336f295008e304262083f Mon Sep 17 00:00:00 2001 From: moklick Date: Mon, 11 May 2020 22:05:50 +0200 Subject: [PATCH 09/11] feat(examples): add custom edge closes #193 --- example/src/Edges/CustomEdge.js | 16 ++++++++++++++++ example/src/Edges/index.js | 15 +++++++++++---- src/index.ts | 9 ++------- 3 files changed, 29 insertions(+), 11 deletions(-) create mode 100644 example/src/Edges/CustomEdge.js diff --git a/example/src/Edges/CustomEdge.js b/example/src/Edges/CustomEdge.js new file mode 100644 index 00000000..9cfd9619 --- /dev/null +++ b/example/src/Edges/CustomEdge.js @@ -0,0 +1,16 @@ +import React from 'react'; + +export default function CustomEdge({ + id, sourceX, sourceY, targetX, targetY, label, style = {} +}) { + return ( + <> + + + + {label} + + + + ); +}; \ No newline at end of file diff --git a/example/src/Edges/index.js b/example/src/Edges/index.js index 8f959066..16333481 100644 --- a/example/src/Edges/index.js +++ b/example/src/Edges/index.js @@ -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 + }} > diff --git a/src/index.ts b/src/index.ts index e856955f..c76d01a6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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'; From 0d28e8fb0cc480c484f0b85bda3a2bd3598cb568 Mon Sep 17 00:00:00 2001 From: moklick Date: Mon, 11 May 2020 22:21:37 +0200 Subject: [PATCH 10/11] feat(examples): add horizontal flow --- README.md | 2 ++ example/src/Basic/index.js | 6 +++--- example/src/Horizontal/index.js | 35 +++++++++++++++++++++++++++++++++ example/src/index.js | 5 +++++ 4 files changed, 45 insertions(+), 3 deletions(-) create mode 100644 example/src/Horizontal/index.js diff --git a/README.md b/README.md index cf3a2550..c2683522 100644 --- a/README.md +++ b/README.md @@ -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 @@ -283,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) diff --git a/example/src/Basic/index.js b/example/src/Basic/index.js index ca736bed..f1210672 100644 --- a/example/src/Basic/index.js +++ b/example/src/Basic/index.js @@ -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 ( - { + 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 ( + + ); +} + +export default HorizontalFlow; diff --git a/example/src/index.js b/example/src/index.js index 76b5ed84..0ca8f17a 100644 --- a/example/src/index.js +++ b/example/src/index.js @@ -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, From 65134b0afda7693876c2a7218b9770b586ab1466 Mon Sep 17 00:00:00 2001 From: moklick Date: Mon, 11 May 2020 22:34:14 +0200 Subject: [PATCH 11/11] fix(tests): dont start lib dev server while running tests --- cypress/integration/flow/empty.spec.js | 15 ++++----------- package.json | 25 +++++++++++++------------ 2 files changed, 17 insertions(+), 23 deletions(-) diff --git a/cypress/integration/flow/empty.spec.js b/cypress/integration/flow/empty.spec.js index 8fffb924..d378ef85 100644 --- a/cypress/integration/flow/empty.spec.js +++ b/cypress/integration/flow/empty.spec.js @@ -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', () => { @@ -33,12 +33,8 @@ describe('Empty Flow Rendering', () => { }); it('uses lock view control', () => { - cy.get('.react-flow__controls-unlocked').click(); - cy.get('.react-flow__controls-unlocked').should('not.exist'); - - cy.get('.react-flow__controls-locked').click(); - cy.get('.react-flow__controls-locked').should('not.exist'); - }) + cy.get('.react-flow__controls-interactive').click(); + }); it('renders an empty mini map', () => { cy.get('.react-flow__minimap'); @@ -51,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() diff --git a/package.json b/package.json index 2d1a271d..af3a7aee 100644 --- a/package.json +++ b/package.json @@ -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" ]