refactor(repo): use turbo + rollup + vite
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -0,0 +1,18 @@
|
||||
import { defineConfig } from 'cypress';
|
||||
|
||||
export default defineConfig({
|
||||
e2e: {
|
||||
baseUrl: 'http://localhost:5173',
|
||||
viewportWidth: 1280,
|
||||
viewportHeight: 720,
|
||||
video: false,
|
||||
},
|
||||
component: {
|
||||
devServer: {
|
||||
framework: 'react',
|
||||
bundler: 'vite',
|
||||
},
|
||||
screenshotOnRunFailure: false,
|
||||
video: false,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import ReactFlow, { Edge, useEdges } from 'reactflow';
|
||||
|
||||
import { nodes as initialNodes, edges as initialEdges } from '../../fixtures/simpleflow';
|
||||
|
||||
describe('useEdges.cy.tsx', () => {
|
||||
it('handles edges', () => {
|
||||
const onChangeSpy = cy.spy().as('onChangeSpy');
|
||||
|
||||
cy.mount(
|
||||
<ReactFlow nodes={initialNodes} edges={initialEdges}>
|
||||
<HookHelperComponent onChange={onChangeSpy} />
|
||||
</ReactFlow>
|
||||
);
|
||||
|
||||
cy.get('@onChangeSpy').should('have.been.calledWith', []);
|
||||
cy.get('@onChangeSpy').should('have.been.calledWith', initialEdges);
|
||||
});
|
||||
|
||||
it('handles defaultEdges', () => {
|
||||
const onChangeSpy = cy.spy().as('onChangeSpy');
|
||||
|
||||
cy.mount(
|
||||
<ReactFlow defaultNodes={initialNodes} defaultEdges={initialEdges}>
|
||||
<HookHelperComponent onChange={onChangeSpy} />
|
||||
</ReactFlow>
|
||||
);
|
||||
|
||||
cy.get('@onChangeSpy').should('have.been.calledWith', []);
|
||||
cy.get('@onChangeSpy').should('have.been.calledWith', initialEdges);
|
||||
});
|
||||
});
|
||||
|
||||
const HookHelperComponent = ({ onChange }: { onChange: (edges: Edge[]) => void }) => {
|
||||
const edges = useEdges();
|
||||
|
||||
useEffect(() => {
|
||||
onChange(edges);
|
||||
}, [edges]);
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
import { useEffect } from 'react';
|
||||
import ReactFlow, { Node, useNodes } from 'reactflow';
|
||||
|
||||
import { nodes } from '../../fixtures/simpleflow';
|
||||
|
||||
const nodeDimensions = {
|
||||
width: 100,
|
||||
height: 50,
|
||||
};
|
||||
|
||||
const initialNodes: Node[] = nodes.map((n) => ({
|
||||
...n,
|
||||
style: nodeDimensions,
|
||||
}));
|
||||
|
||||
const expectedNodes: Node[] = initialNodes.map((n) => ({
|
||||
...n,
|
||||
positionAbsolute: n.position,
|
||||
...nodeDimensions,
|
||||
}));
|
||||
|
||||
describe('useNodes.cy.tsx', () => {
|
||||
it('handles nodes', () => {
|
||||
const onChangeSpy = cy.spy().as('onChangeSpy');
|
||||
|
||||
cy.mount(
|
||||
<ReactFlow nodes={initialNodes}>
|
||||
<HookHelperComponent onChange={onChangeSpy} />
|
||||
</ReactFlow>
|
||||
);
|
||||
|
||||
cy.get('@onChangeSpy').should('have.been.calledWith', []);
|
||||
cy.get('@onChangeSpy').should('have.been.calledWith', expectedNodes);
|
||||
});
|
||||
|
||||
it('handles defaultNodes', () => {
|
||||
const onChangeSpy = cy.spy().as('onChangeSpy');
|
||||
|
||||
cy.mount(
|
||||
<ReactFlow defaultNodes={initialNodes}>
|
||||
<HookHelperComponent onChange={onChangeSpy} />
|
||||
</ReactFlow>
|
||||
);
|
||||
|
||||
cy.get('@onChangeSpy').should('have.been.calledWith', []);
|
||||
cy.get('@onChangeSpy').should('have.been.calledWith', expectedNodes);
|
||||
});
|
||||
});
|
||||
|
||||
const HookHelperComponent = ({ onChange }: { onChange: (nodes: Node[]) => void }) => {
|
||||
const nodes = useNodes();
|
||||
|
||||
useEffect(() => {
|
||||
onChange(nodes);
|
||||
}, [nodes]);
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
import ReactFlow, { useNodesInitialized } from 'reactflow';
|
||||
|
||||
import { nodes } from '../../fixtures/simpleflow';
|
||||
import ControlledFlow from '../../support/ControlledFlow';
|
||||
|
||||
describe('useNodesInitialized.cy.tsx', () => {
|
||||
it('returns false for no nodes', () => {
|
||||
const initSpy = cy.spy().as('initSpy');
|
||||
|
||||
cy.mount(
|
||||
<ReactFlow nodes={[]}>
|
||||
<HookHelperComponent onChange={initSpy} />
|
||||
</ReactFlow>
|
||||
);
|
||||
|
||||
cy.get('@initSpy').should('to.be.calledOnce');
|
||||
cy.get('@initSpy').should('have.been.calledWith', false);
|
||||
});
|
||||
|
||||
it('returns false without onNodesChange', () => {
|
||||
const initSpy = cy.spy().as('initSpy');
|
||||
|
||||
cy.mount(
|
||||
<ReactFlow nodes={nodes}>
|
||||
<HookHelperComponent onChange={initSpy} />
|
||||
</ReactFlow>
|
||||
);
|
||||
|
||||
cy.get('@initSpy').should('to.be.calledOnce');
|
||||
cy.get('@initSpy').should('have.be.calledWith', false);
|
||||
});
|
||||
|
||||
it('returns true for defaultNodes', () => {
|
||||
const initSpy = cy.spy().as('initSpy');
|
||||
|
||||
cy.mount(
|
||||
<ReactFlow defaultNodes={nodes}>
|
||||
<HookHelperComponent onChange={initSpy} />
|
||||
</ReactFlow>
|
||||
);
|
||||
|
||||
cy.get('@initSpy').should('have.been.calledWith', false);
|
||||
cy.get('@initSpy').should('have.been.calledWith', true);
|
||||
});
|
||||
|
||||
it('returns true for nodes + onNodesChange', () => {
|
||||
const initSpy = cy.spy().as('initSpy');
|
||||
|
||||
cy.mount(
|
||||
<ControlledFlow initialNodes={nodes}>
|
||||
<HookHelperComponent onChange={initSpy} />
|
||||
</ControlledFlow>
|
||||
);
|
||||
|
||||
cy.get('@initSpy').should('have.been.calledWith', false);
|
||||
cy.get('@initSpy').should('have.been.calledWith', true);
|
||||
});
|
||||
});
|
||||
|
||||
// test specific helpers
|
||||
|
||||
function HookHelperComponent({ onChange }: { onChange: (init: boolean) => void }) {
|
||||
const initialized = useNodesInitialized();
|
||||
|
||||
onChange(initialized);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import ReactFlow, { useOnViewportChange, Viewport } from 'reactflow';
|
||||
|
||||
describe('useOnViewportChange.cy.tsx', () => {
|
||||
it('listen to viewport drag', () => {
|
||||
const onStartSpy = cy.spy().as('onStartSpy');
|
||||
const onChangeSpy = cy.spy().as('onChangeSpy');
|
||||
const onEndSpy = cy.spy().as('onEndSpy');
|
||||
|
||||
cy.mount(
|
||||
<ReactFlow>
|
||||
<HookHelperComponent onStart={onStartSpy} onChange={onChangeSpy} onEnd={onEndSpy} />
|
||||
</ReactFlow>
|
||||
);
|
||||
|
||||
cy.dragPane({ from: { x: 50, y: 50 }, to: { x: 50, y: 100 } }).then(() => {
|
||||
cy.get('@onStartSpy').should('have.been.calledWith', { x: 0, y: 0, zoom: 1 });
|
||||
cy.get('@onChangeSpy').should('have.been.calledWith', { x: 0, y: 50, zoom: 1 });
|
||||
cy.get('@onEndSpy').should('have.been.calledWith', { x: 0, y: 50, zoom: 1 });
|
||||
});
|
||||
});
|
||||
|
||||
it('handles zoom in', () => {
|
||||
const onStartSpy = cy.spy().as('onStartSpy');
|
||||
const onChangeSpy = cy.spy().as('onChangeSpy');
|
||||
const onEndSpy = cy.spy().as('onEndSpy');
|
||||
|
||||
cy.mount(
|
||||
<ReactFlow>
|
||||
<HookHelperComponent onStart={onStartSpy} onChange={onChangeSpy} onEnd={onEndSpy} />
|
||||
</ReactFlow>
|
||||
);
|
||||
|
||||
cy.zoomPane(-200).then(() => {
|
||||
cy.get('@onStartSpy').should('have.been.callCount', 1);
|
||||
cy.get('@onChangeSpy').should('have.been.callCount', 1);
|
||||
cy.get('@onEndSpy').should('have.been.callCount', 1);
|
||||
|
||||
expect(getLatestViewport(onChangeSpy).zoom).to.be.gt(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('handles zoom out', () => {
|
||||
const onStartSpy = cy.spy().as('onStartSpy');
|
||||
const onChangeSpy = cy.spy().as('onChangeSpy');
|
||||
const onEndSpy = cy.spy().as('onEndSpy');
|
||||
|
||||
cy.mount(
|
||||
<ReactFlow>
|
||||
<HookHelperComponent onStart={onStartSpy} onChange={onChangeSpy} onEnd={onEndSpy} />
|
||||
</ReactFlow>
|
||||
);
|
||||
|
||||
cy.zoomPane(200).then(() => {
|
||||
cy.get('@onStartSpy').should('have.been.callCount', 1);
|
||||
cy.get('@onChangeSpy').should('have.been.callCount', 1);
|
||||
cy.get('@onEndSpy').should('have.been.callCount', 1);
|
||||
|
||||
expect(getLatestViewport(onChangeSpy).zoom).to.be.lt(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('handles default viewport', () => {
|
||||
const defaultViewport = { x: 100, y: 100, zoom: 0.5 };
|
||||
const onStartSpy = cy.spy().as('onStartSpy');
|
||||
|
||||
cy.mount(
|
||||
<ReactFlow defaultViewport={defaultViewport}>
|
||||
<HookHelperComponent onStart={onStartSpy} />
|
||||
</ReactFlow>
|
||||
);
|
||||
|
||||
cy.dragPane({ from: { x: 50, y: 50 }, to: { x: 50, y: 100 } }).then(() => {
|
||||
cy.get('@onStartSpy').should('have.been.calledWith', defaultViewport);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// test specific helpers
|
||||
|
||||
function HookHelperComponent({
|
||||
onStart,
|
||||
onChange,
|
||||
onEnd,
|
||||
}: {
|
||||
onStart?: (viewport: Viewport) => void;
|
||||
onChange?: (viewport: Viewport) => void;
|
||||
onEnd?: (viewport: Viewport) => void;
|
||||
}) {
|
||||
useOnViewportChange({ onStart, onChange, onEnd });
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function getLatestViewport(onChangeSpy: any) {
|
||||
return onChangeSpy.lastCall.args[0] as unknown as Viewport;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import ReactFlow, { useViewport, Viewport } from 'reactflow';
|
||||
|
||||
describe('useViewport.cy.tsx', () => {
|
||||
it('handles drag', () => {
|
||||
const onChangeSpy = cy.spy().as('onChangeSpy');
|
||||
|
||||
cy.mount(
|
||||
<ReactFlow>
|
||||
<HookHelperComponent onChange={onChangeSpy} />
|
||||
</ReactFlow>
|
||||
);
|
||||
|
||||
cy.get('@onChangeSpy').should('have.been.calledWith', { x: 0, y: 0, zoom: 1 });
|
||||
|
||||
cy.dragPane({ from: { x: 50, y: 0 }, to: { x: 50, y: 400 } }).then(() => {
|
||||
cy.get('@onChangeSpy').should('have.been.callCount', 2);
|
||||
cy.get('@onChangeSpy').should('have.been.calledWith', { x: 0, y: 0, zoom: 1 });
|
||||
|
||||
expect(getLatestViewport(onChangeSpy)).to.eql({ x: 0, y: 400, zoom: 1 });
|
||||
});
|
||||
});
|
||||
|
||||
it('handles zoom in', () => {
|
||||
const onChangeSpy = cy.spy().as('onChangeSpy');
|
||||
|
||||
cy.mount(
|
||||
<ReactFlow>
|
||||
<HookHelperComponent onChange={onChangeSpy} />
|
||||
</ReactFlow>
|
||||
);
|
||||
|
||||
cy.zoomPane(-200).then(() => {
|
||||
cy.get('@onChangeSpy').should('have.been.callCount', 2);
|
||||
expect(getLatestViewport(onChangeSpy).zoom).to.be.gt(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('handles zoom out', () => {
|
||||
const onChangeSpy = cy.spy().as('onChangeSpy');
|
||||
|
||||
cy.mount(
|
||||
<ReactFlow>
|
||||
<HookHelperComponent onChange={onChangeSpy} />
|
||||
</ReactFlow>
|
||||
);
|
||||
|
||||
cy.zoomPane(200).then(() => {
|
||||
cy.get('@onChangeSpy').should('have.been.callCount', 2);
|
||||
expect(getLatestViewport(onChangeSpy).zoom).to.be.lt(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('handles default viewport', () => {
|
||||
const defaultViewport = { x: 100, y: 100, zoom: 0.5 };
|
||||
const onChangeSpy = cy.spy().as('onChangeSpy');
|
||||
|
||||
cy.mount(
|
||||
<ReactFlow defaultViewport={defaultViewport}>
|
||||
<HookHelperComponent onChange={onChangeSpy} />
|
||||
</ReactFlow>
|
||||
);
|
||||
|
||||
cy.get('@onChangeSpy').should('have.been.calledWith', defaultViewport);
|
||||
});
|
||||
});
|
||||
|
||||
// test specific helpers
|
||||
|
||||
function HookHelperComponent({ onChange }: { onChange: (vp: Viewport) => void }) {
|
||||
const viewport = useViewport();
|
||||
|
||||
onChange(viewport);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function getLatestViewport(onChangeSpy: any) {
|
||||
return onChangeSpy.lastCall.args[0] as unknown as Viewport;
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
import ReactFlow, { EdgeProps } from 'reactflow';
|
||||
|
||||
import ControlledFlow from '../../support/ControlledFlow';
|
||||
import * as simpleflow from '../../fixtures/simpleflow';
|
||||
|
||||
describe('<ReactFlow />: Basic Props', () => {
|
||||
describe('uses defaultNodes and defaultEdges', () => {
|
||||
beforeEach(() => {
|
||||
cy.mount(<ReactFlow defaultNodes={simpleflow.nodes} defaultEdges={simpleflow.edges} />);
|
||||
});
|
||||
|
||||
it('mounts nodes and edges', () => {
|
||||
cy.get('.react-flow__node').should('have.length', simpleflow.nodes.length);
|
||||
cy.get('.react-flow__edge').should('have.length', simpleflow.edges.length);
|
||||
});
|
||||
|
||||
it('drags a node', () => {
|
||||
const styleBeforeDrag = Cypress.$('.react-flow__node:first').css('transform');
|
||||
|
||||
cy.drag('.react-flow__node:first', { x: 200, y: 25 }).then(($el: JQuery<HTMLElement>) => {
|
||||
const styleAfterDrag = $el.css('transform');
|
||||
expect(styleBeforeDrag).to.not.equal(styleAfterDrag);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('uses nodes and edges', () => {
|
||||
beforeEach(() => {
|
||||
cy.mount(
|
||||
<ControlledFlow
|
||||
addOnNodeChangeHandler={false}
|
||||
initialNodes={simpleflow.nodes}
|
||||
initialEdges={simpleflow.edges}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
it('mounts nodes and edges', () => {
|
||||
cy.get('.react-flow__node').should('have.length', simpleflow.nodes.length);
|
||||
cy.get('.react-flow__edge').should('have.length', simpleflow.edges.length);
|
||||
});
|
||||
|
||||
it('can not drag a node', () => {
|
||||
const styleBeforeDrag = Cypress.$('.react-flow__node:first').css('transform');
|
||||
|
||||
cy.drag('.react-flow__node:first', { x: 200, y: 25 }).then(($el: JQuery<HTMLElement>) => {
|
||||
const styleAfterDrag = $el.css('transform');
|
||||
expect(styleBeforeDrag).to.equal(styleAfterDrag);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('uses onNodesChange handler', () => {
|
||||
beforeEach(() => {
|
||||
cy.mount(<ControlledFlow addOnConnectHandler={false} initialNodes={simpleflow.nodes} />);
|
||||
});
|
||||
|
||||
it('mounts nodes', () => {
|
||||
cy.get('.react-flow__node').should('have.length', simpleflow.nodes.length);
|
||||
cy.get('.react-flow__edge').should('have.length', 0);
|
||||
});
|
||||
|
||||
it('drags a node', () => {
|
||||
const styleBeforeDrag = Cypress.$('.react-flow__node:first').css('transform');
|
||||
|
||||
cy.drag('.react-flow__node:first', { x: 200, y: 25 }).then(($el: JQuery<HTMLElement>) => {
|
||||
const styleAfterDrag = $el.css('transform');
|
||||
expect(styleBeforeDrag).to.not.equal(styleAfterDrag);
|
||||
});
|
||||
});
|
||||
|
||||
it('can not connect nodes', () => {
|
||||
cy.get('.react-flow__node').first().find('.react-flow__handle.source').trigger('mousedown', { button: 0 });
|
||||
|
||||
cy.get('.react-flow__node')
|
||||
.last()
|
||||
.find('.react-flow__handle.target')
|
||||
.trigger('mousemove')
|
||||
.trigger('mouseup', { force: true });
|
||||
|
||||
cy.get('.react-flow__edge').should('have.length', 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('uses onEdgeChange handler', () => {
|
||||
beforeEach(() => {
|
||||
cy.mount(<ControlledFlow initialNodes={simpleflow.nodes} initialEdges={simpleflow.edges} />);
|
||||
});
|
||||
|
||||
it('mounts nodes and edges', () => {
|
||||
cy.get('.react-flow__node').should('have.length', simpleflow.nodes.length);
|
||||
cy.get('.react-flow__edge').should('have.length', simpleflow.edges.length);
|
||||
});
|
||||
|
||||
it('selects an edge', () => {
|
||||
cy.get('.react-flow__edge').first().click().should('have.class', 'selected');
|
||||
});
|
||||
});
|
||||
|
||||
describe('uses onConnect handlers', () => {
|
||||
beforeEach(() => {
|
||||
cy.mount(<ControlledFlow initialNodes={simpleflow.nodes} />);
|
||||
});
|
||||
|
||||
it('mounts nodes', () => {
|
||||
cy.get('.react-flow__node').should('have.length', simpleflow.nodes.length);
|
||||
cy.get('.react-flow__edge').should('have.length', 0);
|
||||
});
|
||||
|
||||
it('connects nodes', () => {
|
||||
cy.get('.react-flow__node').first().find('.react-flow__handle.source').trigger('mousedown', { button: 0 });
|
||||
|
||||
cy.get('.react-flow__node')
|
||||
.last()
|
||||
.find('.react-flow__handle.target')
|
||||
.trigger('mousemove')
|
||||
.trigger('mouseup', { force: true });
|
||||
|
||||
cy.get('.react-flow__edge').should('have.length', 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('uses nodeTypes', () => {
|
||||
it('renders custom nodes', () => {
|
||||
const nodeTypes = {
|
||||
custom: () => <div className="customnode">custom node</div>,
|
||||
};
|
||||
|
||||
const initialNodes = simpleflow.nodes.map((n) => ({
|
||||
...n,
|
||||
type: 'custom',
|
||||
}));
|
||||
|
||||
cy.mount(<ControlledFlow nodeTypes={nodeTypes} initialNodes={initialNodes} />);
|
||||
|
||||
cy.get('.react-flow__node-custom').should('have.length', simpleflow.nodes.length);
|
||||
cy.get('.react-flow').contains('custom node');
|
||||
});
|
||||
|
||||
it('tries invalid node type - should fallback to default', () => {
|
||||
const initialNodes = simpleflow.nodes.map((n) => ({
|
||||
...n,
|
||||
type: 'invalid',
|
||||
}));
|
||||
|
||||
cy.mount(<ControlledFlow initialNodes={initialNodes} />);
|
||||
|
||||
cy.get('.react-flow__node-invalid').should('have.length', 0);
|
||||
cy.get('.react-flow__node-default').should('have.length', simpleflow.nodes.length);
|
||||
});
|
||||
});
|
||||
|
||||
describe('uses edgeTypes', () => {
|
||||
it('renders custom edge', () => {
|
||||
const edgeTypes = {
|
||||
custom: ({ sourceX, sourceY, targetX, targetY }: EdgeProps) => (
|
||||
<path d={`M${sourceX} ${sourceY} L${targetX} ${targetY}`} stroke="black" />
|
||||
),
|
||||
};
|
||||
|
||||
const initialEdges = simpleflow.edges.map((e) => ({
|
||||
...e,
|
||||
type: 'custom',
|
||||
}));
|
||||
|
||||
cy.mount(<ControlledFlow edgeTypes={edgeTypes} initialNodes={simpleflow.nodes} initialEdges={initialEdges} />);
|
||||
|
||||
cy.get('.react-flow__edge-custom').should('have.length', simpleflow.edges.length);
|
||||
});
|
||||
|
||||
it('tries invalid edge type - should fallback to default', () => {
|
||||
const initialEdges = simpleflow.edges.map((e) => ({
|
||||
...e,
|
||||
type: 'invalid',
|
||||
}));
|
||||
|
||||
cy.mount(<ControlledFlow initialNodes={simpleflow.nodes} initialEdges={initialEdges} />);
|
||||
|
||||
cy.get('.react-flow__edge-invalid').should('have.length', 0);
|
||||
cy.get('.react-flow__edge-default').should('have.length', simpleflow.edges.length);
|
||||
});
|
||||
});
|
||||
|
||||
it('uses style', () => {
|
||||
const styles = {
|
||||
backgroundColor: 'red',
|
||||
};
|
||||
|
||||
cy.mount(<ControlledFlow style={styles} />);
|
||||
cy.get('.react-flow').should('have.css', 'background-color', 'rgb(255, 0, 0)');
|
||||
});
|
||||
|
||||
it('uses classname', () => {
|
||||
cy.mount(<ControlledFlow className="custom" />);
|
||||
cy.get('.react-flow').should('have.class', 'custom');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,207 @@
|
||||
import { skipOn } from '@cypress/skip-test';
|
||||
|
||||
import { nodes, edges } from '../../fixtures/simpleflow';
|
||||
import ControlledFlow from '../../support/ControlledFlow';
|
||||
|
||||
describe('<ReactFlow />: Event handlers', () => {
|
||||
describe('Node event handlers', () => {
|
||||
it('handles onInit', () => {
|
||||
const onInitSpy = cy.spy().as('onInitSpy');
|
||||
|
||||
cy.mount(<ControlledFlow initialNodes={nodes} initialEdges={edges} onInit={onInitSpy} />)
|
||||
.wait(100)
|
||||
.then(() => {
|
||||
expect(onInitSpy.callCount).to.be.eq(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('handles onNodeClick', () => {
|
||||
const onNodeClickSpy = cy.spy().as('onNodeClickSpy');
|
||||
|
||||
cy.mount(<ControlledFlow initialNodes={nodes} initialEdges={edges} onNodeClick={onNodeClickSpy} />).then(() => {
|
||||
expect(onNodeClickSpy.callCount).to.be.eq(0);
|
||||
cy.get('.react-flow__node:first')
|
||||
.click()
|
||||
.then(() => {
|
||||
expect(onNodeClickSpy.callCount).to.be.eq(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('handles onNodeDrag handlers', () => {
|
||||
const onNodeDragStart = cy.spy().as('onNodeDragStart');
|
||||
const onNodeDrag = cy.spy().as('onNodeDrag');
|
||||
const onNodeDragStop = cy.spy().as('onNodeDragStop');
|
||||
|
||||
cy.mount(
|
||||
<ControlledFlow
|
||||
initialNodes={nodes}
|
||||
initialEdges={edges}
|
||||
onNodeDragStart={onNodeDragStart}
|
||||
onNodeDrag={onNodeDrag}
|
||||
onNodeDragStop={onNodeDragStop}
|
||||
/>
|
||||
).then(() => {
|
||||
expect(onNodeDragStart.callCount).to.be.eq(0);
|
||||
expect(onNodeDrag.callCount).to.be.eq(0);
|
||||
expect(onNodeDragStop.callCount).to.be.eq(0);
|
||||
|
||||
cy.drag('.react-flow__node:first', { x: 200, y: 0 }).then(() => {
|
||||
expect(onNodeDragStart.callCount).to.be.eq(1);
|
||||
expect(onNodeDrag.callCount).to.be.eq(1);
|
||||
expect(onNodeDragStop.callCount).to.be.eq(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('handles onNodeMouse handlers', () => {
|
||||
const onNodeMouseEnter = cy.spy().as('onNodeMouseEnter');
|
||||
const onNodeMouseMove = cy.spy().as('onNodeMouseMove');
|
||||
const onNodeMouseLeave = cy.spy().as('onNodeMouseLeave');
|
||||
const onNodeContextMenu = cy.spy().as('onNodeContextMenu');
|
||||
const onNodeDoubleClick = cy.spy().as('onNodeDoubleClick');
|
||||
|
||||
cy.mount(
|
||||
<ControlledFlow
|
||||
initialNodes={nodes}
|
||||
initialEdges={edges}
|
||||
onNodeMouseEnter={onNodeMouseEnter}
|
||||
onNodeMouseMove={onNodeMouseMove}
|
||||
onNodeMouseLeave={onNodeMouseLeave}
|
||||
onNodeContextMenu={onNodeContextMenu}
|
||||
onNodeDoubleClick={onNodeDoubleClick}
|
||||
/>
|
||||
)
|
||||
.wait(200)
|
||||
.then(() => {
|
||||
expect(onNodeMouseEnter.callCount).to.be.eq(0);
|
||||
expect(onNodeMouseMove.callCount).to.be.eq(0);
|
||||
expect(onNodeMouseLeave.callCount).to.be.eq(0);
|
||||
expect(onNodeContextMenu.callCount).to.be.eq(0);
|
||||
expect(onNodeDoubleClick.callCount).to.be.eq(0);
|
||||
|
||||
const node = cy.get('.react-flow__node').contains('Node 1');
|
||||
|
||||
node
|
||||
.rightclick()
|
||||
.dblclick()
|
||||
.then(() => {
|
||||
expect(onNodeContextMenu.callCount).to.be.eq(1);
|
||||
expect(onNodeDoubleClick.callCount).to.be.eq(1);
|
||||
});
|
||||
|
||||
skipOn('firefox');
|
||||
|
||||
node
|
||||
.realHover()
|
||||
.realMouseMove(100, 100)
|
||||
.then(() => {
|
||||
expect(onNodeMouseEnter.callCount).to.be.gt(0);
|
||||
expect(onNodeMouseMove.callCount).to.be.gt(0);
|
||||
expect(onNodeMouseLeave.callCount).to.be.gt(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge event handlers', () => {
|
||||
it('handles onEdgeClick', () => {
|
||||
const onEdgeClick = cy.spy().as('onEdgeClick');
|
||||
|
||||
cy.mount(<ControlledFlow initialNodes={nodes} initialEdges={edges} onEdgeClick={onEdgeClick} />).then(() => {
|
||||
expect(onEdgeClick.callCount).to.be.eq(0);
|
||||
cy.get('.react-flow__edge:first')
|
||||
.click()
|
||||
.then(() => {
|
||||
expect(onEdgeClick.callCount).to.be.eq(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('handles onEdgeMouse handlers', () => {
|
||||
const onEdgeMouseEnter = cy.spy().as('onEdgeMouseEnter');
|
||||
const onEdgeMouseMove = cy.spy().as('onEdgeMouseMove');
|
||||
const onEdgeMouseLeave = cy.spy().as('onEdgeMouseLeave');
|
||||
const onEdgeContextMenu = cy.spy().as('onEdgeContextMenu');
|
||||
const onEdgeDoubleClick = cy.spy().as('onEdgedoubleClick');
|
||||
|
||||
cy.mount(
|
||||
<ControlledFlow
|
||||
initialNodes={nodes}
|
||||
initialEdges={edges}
|
||||
onEdgeMouseEnter={onEdgeMouseEnter}
|
||||
onEdgeMouseMove={onEdgeMouseMove}
|
||||
onEdgeMouseLeave={onEdgeMouseLeave}
|
||||
onEdgeContextMenu={onEdgeContextMenu}
|
||||
onEdgeDoubleClick={onEdgeDoubleClick}
|
||||
/>
|
||||
)
|
||||
.wait(200)
|
||||
.then(() => {
|
||||
expect(onEdgeMouseEnter.callCount).to.be.eq(0);
|
||||
expect(onEdgeMouseMove.callCount).to.be.eq(0);
|
||||
expect(onEdgeMouseLeave.callCount).to.be.eq(0);
|
||||
expect(onEdgeContextMenu.callCount).to.be.eq(0);
|
||||
expect(onEdgeDoubleClick.callCount).to.be.eq(0);
|
||||
|
||||
const edge = cy.get('.react-flow__edge:first');
|
||||
|
||||
edge
|
||||
.rightclick()
|
||||
.dblclick()
|
||||
.then(() => {
|
||||
expect(onEdgeContextMenu.callCount).to.be.eq(1);
|
||||
});
|
||||
|
||||
skipOn('firefox');
|
||||
|
||||
edge
|
||||
.realHover()
|
||||
.realMouseMove(100, 100)
|
||||
.then(() => {
|
||||
expect(onEdgeMouseEnter.callCount).to.be.gt(0);
|
||||
expect(onEdgeMouseMove.callCount).to.be.gt(0);
|
||||
expect(onEdgeMouseLeave.callCount).to.be.gt(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Pane event handlers', () => {
|
||||
it('handles onMove handlers', () => {
|
||||
const onMoveStart = cy.spy().as('onMoveStart');
|
||||
const onMove = cy.spy().as('onMove');
|
||||
const onMoveEnd = cy.spy().as('onMoveEnd');
|
||||
|
||||
cy.mount(<ControlledFlow onMoveStart={onMoveStart} onMove={onMove} onMoveEnd={onMoveEnd} />).then(() => {
|
||||
expect(onMoveStart.callCount).to.be.eq(0);
|
||||
expect(onMove.callCount).to.be.eq(0);
|
||||
expect(onMoveEnd.callCount).to.be.eq(0);
|
||||
|
||||
cy.dragPane({ from: { x: 10, y: 200 }, to: { x: 100, y: 200 } }).then(() => {
|
||||
expect(onMoveStart.callCount).to.be.eq(1);
|
||||
expect(onMove.callCount).to.be.gt(0);
|
||||
expect(onMoveEnd.callCount).to.be.eq(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('handles click handlers', () => {
|
||||
const onPaneClick = cy.spy().as('onPaneClick');
|
||||
const onPaneContextMenu = cy.spy().as('onPaneContextMenu');
|
||||
|
||||
cy.mount(<ControlledFlow onPaneClick={onPaneClick} onPaneContextMenu={onPaneContextMenu} />).then(() => {
|
||||
expect(onPaneClick.callCount).to.be.eq(0);
|
||||
expect(onPaneContextMenu.callCount).to.be.eq(0);
|
||||
|
||||
cy.get('.react-flow__pane')
|
||||
.rightclick()
|
||||
.click()
|
||||
.then(() => {
|
||||
expect(onPaneClick.callCount).to.be.eq(1);
|
||||
expect(onPaneContextMenu.callCount).to.be.eq(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import ReactFlow from 'reactflow';
|
||||
|
||||
import { nodes, edges } from '../../fixtures/simpleflow';
|
||||
|
||||
describe('<ReactFlow />: Uncontrolled Flow', () => {
|
||||
beforeEach(() => {
|
||||
cy.mount(<ReactFlow defaultNodes={nodes} defaultEdges={edges} />);
|
||||
});
|
||||
|
||||
it('mounts nodes and edges', () => {
|
||||
cy.get('.react-flow__node').should('have.length', nodes.length);
|
||||
cy.get('.react-flow__edge').should('have.length', edges.length);
|
||||
});
|
||||
|
||||
it('selects a node', () => {
|
||||
cy.get('.react-flow__node').first().click().should('have.class', 'selected');
|
||||
cy.get('.react-flow__pane').click();
|
||||
cy.get('.react-flow__node').first().should('not.have.class', 'selected');
|
||||
});
|
||||
|
||||
it('drags a node', () => {
|
||||
const styleBeforeDrag = Cypress.$('.react-flow__node:first').css('transform');
|
||||
|
||||
cy.drag('.react-flow__node:first', { x: 200, y: 25 }).then(($el: JQuery<HTMLElement>) => {
|
||||
const styleAfterDrag = $el.css('transform');
|
||||
expect(styleBeforeDrag).to.not.equal(styleAfterDrag);
|
||||
});
|
||||
});
|
||||
|
||||
it('selects an edge', () => {
|
||||
cy.get('.react-flow__edge').first().click().should('have.class', 'selected');
|
||||
cy.get('.react-flow__pane').click();
|
||||
cy.get('.react-flow__edge').first().should('not.have.class', 'selected');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,312 @@
|
||||
import ReactFlow, { ReactFlowProps, Viewport, useViewport, SnapGrid, CoordinateExtent, Node } from 'reactflow';
|
||||
|
||||
import ControlledFlow from '../../support/ControlledFlow';
|
||||
import * as simpleflow from '../../fixtures/simpleflow';
|
||||
|
||||
const nodesOutsideOfView = [
|
||||
{
|
||||
...simpleflow.nodes[0],
|
||||
position: {
|
||||
x: -500,
|
||||
y: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
...simpleflow.nodes[1],
|
||||
position: {
|
||||
x: 500,
|
||||
y: 0,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
describe('<ReactFlow />: View Props', () => {
|
||||
it('uses fitView', () => {
|
||||
cy.mount(<Comp nodes={nodesOutsideOfView} fitView minZoom={0.2} />).wait(200);
|
||||
cy.get('.react-flow__node:first').isWithinViewport();
|
||||
});
|
||||
|
||||
it('uses fitViewOptions', () => {
|
||||
const fitViewOptions = { padding: 0.5 };
|
||||
|
||||
cy.mount(<ReactFlow nodes={[nodesOutsideOfView[0]]} fitView fitViewOptions={fitViewOptions} />).wait(200);
|
||||
cy.get('.react-flow__node:first').isWithinViewport();
|
||||
});
|
||||
|
||||
describe('uses minZoom', () => {
|
||||
it('minZoom: 0.2', () => {
|
||||
const onChangeSpy = cy.spy().as('onChangeSpy');
|
||||
|
||||
cy.mount(<Comp minZoom={0.2} onChange={onChangeSpy} />);
|
||||
cy.zoomPane(10000).then(() => {
|
||||
expect(getLatestViewport(onChangeSpy).zoom).to.be.eq(0.2);
|
||||
});
|
||||
});
|
||||
|
||||
it('minZoom: 1', () => {
|
||||
const onChangeSpy = cy.spy().as('onChangeSpy');
|
||||
|
||||
cy.mount(<Comp minZoom={1} onChange={onChangeSpy} />);
|
||||
cy.zoomPane(10000).then(() => {
|
||||
expect(getLatestViewport(onChangeSpy).zoom).to.be.eq(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('sets invalid defaultZoom', () => {
|
||||
const onChangeSpy = cy.spy().as('onChangeSpy');
|
||||
const defaultViewport = { x: 0, y: 0, zoom: 0.2 };
|
||||
|
||||
cy.mount(<Comp minZoom={1} defaultViewport={defaultViewport} onChange={onChangeSpy} />);
|
||||
cy.zoomPane(10000).then(() => {
|
||||
expect(getLatestViewport(onChangeSpy).zoom).to.be.eq(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('uses maxZoom', () => {
|
||||
it('maxZoom: 1', () => {
|
||||
const onChangeSpy = cy.spy().as('onChangeSpy');
|
||||
|
||||
cy.mount(<Comp maxZoom={1} onChange={onChangeSpy} />);
|
||||
cy.zoomPane(-10000).then(() => {
|
||||
expect(getLatestViewport(onChangeSpy).zoom).to.be.eq(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('maxZoom: 2', () => {
|
||||
const onChangeSpy = cy.spy().as('onChangeSpy');
|
||||
|
||||
cy.mount(<Comp maxZoom={2} onChange={onChangeSpy} />);
|
||||
cy.zoomPane(-10000).then(() => {
|
||||
expect(getLatestViewport(onChangeSpy).zoom).to.be.eq(2);
|
||||
});
|
||||
});
|
||||
|
||||
it('sets invalid defaultZoom', () => {
|
||||
const onChangeSpy = cy.spy().as('onChangeSpy');
|
||||
const defaultViewport = { x: 0, y: 0, zoom: 2 };
|
||||
|
||||
cy.mount(<Comp maxZoom={1.5} defaultViewport={defaultViewport} onChange={onChangeSpy} />);
|
||||
cy.zoomPane(-10000).then(() => {
|
||||
expect(getLatestViewport(onChangeSpy).zoom).to.be.eq(1.5);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('uses defaultViewport', () => {
|
||||
it('defaultViewport: { x: 0, y: 0, zoom: 1 }', () => {
|
||||
const onChangeSpy = cy.spy().as('onChangeSpy');
|
||||
const defaultViewport = { x: 0, y: 0, zoom: 1 };
|
||||
|
||||
cy.mount(<Comp nodes={simpleflow.nodes} defaultViewport={defaultViewport} onChange={onChangeSpy} />).then(() => {
|
||||
expect(getLatestViewport(onChangeSpy)).to.be.eql(defaultViewport);
|
||||
});
|
||||
});
|
||||
|
||||
it('defaultViewport: { x: 0, y: 0, zoom: 1.5 }', () => {
|
||||
const onChangeSpy = cy.spy().as('onChangeSpy');
|
||||
const defaultViewport = { x: 0, y: 0, zoom: 1.5 };
|
||||
|
||||
cy.mount(<Comp nodes={simpleflow.nodes} defaultViewport={defaultViewport} onChange={onChangeSpy} />).then(() => {
|
||||
expect(getLatestViewport(onChangeSpy)).to.be.eql(defaultViewport);
|
||||
});
|
||||
});
|
||||
|
||||
it('defaultViewport: { x: 100, y: 100, zoom: 1.5 }', () => {
|
||||
const onChangeSpy = cy.spy().as('onChangeSpy');
|
||||
const defaultViewport = { x: 100, y: 100, zoom: 1.5 };
|
||||
|
||||
cy.mount(<Comp nodes={simpleflow.nodes} defaultViewport={defaultViewport} onChange={onChangeSpy} />).then(() => {
|
||||
expect(getLatestViewport(onChangeSpy)).to.be.eql(defaultViewport);
|
||||
});
|
||||
});
|
||||
|
||||
it('defaultViewport: invalid { x: 0, y: 0, zoom: -1 }', () => {
|
||||
const onChangeSpy = cy.spy().as('onChangeSpy');
|
||||
const defaultViewport = { x: 0, y: 0, zoom: -1 };
|
||||
|
||||
cy.mount(<Comp nodes={simpleflow.nodes} defaultViewport={defaultViewport} onChange={onChangeSpy} />).then(() => {
|
||||
// this should be min zoom because zoom is clamped
|
||||
expect(getLatestViewport(onChangeSpy).zoom).to.be.eql(0.5);
|
||||
});
|
||||
});
|
||||
|
||||
it('defaultViewport: invalid { x: 0, y: 0, zoom: 100 }', () => {
|
||||
const onChangeSpy = cy.spy().as('onChangeSpy');
|
||||
const defaultViewport = { x: 0, y: 0, zoom: 100 };
|
||||
|
||||
cy.mount(<Comp nodes={simpleflow.nodes} defaultViewport={defaultViewport} onChange={onChangeSpy} />).then(() => {
|
||||
// this should be max zoom because zoom is clamped
|
||||
expect(getLatestViewport(onChangeSpy).zoom).to.be.eql(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('uses snapGrid and snapToGrid', () => {
|
||||
const snapGrid: SnapGrid = [100, 100];
|
||||
|
||||
cy.mount(<ControlledFlow initialNodes={simpleflow.nodes} snapGrid={snapGrid} snapToGrid />).then(() => {
|
||||
cy.drag('.react-flow__node:first', { x: 125, y: 125 }).then(($el: any) => {
|
||||
const transformAfterDrag = $el.css('transform');
|
||||
const { m41: nodeX, m42: nodeY } = new DOMMatrixReadOnly(transformAfterDrag);
|
||||
expect([nodeX, nodeY]).to.eql(snapGrid);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('uses onlyRenderVisibleElements', () => {
|
||||
it('displays no nodes', () => {
|
||||
cy.mount(<ControlledFlow initialNodes={nodesOutsideOfView} onlyRenderVisibleElements />).then(() => {
|
||||
cy.get('.react-flow__node').should('have.length', 0);
|
||||
});
|
||||
});
|
||||
|
||||
it('displays one node', () => {
|
||||
const nodes = nodesOutsideOfView.map((n) => {
|
||||
if (n.id === '1') {
|
||||
return { ...n, position: { x: 0, y: 0 } };
|
||||
}
|
||||
return n;
|
||||
});
|
||||
cy.mount(<ControlledFlow initialNodes={nodes} onlyRenderVisibleElements />).then(() => {
|
||||
cy.get('.react-flow__node').should('have.length', 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('uses translateExtent', () => {
|
||||
const translateExtent: CoordinateExtent = [
|
||||
[0, 0],
|
||||
[1000, 1000],
|
||||
];
|
||||
|
||||
cy.mount(<ControlledFlow translateExtent={translateExtent} />).then(() => {
|
||||
const transformBeforeDrag = Cypress.$('.react-flow__viewport').css('transform');
|
||||
|
||||
cy.dragPane({ from: { x: 0, y: 0 }, to: { x: 100, y: 100 } }).then(() => {
|
||||
const transformAfterDrag = Cypress.$('.react-flow__viewport').css('transform');
|
||||
expect(transformBeforeDrag).to.eql(transformAfterDrag);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('uses nodeExtent', () => {
|
||||
const nodes: Node[] = [
|
||||
{
|
||||
id: '1',
|
||||
position: { x: 200, y: 200 },
|
||||
data: { label: '1' },
|
||||
},
|
||||
];
|
||||
|
||||
const nodeExtent: CoordinateExtent = [
|
||||
[0, 0],
|
||||
[50, 50],
|
||||
];
|
||||
|
||||
cy.mount(<ControlledFlow nodes={nodes} nodeExtent={nodeExtent} />)
|
||||
.wait(500)
|
||||
.then(() => {
|
||||
const transform = Cypress.$('.react-flow__node:first').css('transform');
|
||||
const { m41: nodeX, m42: nodeY } = new DOMMatrixReadOnly(transform);
|
||||
expect(nodeX).to.equal(50);
|
||||
expect(nodeY).to.equal(50);
|
||||
});
|
||||
});
|
||||
|
||||
describe('uses preventScrolling', () => {
|
||||
function ScrollFlow({ preventScrolling }: { preventScrolling: boolean }) {
|
||||
return (
|
||||
<div className="wrapper" style={{ height: 2000 }}>
|
||||
<div className="inner" style={{ height: 500 }}>
|
||||
<ControlledFlow preventScrolling={preventScrolling} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
it('lets user scroll', () => {
|
||||
cy.mount(<ScrollFlow preventScrolling={false} />).then(() => {
|
||||
cy.window().then((window) => {
|
||||
cy.get('.react-flow__pane').trigger('wheel', {
|
||||
wheelDelta: 240,
|
||||
wheelDeltaX: 0,
|
||||
wheelDeltaY: 240,
|
||||
eventConstructor: 'WheelEvent',
|
||||
view: window,
|
||||
});
|
||||
|
||||
// @TODO: why is this not working?
|
||||
// it seems that the event is somehow broken. This works fine when using the mouse.
|
||||
// cy.window().its('scrollY').should('be.gt', 0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('does not user scroll', () => {
|
||||
cy.mount(<ScrollFlow preventScrolling={true} />).then(() => {
|
||||
cy.window().then((window) => {
|
||||
cy.get('.react-flow__pane').trigger('wheel', {
|
||||
wheelDelta: 240,
|
||||
wheelDeltaX: 0,
|
||||
wheelDeltaY: 240,
|
||||
eventConstructor: 'WheelEvent',
|
||||
view: window,
|
||||
});
|
||||
|
||||
cy.window().its('scrollY').should('be.equal', 0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('uses attributionPosition', () => {
|
||||
it('displays it on the bottom right', () => {
|
||||
cy.mount(<ControlledFlow />).then(() => {
|
||||
cy.get('.react-flow__attribution').then(($el) => {
|
||||
const { left, top, width, height } = $el[0].getBoundingClientRect();
|
||||
|
||||
expect(left).equal(window.innerWidth - width);
|
||||
expect(top).equal(window.innerHeight - height);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('displays it on the top left', () => {
|
||||
cy.mount(<ControlledFlow attributionPosition="top-left" />).then(() => {
|
||||
cy.get('.react-flow__attribution').then(($el) => {
|
||||
const { left, top } = $el[0].getBoundingClientRect();
|
||||
|
||||
expect(left).equal(0);
|
||||
expect(top).equal(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// test specific helpers
|
||||
|
||||
type CompProps = {
|
||||
onChange?: (vp: Viewport) => void;
|
||||
};
|
||||
|
||||
function InnerComp({ onChange }: CompProps) {
|
||||
const viewport = useViewport();
|
||||
|
||||
onChange?.(viewport);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function Comp({ onChange, ...rest }: CompProps & ReactFlowProps) {
|
||||
return (
|
||||
<ReactFlow {...rest}>
|
||||
<InnerComp onChange={onChange} />
|
||||
</ReactFlow>
|
||||
);
|
||||
}
|
||||
|
||||
function getLatestViewport(onChangeSpy: any) {
|
||||
return onChangeSpy.lastCall.args[0] as unknown as Viewport;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { Node, Edge, isNode, isEdge, getOutgoers, getIncomers, addEdge } from 'reactflow';
|
||||
|
||||
const nodes: Node[] = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'input',
|
||||
data: { label: 'Node 1' },
|
||||
position: { x: 250, y: 5 },
|
||||
},
|
||||
{ id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 } },
|
||||
{ id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 } },
|
||||
{ id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 } },
|
||||
];
|
||||
|
||||
const edges: Edge[] = [
|
||||
{ id: 'e1-2', source: '1', target: '2', animated: true },
|
||||
{ id: 'e1-3', source: '1', target: '3' },
|
||||
{ id: 'e2-3', source: '2', target: '3' },
|
||||
];
|
||||
|
||||
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], nodes, edges);
|
||||
|
||||
expect(outgoers.length).to.be.equal(2);
|
||||
|
||||
const noOutgoers = getOutgoers(nodes[2], nodes, edges);
|
||||
expect(noOutgoers.length).to.be.equal(0);
|
||||
});
|
||||
|
||||
it('tests getIncomers function', () => {
|
||||
const incomers = getIncomers(nodes[2], nodes, edges);
|
||||
expect(incomers.length).to.be.equal(2);
|
||||
|
||||
const noIncomers = getIncomers(nodes[0], nodes, edges);
|
||||
expect(noIncomers.length).to.be.equal(0);
|
||||
});
|
||||
|
||||
describe('tests addEdge function', () => {
|
||||
it('adds edge', () => {
|
||||
const newEdge: Edge = { source: '1', target: '4', id: 'new-edge-1-4' };
|
||||
const nextEdges = addEdge(newEdge, edges);
|
||||
|
||||
expect(nextEdges.length).to.be.equal(edges.length + 1);
|
||||
});
|
||||
|
||||
it('tries to add existing edge', () => {
|
||||
const newEdge: Edge = { source: '2', target: '3', id: 'new-edge-2-3' };
|
||||
const nextEdges = addEdge(newEdge, edges);
|
||||
|
||||
expect(nextEdges.length).to.be.equal(edges.length);
|
||||
});
|
||||
|
||||
it('tries to add invalid edge', () => {
|
||||
// @ts-ignore
|
||||
const newEdge: Edge = { nosource: '1', notarget: '3' };
|
||||
|
||||
try {
|
||||
addEdge(newEdge, edges);
|
||||
} catch (e: any) {
|
||||
console.log(e.message);
|
||||
|
||||
expect(e.message).to.be.equal("Can't create edge. An edge needs a source and a target.");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
export {};
|
||||
@@ -0,0 +1,167 @@
|
||||
describe('Basic Flow Rendering', () => {
|
||||
before(() => {
|
||||
cy.visit('/');
|
||||
});
|
||||
|
||||
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);
|
||||
cy.get('.react-flow__edge').should('have.length', 2);
|
||||
cy.get('.react-flow__node').children('.react-flow__handle');
|
||||
});
|
||||
|
||||
it('renders a grid', () => {
|
||||
cy.get('.react-flow__background');
|
||||
});
|
||||
|
||||
it('selects two nodes by clicks', () => {
|
||||
cy.get('body').type('{cmd}', { release: false });
|
||||
cy.get('.react-flow__node:first')
|
||||
.click()
|
||||
.should('have.class', 'selected')
|
||||
.get('.react-flow__node:last')
|
||||
.click()
|
||||
.should('have.class', 'selected')
|
||||
.get('.react-flow__node:first')
|
||||
.should('have.class', 'selected');
|
||||
cy.get('body').type('{cmd}', { release: true });
|
||||
});
|
||||
|
||||
it('selects a node by click', () => {
|
||||
cy.get('.react-flow__node:first').click({ force: true }).should('have.class', 'selected');
|
||||
});
|
||||
|
||||
it('deselects node', () => {
|
||||
cy.get('.react-flow__renderer').click('bottomLeft');
|
||||
cy.get('.react-flow__node:first').should('not.have.class', 'selected');
|
||||
});
|
||||
|
||||
it('selects an edge by click', () => {
|
||||
cy.get('.react-flow__edge:first').click({ force: true }).should('have.class', 'selected');
|
||||
});
|
||||
|
||||
it('deselects edge', () => {
|
||||
cy.get('.react-flow__renderer').click('bottomLeft');
|
||||
cy.get('.react-flow__edge:first').should('not.have.class', 'selected');
|
||||
});
|
||||
|
||||
it('selects one node with a selection', () => {
|
||||
cy.get('body')
|
||||
.type('{shift}', { release: false })
|
||||
.wait(50)
|
||||
.get('.react-flow__selectionpane')
|
||||
.trigger('mousedown', 1000, 50, { which: 1, force: true })
|
||||
.trigger('mousemove', 1, 400, { which: 1 })
|
||||
.wait(50)
|
||||
.trigger('mouseup', 1, 200, { force: true });
|
||||
|
||||
cy.wait(100);
|
||||
|
||||
cy.get('.react-flow__node').eq(1).should('have.class', 'selected');
|
||||
|
||||
cy.get('.react-flow__node').eq(0).should('have.not.class', 'selected');
|
||||
|
||||
cy.get('.react-flow__nodesselection-rect');
|
||||
|
||||
cy.get('body').type('{shift}', { release: true, force: true });
|
||||
});
|
||||
|
||||
it('selects all nodes', () => {
|
||||
cy.get('body')
|
||||
.type('{shift}', { release: false })
|
||||
.get('.react-flow__selectionpane')
|
||||
.trigger('mousedown', 'topRight', { which: 1, force: true })
|
||||
.trigger('mousemove', 'bottomLeft', { which: 1 })
|
||||
.wait(50)
|
||||
.trigger('mouseup', 'bottomLeft', { force: true })
|
||||
.wait(50)
|
||||
.get('.react-flow__node')
|
||||
.should('have.class', 'selected')
|
||||
.get('.react-flow__nodesselection-rect');
|
||||
|
||||
cy.get('body').type('{shift}', { release: true });
|
||||
});
|
||||
|
||||
it('removes selection', () => {
|
||||
cy.get('.react-flow__renderer').click('bottomLeft');
|
||||
cy.get('.react-flow__nodesselection-rect').should('not.exist');
|
||||
});
|
||||
|
||||
it('selects an edge', () => {
|
||||
cy.get('.react-flow__edge:first').click({ force: true }).should('have.class', 'selected');
|
||||
});
|
||||
|
||||
it('drags a node', () => {
|
||||
const styleBeforeDrag = Cypress.$('.react-flow__node:first').css('transform');
|
||||
|
||||
cy.drag('.react-flow__node:first', { x: 500, y: 25 }).then(($el: any) => {
|
||||
const styleAfterDrag = $el.css('transform');
|
||||
expect(styleBeforeDrag).to.not.equal(styleAfterDrag);
|
||||
});
|
||||
});
|
||||
|
||||
// @TODO: why does this fail since react18?
|
||||
// it('removes a node', () => {
|
||||
// cy.get('.react-flow__node').contains('Node 1').click().should('have.class', 'selected');
|
||||
// cy.get('html').type('{backspace}').wait(100);
|
||||
// cy.get('.react-flow__node').should('have.length', 3);
|
||||
// cy.get('.react-flow__edge').should('have.length', 1);
|
||||
// });
|
||||
|
||||
it('connects nodes', () => {
|
||||
cy.get('.react-flow__node')
|
||||
.contains('Node 3')
|
||||
.find('.react-flow__handle.source')
|
||||
.trigger('mousedown', { button: 0 });
|
||||
|
||||
cy.get('.react-flow__node')
|
||||
.contains('Node 4')
|
||||
.find('.react-flow__handle.target')
|
||||
.trigger('mousemove', { force: true })
|
||||
.wait(50)
|
||||
.trigger('mouseup', { force: true });
|
||||
|
||||
cy.get('.react-flow__edge').should('have.length', 3);
|
||||
});
|
||||
|
||||
// @TODO: why does this fail since react18?
|
||||
// it('removes an edge', () => {
|
||||
// cy.get('.react-flow__edge:first').click();
|
||||
// cy.get('body').type('{backspace}');
|
||||
|
||||
// cy.get('.react-flow__edge').should('have.length', 1);
|
||||
// });
|
||||
|
||||
it('drags the pane', () => {
|
||||
const styleBeforeDrag = Cypress.$('.react-flow__viewport').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__pane')
|
||||
.trigger('mousedown', 'topLeft', { which: 1, view: win })
|
||||
.trigger('mousemove', 'bottomLeft')
|
||||
.wait(50)
|
||||
.trigger('mouseup', { force: true, view: win })
|
||||
.then(() => {
|
||||
const styleAfterDrag = Cypress.$('.react-flow__viewport').css('transform');
|
||||
expect(styleBeforeDrag).to.not.equal(styleAfterDrag);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('zooms the pane', () => {
|
||||
const styleBeforeZoom = Cypress.$('.react-flow__viewport').css('transform');
|
||||
|
||||
cy.get('.react-flow__pane')
|
||||
.trigger('wheel', 'topLeft', { deltaY: -200 })
|
||||
.wait(50)
|
||||
.then(() => {
|
||||
const styleAfterZoom = Cypress.$('.react-flow__viewport').css('transform');
|
||||
expect(styleBeforeZoom).to.not.equal(styleAfterZoom);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
export {};
|
||||
@@ -0,0 +1,83 @@
|
||||
describe('Controls Testing', () => {
|
||||
before(() => {
|
||||
cy.visit('/');
|
||||
});
|
||||
|
||||
it('renders the control panel', () => {
|
||||
cy.get('.react-flow__controls');
|
||||
});
|
||||
|
||||
it('zooms in', () => {
|
||||
const styleBeforeZoom = Cypress.$('.react-flow__viewport').css('transform');
|
||||
|
||||
cy.get('.react-flow__controls-zoomin')
|
||||
.click()
|
||||
.then(() => {
|
||||
const styleAfterZoom = Cypress.$('.react-flow__viewport').css('transform');
|
||||
expect(styleBeforeZoom).to.not.equal(styleAfterZoom);
|
||||
});
|
||||
});
|
||||
|
||||
it('zooms out', () => {
|
||||
const styleBeforeZoom = Cypress.$('.react-flow__viewport').css('transform');
|
||||
|
||||
cy.get('.react-flow__controls-zoomout')
|
||||
.click()
|
||||
.then(() => {
|
||||
const styleAfterZoom = Cypress.$('.react-flow__viewport').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__viewport').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__renderer')
|
||||
.trigger('mousedown', 'topLeft', { which: 1, view: win })
|
||||
.trigger('mousemove', 10, 400)
|
||||
.wait(50)
|
||||
.trigger('mouseup', 10, 400, { force: true, view: win })
|
||||
.then(() => {
|
||||
const styleAfterDrag = Cypress.$('.react-flow__viewport').css('transform');
|
||||
expect(styleBeforeDrag).to.not.equal(styleAfterDrag);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('fits view', () => {
|
||||
const styleBeforeZoom = Cypress.$('.react-flow__viewport').css('transform');
|
||||
|
||||
cy.get('.react-flow__controls-fitview')
|
||||
.click()
|
||||
.then(() => {
|
||||
const styleAfterZoom = Cypress.$('.react-flow__viewport').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__pane').click('topLeft');
|
||||
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({ force: true }).should('have.class', 'selected');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
export {};
|
||||
@@ -0,0 +1,32 @@
|
||||
describe('DragHandle Flow Rendering', () => {
|
||||
before(() => {
|
||||
cy.visit('/DragHandle');
|
||||
});
|
||||
|
||||
it('renders a flow with a node', () => {
|
||||
cy.get('.react-flow__renderer');
|
||||
cy.get('.react-flow__node').should('have.length', 1);
|
||||
});
|
||||
|
||||
it('tries to drag a node', () => {
|
||||
const $nodeElement = Cypress.$('.react-flow__node:first');
|
||||
const styleBeforeDrag = $nodeElement.css('transform');
|
||||
|
||||
cy.drag('.react-flow__node:first', { x: 500, y: 500 }).then(() => {
|
||||
const styleAfterDrag = $nodeElement.css('transform');
|
||||
expect(styleBeforeDrag).to.be.equal(styleAfterDrag);
|
||||
});
|
||||
});
|
||||
|
||||
it('drags a node', () => {
|
||||
const $nodeElement = Cypress.$('.react-flow__node:first');
|
||||
const styleBeforeDrag = $nodeElement.css('transform');
|
||||
|
||||
cy.drag('.custom-drag-handle:first', { x: 500, y: 500 }).then(() => {
|
||||
const styleAfterDrag = $nodeElement.css('transform');
|
||||
expect(styleBeforeDrag).to.not.equal(styleAfterDrag);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
export {};
|
||||
@@ -0,0 +1,44 @@
|
||||
describe('Empty Flow Rendering', () => {
|
||||
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');
|
||||
});
|
||||
|
||||
it('renders empty selection', () => {
|
||||
cy.get('.react-flow__renderer').click();
|
||||
cy.get('body')
|
||||
.type('{shift}', { release: false })
|
||||
.wait(50)
|
||||
.get('.react-flow__selectionpane')
|
||||
.trigger('mousedown', 400, 50, { which: 1, force: true })
|
||||
.trigger('mousemove', 200, 200, { which: 1 })
|
||||
.wait(50)
|
||||
.trigger('mouseup', 200, 200, { force: true });
|
||||
|
||||
cy.get('body').type('{shift}', { release: true });
|
||||
});
|
||||
|
||||
it('adds two nodes', () => {
|
||||
cy.contains('add node').click();
|
||||
cy.contains('add node').click();
|
||||
});
|
||||
|
||||
it('connects nodes', () => {
|
||||
cy.get('.react-flow__node').first().find('.react-flow__handle.source').trigger('mousedown', { button: 0 });
|
||||
|
||||
cy.get('.react-flow__node')
|
||||
.last()
|
||||
.find('.react-flow__handle.target')
|
||||
.trigger('mousemove')
|
||||
.trigger('mouseup', { force: true });
|
||||
|
||||
cy.get('.react-flow__edge').should('have.length', 1);
|
||||
});
|
||||
});
|
||||
|
||||
export {};
|
||||
@@ -0,0 +1,34 @@
|
||||
describe('Hidden Flow Rendering', () => {
|
||||
before(() => {
|
||||
cy.visit('/Hidden');
|
||||
});
|
||||
|
||||
it('renders empty flow', () => {
|
||||
cy.get('.react-flow__node').should('not.exist');
|
||||
cy.get('.react-flow__edge').should('not.exist');
|
||||
cy.get('.react-flow__minimap-node').should('not.exist');
|
||||
});
|
||||
|
||||
it('toggles isHidden mode', () => {
|
||||
cy.get('.react-flow__ishidden').click();
|
||||
});
|
||||
|
||||
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);
|
||||
cy.get('.react-flow__minimap-node').should('have.length', 4);
|
||||
});
|
||||
|
||||
it('toggles isHidden mode again', () => {
|
||||
cy.get('.react-flow__ishidden').click();
|
||||
});
|
||||
|
||||
it('renders empty flow', () => {
|
||||
cy.get('.react-flow__node').should('not.exist');
|
||||
cy.get('.react-flow__edge').should('not.exist');
|
||||
cy.get('.react-flow__minimap-node').should('not.exist');
|
||||
});
|
||||
});
|
||||
|
||||
export {};
|
||||
@@ -0,0 +1,153 @@
|
||||
describe('Interaction Flow Rendering', () => {
|
||||
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);
|
||||
cy.get('.react-flow__node').children('.react-flow__handle');
|
||||
});
|
||||
|
||||
it('tries to select a node by click', () => {
|
||||
const pointerEvents = Cypress.$('.react-flow__node:first').css('pointer-events');
|
||||
expect(pointerEvents).to.equal('none');
|
||||
});
|
||||
|
||||
it('tries to select an edge by click', () => {
|
||||
const pointerEvents = Cypress.$('.react-flow__edge:first').css('pointer-events');
|
||||
expect(pointerEvents).to.equal('none');
|
||||
});
|
||||
|
||||
it('toggles on capture element click', () => {
|
||||
cy.get('.react-flow__captureelementclick').click();
|
||||
});
|
||||
|
||||
it('allows node clicks when enabled', () => {
|
||||
const pointerEvents = Cypress.$('.react-flow__node:first').css('pointer-events');
|
||||
expect(pointerEvents).to.equal('all');
|
||||
});
|
||||
|
||||
it('allows edge clicks when enabled', () => {
|
||||
const pointerEvents = Cypress.$('.react-flow__edge:first').css('pointer-events');
|
||||
expect(pointerEvents.toLowerCase()).to.equal('visiblestroke');
|
||||
});
|
||||
|
||||
it('tries to do a selection', () => {
|
||||
cy.get('body').type('{shift}', { release: false }).get('.react-flow__selectionpane').should('not.exist');
|
||||
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 double click', () => {
|
||||
const styleBeforeZoom = Cypress.$('.react-flow__nodes').css('transform');
|
||||
|
||||
cy.get('.react-flow__renderer')
|
||||
.dblclick()
|
||||
.then(() => {
|
||||
const styleAfterZoom = Cypress.$('.react-flow__nodes').css('transform');
|
||||
expect(styleBeforeZoom).to.equal(styleAfterZoom);
|
||||
});
|
||||
});
|
||||
|
||||
it('tries to zoom by scroll', () => {
|
||||
const styleBeforeZoom = Cypress.$('.react-flow__nodes').css('transform');
|
||||
|
||||
cy.get('.react-flow__renderer')
|
||||
.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();
|
||||
});
|
||||
|
||||
it('drags a node', () => {
|
||||
const styleBeforeDrag = Cypress.$('.react-flow__node:first').css('transform');
|
||||
|
||||
cy.drag('.react-flow__node:first', { x: 325, y: 100 }).then(($el) => {
|
||||
const styleAfterDrag = $el.css('transform');
|
||||
expect(styleBeforeDrag).to.not.equal(styleAfterDrag);
|
||||
});
|
||||
});
|
||||
|
||||
it('toggles selectable mode', () => {
|
||||
cy.get('.react-flow__selectable').click();
|
||||
});
|
||||
|
||||
it('selects a node by click', () => {
|
||||
cy.get('.react-flow__node:first').click().should('have.class', 'selected');
|
||||
});
|
||||
|
||||
it('selects an edge by click', () => {
|
||||
cy.get('.react-flow__edge:first').click({ force: true });
|
||||
cy.get('.react-flow__edge:first').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', { button: 0 });
|
||||
|
||||
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__viewport').css('transform');
|
||||
|
||||
cy.get('.react-flow__pane')
|
||||
.trigger('wheel', 'topLeft', { deltaY: 200 })
|
||||
.wait(50)
|
||||
.then(() => {
|
||||
const styleAfterZoom = Cypress.$('.react-flow__viewport').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', () => {
|
||||
cy.get('.react-flow__controls-zoomout').click();
|
||||
const styleBeforeZoom = Cypress.$('.react-flow__viewport').css('transform');
|
||||
|
||||
cy.get('.react-flow__pane')
|
||||
.dblclick()
|
||||
.wait(50)
|
||||
.then(() => {
|
||||
const styleAfterZoom = Cypress.$('.react-flow__viewport').css('transform');
|
||||
expect(styleBeforeZoom).not.to.equal(styleAfterZoom);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
export {};
|
||||
@@ -0,0 +1,75 @@
|
||||
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;
|
||||
|
||||
cy.wait(200);
|
||||
|
||||
const minimapNodes = Cypress.$('.react-flow__minimap-node').length;
|
||||
|
||||
expect(paneNodes).equal(minimapNodes);
|
||||
});
|
||||
|
||||
it('changes zoom level', () => {
|
||||
const viewBoxBeforeZoom = Cypress.$('.react-flow__minimap svg').attr('viewBox');
|
||||
const maskPathBeforeZoom = Cypress.$('.react-flow__minimap-mask').attr('d');
|
||||
|
||||
cy.get('.react-flow__pane')
|
||||
.trigger('wheel', 'topLeft', { deltaY: -200 })
|
||||
.wait(50)
|
||||
.then(() => {
|
||||
const viewBoxAfterZoom = Cypress.$('.react-flow__minimap svg').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');
|
||||
|
||||
cy.drag('.react-flow__node:first', { x: 500, y: 25 })
|
||||
.wait(100)
|
||||
.then(() => {
|
||||
const xPosAfterDrag = Cypress.$('.react-flow__minimap-node:first').attr('x');
|
||||
const yPosAfterDrag = Cypress.$('.react-flow__minimap-node:first').attr('y');
|
||||
|
||||
expect(xPosBeforeDrag).to.not.equal(xPosAfterDrag);
|
||||
expect(yPosBeforeDrag).to.not.equal(yPosAfterDrag);
|
||||
});
|
||||
});
|
||||
|
||||
it('changes node positions via pane drag', () => {
|
||||
const viewBoxBeforeDrag = Cypress.$('.react-flow__minimap svg').attr('viewBox');
|
||||
const maskPathBeforeDrag = Cypress.$('.react-flow__minimap-mask').attr('d');
|
||||
|
||||
// for d3 we have to pass the window to the event
|
||||
// https://github.com/cypress-io/cypress/issues/3441
|
||||
cy.window().then((win) => {
|
||||
cy.get('.react-flow__pane')
|
||||
.trigger('mousedown', 'topLeft', { which: 1, view: win })
|
||||
.trigger('mousemove', 'bottomLeft')
|
||||
.wait(50)
|
||||
.trigger('mouseup', { force: true, view: win })
|
||||
.then(() => {
|
||||
const viewBoxAfterDrag = Cypress.$('.react-flow__minimap svg').attr('viewBox');
|
||||
const maskPathAfterDrag = Cypress.$('.react-flow__minimap-mask').attr('d');
|
||||
|
||||
expect(viewBoxBeforeDrag).to.not.equal(viewBoxAfterDrag);
|
||||
expect(maskPathBeforeDrag).to.not.equal(maskPathAfterDrag);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
export {};
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Node, Edge } from 'reactflow';
|
||||
|
||||
export const nodes: Node[] = [
|
||||
{
|
||||
id: '1',
|
||||
data: { label: 'Node 1' },
|
||||
position: { x: 0, y: 0 },
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
data: { label: 'Node 2' },
|
||||
position: { x: 200, y: 200 },
|
||||
},
|
||||
];
|
||||
|
||||
export const edges: Edge[] = [
|
||||
{
|
||||
id: 'e1',
|
||||
source: '1',
|
||||
target: '2',
|
||||
},
|
||||
];
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 110 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 104 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 121 KiB |
@@ -0,0 +1,65 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import {
|
||||
ReactFlow,
|
||||
Node,
|
||||
Edge,
|
||||
NodeChange,
|
||||
EdgeChange,
|
||||
applyNodeChanges,
|
||||
applyEdgeChanges,
|
||||
Connection,
|
||||
addEdge,
|
||||
ReactFlowProps,
|
||||
} from 'reactflow';
|
||||
|
||||
function ControlledFlow({
|
||||
addOnNodeChangeHandler = true,
|
||||
addOnEdgeChangeHandler = true,
|
||||
addOnConnectHandler = true,
|
||||
initialNodes = [],
|
||||
initialEdges = [],
|
||||
...rest
|
||||
}: {
|
||||
initialNodes?: Node[];
|
||||
initialEdges?: Edge[];
|
||||
addOnNodeChangeHandler?: boolean;
|
||||
addOnEdgeChangeHandler?: boolean;
|
||||
addOnConnectHandler?: boolean;
|
||||
} & Partial<ReactFlowProps>) {
|
||||
const [nodes, setNodes] = useState<Node[]>(initialNodes);
|
||||
const [edges, setEdges] = useState<Edge[]>(initialEdges);
|
||||
|
||||
const onNodesChange = useCallback(
|
||||
(changes: NodeChange[]) => setNodes((nds) => applyNodeChanges(changes, nds)),
|
||||
[setNodes]
|
||||
);
|
||||
|
||||
const onEdgesChange = useCallback(
|
||||
(changes: EdgeChange[]) => setEdges((eds) => applyEdgeChanges(changes, eds)),
|
||||
[setEdges]
|
||||
);
|
||||
|
||||
const onConnect = useCallback((params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
|
||||
|
||||
const handlers: {
|
||||
onNodesChange?: (changes: NodeChange[]) => void;
|
||||
onEdgesChange?: (changes: EdgeChange[]) => void;
|
||||
onConnect?: (params: Connection | Edge) => void;
|
||||
} = {};
|
||||
|
||||
if (addOnNodeChangeHandler) {
|
||||
handlers.onNodesChange = onNodesChange;
|
||||
}
|
||||
|
||||
if (addOnEdgeChangeHandler) {
|
||||
handlers.onEdgesChange = onEdgesChange;
|
||||
}
|
||||
|
||||
if (addOnConnectHandler) {
|
||||
handlers.onConnect = onConnect;
|
||||
}
|
||||
|
||||
return <ReactFlow nodes={nodes} edges={edges} {...handlers} {...rest} />;
|
||||
}
|
||||
|
||||
export default ControlledFlow;
|
||||
@@ -0,0 +1,58 @@
|
||||
Cypress.Commands.add('drag', (selector, { x, y }) =>
|
||||
cy.window().then((window) => {
|
||||
const elementToDrag = cy.get(selector as string);
|
||||
return elementToDrag.then(($el) => {
|
||||
const { left, top, width, height } = $el[0].getBoundingClientRect();
|
||||
const centerX = left + width / 2;
|
||||
const centerY = top + height / 2;
|
||||
const nextX: number = centerX + x;
|
||||
const nextY: number = centerY + y;
|
||||
|
||||
return elementToDrag
|
||||
.trigger('mousedown', { view: window })
|
||||
.trigger('mousemove', nextX, nextY, { force: true })
|
||||
.wait(50)
|
||||
.trigger('mouseup', { view: window, force: true });
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
Cypress.Commands.add('dragPane', ({ from, to }) =>
|
||||
cy
|
||||
.window()
|
||||
.then((window) =>
|
||||
cy
|
||||
.get('.react-flow__pane')
|
||||
.trigger('mousedown', from.x, from.y, { view: window })
|
||||
.trigger('mousemove', to.x, to.y)
|
||||
.trigger('mouseup', { force: true, view: window })
|
||||
)
|
||||
);
|
||||
|
||||
Cypress.Commands.add('zoomPane', (wheelDelta: number) =>
|
||||
cy.get('.react-flow__pane').trigger('wheel', 'center', { deltaY: wheelDelta }).wait(250)
|
||||
);
|
||||
|
||||
Cypress.Commands.add('isWithinViewport', { prevSubject: true }, (subject) => {
|
||||
const rect = subject[0].getBoundingClientRect();
|
||||
|
||||
expect(rect.top).to.be.within(0, window.innerHeight);
|
||||
expect(rect.right).to.be.within(0, window.innerWidth);
|
||||
expect(rect.bottom).to.be.within(0, window.innerHeight);
|
||||
expect(rect.left).to.be.within(0, window.innerWidth);
|
||||
|
||||
return subject;
|
||||
});
|
||||
|
||||
Cypress.Commands.add('isOutsideViewport', { prevSubject: true }, (subject) => {
|
||||
const rect = subject[0].getBoundingClientRect();
|
||||
|
||||
expect(rect.top).not.to.be.within(0, window.innerHeight);
|
||||
expect(rect.right).not.to.be.within(0, window.innerWidth);
|
||||
expect(rect.bottom).not.to.be.within(0, window.innerHeight);
|
||||
expect(rect.left).not.to.be.within(0, window.innerWidth);
|
||||
|
||||
return subject;
|
||||
});
|
||||
|
||||
export {};
|
||||
@@ -0,0 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||
<title>Components App</title>
|
||||
<style>
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div data-cy-root id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,27 @@
|
||||
///<reference types="cypress" />
|
||||
///<reference types="@cypress/skip-test" />
|
||||
///<reference types="cypress-real-events" />
|
||||
|
||||
import './commands';
|
||||
import 'cypress-real-events/support';
|
||||
|
||||
import { mount } from 'cypress/react18';
|
||||
import { XYPosition } from 'reactflow';
|
||||
|
||||
import 'reactflow/dist/style.css';
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
mount: typeof mount;
|
||||
drag: (selector: string, toPosition: XYPosition) => Cypress.Chainable<JQuery<HTMLElement>>;
|
||||
dragPane: ({ from, to }: { from: XYPosition; to: XYPosition }) => Cypress.Chainable<JQuery<HTMLElement>>;
|
||||
zoomPane: (wheelDelta: number) => Cypress.Chainable<JQuery<HTMLElement>>;
|
||||
isWithinViewport: () => Cypress.Chainable<JQuery<HTMLElement>>;
|
||||
isOutsideViewport: () => Cypress.Chainable<JQuery<HTMLElement>>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Cypress.Commands.add('mount', mount);
|
||||
@@ -0,0 +1,25 @@
|
||||
// ***********************************************************
|
||||
// This example support/index.js is processed and
|
||||
// loaded automatically before your test files.
|
||||
//
|
||||
// This is a great place to put global configuration and
|
||||
// behavior that modifies Cypress.
|
||||
//
|
||||
// You can change the location of this file or turn off
|
||||
// automatically serving support files with the
|
||||
// 'supportFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import './commands';
|
||||
|
||||
const resizeObserverLoopErrRe = /^[^(ResizeObserver loop limit exceeded)]/;
|
||||
|
||||
Cypress.on('uncaught:exception', (err) => {
|
||||
if (resizeObserverLoopErrRe.test(err.message)) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React + TS</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "vite-app",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --port 3000",
|
||||
"test": "pnpm test-component",
|
||||
"test-component": "cypress run --component",
|
||||
"test-e2e": "vite serve & cypress run --e2e"
|
||||
},
|
||||
"dependencies": {
|
||||
"dagre": "^0.8.5",
|
||||
"localforage": "^1.10.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.3.0",
|
||||
"reactflow": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cypress/skip-test": "^2.6.1",
|
||||
"@types/react": "^18.0.17",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@vitejs/plugin-react": "^2.1.0",
|
||||
"cypress": "^10.6.0",
|
||||
"cypress-real-events": "^1.7.1",
|
||||
"typescript": "^4.6.4",
|
||||
"vite": "^3.1.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,194 @@
|
||||
import { ChangeEvent } from 'react';
|
||||
import { BrowserRouter, Route, Routes, useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
import Basic from '../examples/Basic';
|
||||
import ControlledUncontrolled from '../examples/ControlledUncontrolled';
|
||||
import CustomConnectionLine from '../examples/CustomConnectionLine';
|
||||
import CustomNode from '../examples/CustomNode';
|
||||
import DefaultNodes from '../examples/DefaultNodes';
|
||||
import DragHandle from '../examples/DragHandle';
|
||||
import DragNDrop from '../examples/DragNDrop';
|
||||
import Edges from '../examples/Edges';
|
||||
import EdgeTypes from '../examples/EdgeTypes';
|
||||
import Empty from '../examples/Empty';
|
||||
import FloatingEdges from '../examples/FloatingEdges';
|
||||
import Hidden from '../examples/Hidden';
|
||||
import Interaction from '../examples/Interaction';
|
||||
import Layouting from '../examples/Layouting';
|
||||
import MultiFlows from '../examples/MultiFlows';
|
||||
import NestedNodes from '../examples/NestedNodes';
|
||||
import NodeTypeChange from '../examples/NodeTypeChange';
|
||||
import NodeTypesObjectChange from '../examples/NodeTypesObjectChange';
|
||||
import Overview from '../examples/Overview';
|
||||
import Provider from '../examples/Provider';
|
||||
import SaveRestore from '../examples/SaveRestore';
|
||||
import Stress from '../examples/Stress';
|
||||
import Subflow from '../examples/Subflow';
|
||||
import SwitchFlow from '../examples/Switch';
|
||||
import TouchDevice from '../examples/TouchDevice';
|
||||
import Undirectional from '../examples/Undirectional';
|
||||
import UpdatableEdge from '../examples/UpdatableEdge';
|
||||
import UpdateNode from '../examples/UpdateNode';
|
||||
import UseUpdateNodeInternals from '../examples/UseUpdateNodeInternals';
|
||||
import UseReactFlow from '../examples/UseReactFlow';
|
||||
import Validation from '../examples/Validation';
|
||||
import UseKeyPress from '../examples/UseKeyPress';
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
component: Basic,
|
||||
},
|
||||
{
|
||||
path: '/default-nodes',
|
||||
component: DefaultNodes,
|
||||
},
|
||||
{
|
||||
path: '/custom-connectionline',
|
||||
component: CustomConnectionLine,
|
||||
},
|
||||
{
|
||||
path: '/custom-node',
|
||||
component: CustomNode,
|
||||
},
|
||||
{
|
||||
path: '/draghandle',
|
||||
component: DragHandle,
|
||||
},
|
||||
{
|
||||
path: '/dragndrop',
|
||||
component: DragNDrop,
|
||||
},
|
||||
{
|
||||
path: '/edges',
|
||||
component: Edges,
|
||||
},
|
||||
{
|
||||
path: '/edge-types',
|
||||
component: EdgeTypes,
|
||||
},
|
||||
{
|
||||
path: '/empty',
|
||||
component: Empty,
|
||||
},
|
||||
{
|
||||
path: '/floating-edges',
|
||||
component: FloatingEdges,
|
||||
},
|
||||
{
|
||||
path: '/hidden',
|
||||
component: Hidden,
|
||||
},
|
||||
{
|
||||
path: '/interaction',
|
||||
component: Interaction,
|
||||
},
|
||||
{
|
||||
path: '/layouting',
|
||||
component: Layouting,
|
||||
},
|
||||
{
|
||||
path: '/multiflows',
|
||||
component: MultiFlows,
|
||||
},
|
||||
{
|
||||
path: '/nested-nodes',
|
||||
component: NestedNodes,
|
||||
},
|
||||
{
|
||||
path: '/nodetype-change',
|
||||
component: NodeTypeChange,
|
||||
},
|
||||
{
|
||||
path: '/nodetypesobject-change',
|
||||
component: NodeTypesObjectChange,
|
||||
},
|
||||
{
|
||||
path: '/provider',
|
||||
component: Provider,
|
||||
},
|
||||
{
|
||||
path: '/save-restore',
|
||||
component: SaveRestore,
|
||||
},
|
||||
{
|
||||
path: '/stress',
|
||||
component: Stress,
|
||||
},
|
||||
{
|
||||
path: '/subflow',
|
||||
component: Subflow,
|
||||
},
|
||||
{
|
||||
path: '/switch',
|
||||
component: SwitchFlow,
|
||||
},
|
||||
{
|
||||
path: '/touch-device',
|
||||
component: TouchDevice,
|
||||
},
|
||||
{
|
||||
path: '/undirectional',
|
||||
component: Undirectional,
|
||||
},
|
||||
{
|
||||
path: '/updatable-edge',
|
||||
component: UpdatableEdge,
|
||||
},
|
||||
{
|
||||
path: '/update-node',
|
||||
component: UpdateNode,
|
||||
},
|
||||
{
|
||||
path: '/usereactflow',
|
||||
component: UseReactFlow,
|
||||
},
|
||||
{
|
||||
path: '/useupdatenodeinternals',
|
||||
component: UseUpdateNodeInternals,
|
||||
},
|
||||
{
|
||||
path: '/validation',
|
||||
component: Validation,
|
||||
},
|
||||
{
|
||||
path: '/controlled-uncontrolled',
|
||||
component: ControlledUncontrolled,
|
||||
},
|
||||
{
|
||||
path: '/use-key-press',
|
||||
component: UseKeyPress,
|
||||
},
|
||||
];
|
||||
|
||||
const Header = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const onChange = (event: ChangeEvent<HTMLSelectElement>) => navigate(event.target.value);
|
||||
|
||||
return (
|
||||
<header>
|
||||
<a className="logo" href="https://github.com/wbkd/react-flow">
|
||||
React Flow Dev
|
||||
</a>
|
||||
<select defaultValue={location.pathname} onChange={onChange}>
|
||||
{routes.map((route) => (
|
||||
<option value={route.path} key={route.path}>
|
||||
{route.path === '/' ? 'overview' : route.path.substring(1, route.path.length)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
export default () => (
|
||||
<BrowserRouter>
|
||||
<Header />
|
||||
<Routes>
|
||||
{routes.map((route) => (
|
||||
<Route path={route.path} key={route.path} element={<route.component />} />
|
||||
))}
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
);
|
||||
@@ -0,0 +1,42 @@
|
||||
import React, { FC } from 'react';
|
||||
|
||||
import ReactFlow, {
|
||||
Node,
|
||||
ReactFlowProvider,
|
||||
useNodesState,
|
||||
Background,
|
||||
BackgroundProps,
|
||||
BackgroundVariant,
|
||||
} from 'reactflow';
|
||||
|
||||
import styles from './style.module.css';
|
||||
|
||||
const initialNodes: Node[] = [
|
||||
{
|
||||
id: '1',
|
||||
data: { label: 'Node 1' },
|
||||
position: { x: 50, y: 50 },
|
||||
},
|
||||
];
|
||||
|
||||
const Flow: FC<{ id: string; bgProps: BackgroundProps }> = ({ id, bgProps }) => {
|
||||
const [nodes, , onNodesChange] = useNodesState(initialNodes);
|
||||
|
||||
return (
|
||||
<ReactFlowProvider>
|
||||
<ReactFlow nodes={nodes} onNodesChange={onNodesChange} id={id}>
|
||||
<Background {...bgProps} />
|
||||
</ReactFlow>
|
||||
</ReactFlowProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const Backgrounds: FC = () => (
|
||||
<div className={styles.wrapper}>
|
||||
<Flow id="flow-a" bgProps={{ variant: BackgroundVariant.Dots }} />
|
||||
<Flow id="flow-b" bgProps={{ variant: BackgroundVariant.Lines, gap: [50, 50] }} />
|
||||
<Flow id="flow-c" bgProps={{ variant: BackgroundVariant.Cross, gap: [100, 50] }} />
|
||||
</div>
|
||||
);
|
||||
|
||||
export default Backgrounds;
|
||||
@@ -0,0 +1,13 @@
|
||||
.wrapper {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.wrapper :global .react-flow {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.wrapper :global .react-flow {
|
||||
border-right: 1px solid #ddd;
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import React, { MouseEvent } from 'react';
|
||||
import ReactFlow, {
|
||||
MiniMap,
|
||||
Background,
|
||||
BackgroundVariant,
|
||||
Controls,
|
||||
ReactFlowProvider,
|
||||
Node,
|
||||
Edge,
|
||||
useReactFlow,
|
||||
} from 'reactflow';
|
||||
|
||||
const onNodeDrag = (_: MouseEvent, node: Node) => console.log('drag', node);
|
||||
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
|
||||
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
|
||||
|
||||
const initialNodes: Node[] = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'input',
|
||||
data: { label: 'Node 1' },
|
||||
position: { x: 250, y: 5 },
|
||||
className: 'light',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
data: { label: 'Node 2' },
|
||||
position: { x: 100, y: 100 },
|
||||
className: 'light',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
data: { label: 'Node 3' },
|
||||
position: { x: 400, y: 100 },
|
||||
className: 'light',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
data: { label: 'Node 4' },
|
||||
position: { x: 400, y: 200 },
|
||||
className: 'light',
|
||||
},
|
||||
];
|
||||
|
||||
const initialEdges: Edge[] = [
|
||||
{ id: 'e1-2', source: '1', target: '2', animated: true },
|
||||
{ id: 'e1-3', source: '1', target: '3' },
|
||||
];
|
||||
|
||||
const defaultEdgeOptions = { zIndex: 0 };
|
||||
|
||||
const BasicFlow = () => {
|
||||
const instance = useReactFlow();
|
||||
|
||||
const updatePos = () => {
|
||||
instance.setNodes((nodes) =>
|
||||
nodes.map((node) => {
|
||||
node.position = {
|
||||
x: Math.random() * 400,
|
||||
y: Math.random() * 400,
|
||||
};
|
||||
|
||||
return node;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const logToObject = () => console.log(instance.toObject());
|
||||
const resetTransform = () => instance.setViewport({ x: 0, y: 0, zoom: 1 });
|
||||
|
||||
const toggleClassnames = () => {
|
||||
instance.setNodes((nodes) =>
|
||||
nodes.map((node) => {
|
||||
node.className = node.className === 'light' ? 'dark' : 'light';
|
||||
|
||||
return node;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
defaultNodes={initialNodes}
|
||||
defaultEdges={initialEdges}
|
||||
onNodeClick={onNodeClick}
|
||||
onNodeDragStop={onNodeDragStop}
|
||||
onNodeDrag={onNodeDrag}
|
||||
className="react-flow-basic-example"
|
||||
minZoom={0.2}
|
||||
maxZoom={4}
|
||||
fitView
|
||||
defaultEdgeOptions={defaultEdgeOptions}
|
||||
selectNodesOnDrag={false}
|
||||
>
|
||||
<Background variant={BackgroundVariant.Lines} />
|
||||
<MiniMap />
|
||||
<Controls />
|
||||
|
||||
<div style={{ position: 'absolute', right: 10, top: 10, zIndex: 4 }}>
|
||||
<button onClick={resetTransform} style={{ marginRight: 5 }}>
|
||||
reset transform
|
||||
</button>
|
||||
<button onClick={updatePos} style={{ marginRight: 5 }}>
|
||||
change pos
|
||||
</button>
|
||||
<button onClick={toggleClassnames} style={{ marginRight: 5 }}>
|
||||
toggle classnames
|
||||
</button>
|
||||
<button onClick={logToObject}>toObject</button>
|
||||
</div>
|
||||
</ReactFlow>
|
||||
);
|
||||
};
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<ReactFlowProvider>
|
||||
<BasicFlow />
|
||||
</ReactFlowProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import React from 'react';
|
||||
import ReactFlow, {
|
||||
useReactFlow,
|
||||
Node,
|
||||
Edge,
|
||||
ReactFlowProvider,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
Background,
|
||||
BackgroundVariant,
|
||||
} from 'reactflow';
|
||||
|
||||
const defaultNodes: Node[] = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'input',
|
||||
data: { label: 'Node 1' },
|
||||
position: { x: 250, y: 5 },
|
||||
className: 'light',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
data: { label: 'Node 2' },
|
||||
position: { x: 100, y: 100 },
|
||||
className: 'light',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
data: { label: 'Node 3' },
|
||||
position: { x: 400, y: 100 },
|
||||
className: 'light',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
data: { label: 'Node 4' },
|
||||
position: { x: 400, y: 200 },
|
||||
className: 'light',
|
||||
},
|
||||
];
|
||||
|
||||
const defaultEdges: Edge[] = [
|
||||
{ id: 'e1-2', source: '1', target: '2' },
|
||||
{ id: 'e1-3', source: '1', target: '3' },
|
||||
];
|
||||
|
||||
const defaultEdgeOptions = {
|
||||
animated: true,
|
||||
};
|
||||
|
||||
// This is bad practise. You should either use a controlled or an uncontrolled component.
|
||||
// This is just an example for testing the API.
|
||||
const ControlledUncontrolled = () => {
|
||||
const [nodes, , onNodesChange] = useNodesState(defaultNodes);
|
||||
const [edges, , onEdgesChange] = useEdgesState(defaultEdges);
|
||||
const instance = useReactFlow();
|
||||
|
||||
const logToObject = () => console.log(instance.toObject());
|
||||
const resetTransform = () => instance.setViewport({ x: 0, y: 0, zoom: 1 });
|
||||
|
||||
const updateNodePositions = () => {
|
||||
instance.setNodes((nodes) =>
|
||||
nodes.map((node) => {
|
||||
node.position = {
|
||||
x: Math.random() * 400,
|
||||
y: Math.random() * 400,
|
||||
};
|
||||
|
||||
return node;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const updateEdgeColors = () => {
|
||||
instance.setEdges((edges) =>
|
||||
edges.map((edge) => {
|
||||
edge.style = {
|
||||
stroke: '#ff5050',
|
||||
};
|
||||
|
||||
return edge;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
defaultNodes={defaultNodes}
|
||||
defaultEdges={defaultEdges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
defaultEdgeOptions={defaultEdgeOptions}
|
||||
fitView
|
||||
>
|
||||
<Background variant={BackgroundVariant.Lines} />
|
||||
|
||||
<div style={{ position: 'absolute', right: 10, top: 10, zIndex: 4 }}>
|
||||
<button onClick={resetTransform} style={{ marginRight: 5 }}>
|
||||
reset transform
|
||||
</button>
|
||||
<button onClick={updateNodePositions} style={{ marginRight: 5 }}>
|
||||
change pos
|
||||
</button>
|
||||
<button onClick={updateEdgeColors} style={{ marginRight: 5 }}>
|
||||
red edges
|
||||
</button>
|
||||
<button onClick={logToObject}>toObject</button>
|
||||
</div>
|
||||
</ReactFlow>
|
||||
);
|
||||
};
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<ReactFlowProvider>
|
||||
<ControlledUncontrolled />
|
||||
</ReactFlowProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import React, { FC } from 'react';
|
||||
import { ConnectionLineComponentProps } from 'reactflow';
|
||||
|
||||
const ConnectionLine: FC<ConnectionLineComponentProps> = ({ fromX, fromY, toX, toY }) => {
|
||||
return (
|
||||
<g>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="#222"
|
||||
strokeWidth={1.5}
|
||||
className="animated"
|
||||
d={`M${fromX},${fromY} C ${fromX} ${toY} ${fromX} ${toY} ${toX},${toY}`}
|
||||
/>
|
||||
<circle cx={toX} cy={toY} fill="#fff" r={3} stroke="#222" strokeWidth={1.5} />
|
||||
</g>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConnectionLine;
|
||||
@@ -0,0 +1,44 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import ReactFlow, {
|
||||
Node,
|
||||
addEdge,
|
||||
Connection,
|
||||
Edge,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
Background,
|
||||
BackgroundVariant,
|
||||
} from 'reactflow';
|
||||
|
||||
import ConnectionLine from './ConnectionLine';
|
||||
|
||||
const initialNodes: Node[] = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'default',
|
||||
data: { label: 'Node 1' },
|
||||
position: { x: 250, y: 5 },
|
||||
},
|
||||
];
|
||||
const initialEdges: Edge[] = [];
|
||||
|
||||
const ConnectionLineFlow = () => {
|
||||
const [nodes, , onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||
const onConnect = useCallback((params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
connectionLineComponent={ConnectionLine}
|
||||
onConnect={onConnect}
|
||||
>
|
||||
<Background variant={BackgroundVariant.Lines} />
|
||||
</ReactFlow>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConnectionLineFlow;
|
||||
@@ -0,0 +1,47 @@
|
||||
import React, { memo, FC, CSSProperties, useCallback } from 'react';
|
||||
import { Handle, Position, NodeProps, Connection, Edge, useOnViewportChange, Viewport } from 'reactflow';
|
||||
|
||||
const targetHandleStyle: CSSProperties = { background: '#555' };
|
||||
const sourceHandleStyleA: CSSProperties = { ...targetHandleStyle, top: 10 };
|
||||
const sourceHandleStyleB: CSSProperties = {
|
||||
...targetHandleStyle,
|
||||
bottom: 10,
|
||||
top: 'auto',
|
||||
};
|
||||
|
||||
const onConnect = (params: Connection | Edge) => console.log('handle onConnect', params);
|
||||
|
||||
const ColorSelectorNode: FC<NodeProps> = ({ data, isConnectable }) => {
|
||||
const onStart = useCallback((viewport: Viewport) => console.log('onStart', viewport), []);
|
||||
const onChange = useCallback((viewport: Viewport) => console.log('onChange', viewport), []);
|
||||
const onEnd = useCallback((viewport: Viewport) => console.log('onEnd', viewport), []);
|
||||
|
||||
useOnViewportChange({
|
||||
onStart,
|
||||
onChange,
|
||||
onEnd,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Handle type="target" position={Position.Left} style={targetHandleStyle} onConnect={onConnect} />
|
||||
<div>
|
||||
Custom Color Picker Node: <strong>{data.color}</strong>
|
||||
</div>
|
||||
<input className="nodrag" type="color" onChange={data.onChange} defaultValue={data.color} />
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
id="a"
|
||||
style={sourceHandleStyleA}
|
||||
isConnectable={isConnectable}
|
||||
onMouseDown={(e) => {
|
||||
console.log('You trigger mousedown event', e);
|
||||
}}
|
||||
/>
|
||||
<Handle type="source" position={Position.Right} id="b" style={sourceHandleStyleB} isConnectable={isConnectable} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ColorSelectorNode);
|
||||
@@ -0,0 +1,164 @@
|
||||
import React, { useState, useEffect, MouseEvent, ChangeEvent, useCallback } from 'react';
|
||||
import ReactFlow, {
|
||||
MiniMap,
|
||||
Controls,
|
||||
addEdge,
|
||||
Node,
|
||||
ReactFlowInstance,
|
||||
Position,
|
||||
SnapGrid,
|
||||
Connection,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
} from 'reactflow';
|
||||
|
||||
import ColorSelectorNode from './ColorSelectorNode';
|
||||
|
||||
const onInit = (reactFlowInstance: ReactFlowInstance) => {
|
||||
console.log('flow loaded:', reactFlowInstance);
|
||||
};
|
||||
|
||||
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
|
||||
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
|
||||
|
||||
const initBgColor = '#1A192B';
|
||||
|
||||
const connectionLineStyle = { stroke: '#fff' };
|
||||
const snapGrid: SnapGrid = [16, 16];
|
||||
|
||||
const nodeTypes = {
|
||||
selectorNode: ColorSelectorNode,
|
||||
};
|
||||
|
||||
const CustomNodeFlow = () => {
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
||||
|
||||
const [bgColor, setBgColor] = useState<string>(initBgColor);
|
||||
|
||||
useEffect(() => {
|
||||
const onChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
setNodes((nds) =>
|
||||
nds.map((node) => {
|
||||
if (node.id !== '2') {
|
||||
return node;
|
||||
}
|
||||
|
||||
const color = event.target.value;
|
||||
|
||||
setBgColor(color);
|
||||
|
||||
return {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
color,
|
||||
},
|
||||
};
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
setNodes([
|
||||
{
|
||||
id: '1',
|
||||
type: 'input',
|
||||
data: { label: 'An input node' },
|
||||
position: { x: 0, y: 50 },
|
||||
sourcePosition: Position.Right,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'selectorNode',
|
||||
data: { onChange: onChange, color: initBgColor },
|
||||
style: { border: '1px solid #777', padding: 10 },
|
||||
position: { x: 250, y: 50 },
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
type: 'output',
|
||||
data: { label: 'Output A' },
|
||||
position: { x: 550, y: 25 },
|
||||
targetPosition: Position.Left,
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
type: 'output',
|
||||
data: { label: 'Output B' },
|
||||
position: { x: 550, y: 100 },
|
||||
targetPosition: Position.Left,
|
||||
},
|
||||
]);
|
||||
|
||||
setEdges([
|
||||
{
|
||||
id: 'e1-2',
|
||||
source: '1',
|
||||
target: '2',
|
||||
animated: true,
|
||||
style: { stroke: '#fff' },
|
||||
},
|
||||
{
|
||||
id: 'e2a-3',
|
||||
source: '2',
|
||||
sourceHandle: 'a',
|
||||
target: '3',
|
||||
animated: true,
|
||||
style: { stroke: '#fff' },
|
||||
},
|
||||
{
|
||||
id: 'e2b-4',
|
||||
source: '2',
|
||||
sourceHandle: 'b',
|
||||
target: '4',
|
||||
animated: true,
|
||||
style: { stroke: '#fff' },
|
||||
},
|
||||
]);
|
||||
}, []);
|
||||
|
||||
const onConnect = useCallback(
|
||||
(connection: Connection) =>
|
||||
setEdges((eds) => addEdge({ ...connection, animated: true, style: { stroke: '#fff' } }, eds)),
|
||||
[setEdges]
|
||||
);
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onNodeClick={onNodeClick}
|
||||
onConnect={onConnect}
|
||||
onNodeDragStop={onNodeDragStop}
|
||||
style={{ background: bgColor }}
|
||||
onInit={onInit}
|
||||
nodeTypes={nodeTypes}
|
||||
connectionLineStyle={connectionLineStyle}
|
||||
snapToGrid={true}
|
||||
snapGrid={snapGrid}
|
||||
fitView
|
||||
minZoom={0.3}
|
||||
maxZoom={2}
|
||||
>
|
||||
<MiniMap
|
||||
nodeStrokeColor={(n: Node): string => {
|
||||
if (n.type === 'input') return '#0041d0';
|
||||
if (n.type === 'selectorNode') return bgColor;
|
||||
if (n.type === 'output') return '#ff0072';
|
||||
|
||||
return '#eee';
|
||||
}}
|
||||
nodeColor={(n: Node): string => {
|
||||
if (n.type === 'selectorNode') return bgColor;
|
||||
|
||||
return '#fff';
|
||||
}}
|
||||
/>
|
||||
<Controls />
|
||||
</ReactFlow>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomNodeFlow;
|
||||
@@ -0,0 +1,98 @@
|
||||
import React from 'react';
|
||||
import ReactFlow, { useReactFlow, Node, Edge, ReactFlowProvider, Background, BackgroundVariant } from 'reactflow';
|
||||
|
||||
const defaultNodes: Node[] = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'input',
|
||||
data: { label: 'Node 1' },
|
||||
position: { x: 250, y: 5 },
|
||||
className: 'light',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
data: { label: 'Node 2' },
|
||||
position: { x: 100, y: 100 },
|
||||
className: 'light',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
data: { label: 'Node 3' },
|
||||
position: { x: 400, y: 100 },
|
||||
className: 'light',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
data: { label: 'Node 4' },
|
||||
position: { x: 400, y: 200 },
|
||||
className: 'light',
|
||||
},
|
||||
];
|
||||
|
||||
const defaultEdges: Edge[] = [
|
||||
{ id: 'e1-2', source: '1', target: '2' },
|
||||
{ id: 'e1-3', source: '1', target: '3' },
|
||||
];
|
||||
|
||||
const defaultEdgeOptions = {
|
||||
animated: true,
|
||||
};
|
||||
|
||||
const DefaultNodes = () => {
|
||||
const instance = useReactFlow();
|
||||
|
||||
const logToObject = () => console.log(instance.toObject());
|
||||
const resetTransform = () => instance.setViewport({ x: 0, y: 0, zoom: 1 });
|
||||
|
||||
const updateNodePositions = () => {
|
||||
instance.setNodes((nodes) =>
|
||||
nodes.map((node) => {
|
||||
node.position = {
|
||||
x: Math.random() * 400,
|
||||
y: Math.random() * 400,
|
||||
};
|
||||
|
||||
return node;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const updateEdgeColors = () => {
|
||||
instance.setEdges((edges) =>
|
||||
edges.map((edge) => {
|
||||
edge.style = {
|
||||
stroke: '#ff5050',
|
||||
};
|
||||
|
||||
return edge;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<ReactFlow defaultNodes={defaultNodes} defaultEdges={defaultEdges} defaultEdgeOptions={defaultEdgeOptions} fitView>
|
||||
<Background variant={BackgroundVariant.Lines} />
|
||||
|
||||
<div style={{ position: 'absolute', right: 10, top: 10, zIndex: 4 }}>
|
||||
<button onClick={resetTransform} style={{ marginRight: 5 }}>
|
||||
reset transform
|
||||
</button>
|
||||
<button onClick={updateNodePositions} style={{ marginRight: 5 }}>
|
||||
change pos
|
||||
</button>
|
||||
<button onClick={updateEdgeColors} style={{ marginRight: 5 }}>
|
||||
red edges
|
||||
</button>
|
||||
<button onClick={logToObject}>toObject</button>
|
||||
</div>
|
||||
</ReactFlow>
|
||||
);
|
||||
};
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<ReactFlowProvider>
|
||||
<DefaultNodes />
|
||||
</ReactFlowProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import React, { memo, FC } from 'react';
|
||||
|
||||
import { Handle, Position, NodeProps, Connection, Edge } from 'reactflow';
|
||||
|
||||
const onConnect = (params: Connection | Edge) => console.log('handle onConnect', params);
|
||||
|
||||
const labelStyle = {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
};
|
||||
|
||||
const dragHandleStyle = {
|
||||
display: 'inline-block',
|
||||
width: 25,
|
||||
height: 25,
|
||||
backgroundColor: 'teal',
|
||||
marginLeft: 5,
|
||||
borderRadius: '50%',
|
||||
};
|
||||
|
||||
const ColorSelectorNode: FC<NodeProps> = () => {
|
||||
return (
|
||||
<>
|
||||
<Handle type="target" position={Position.Left} onConnect={onConnect} />
|
||||
<div style={labelStyle}>
|
||||
Only draggable here → <span className="custom-drag-handle" style={dragHandleStyle} />
|
||||
</div>
|
||||
<Handle type="source" position={Position.Right} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ColorSelectorNode);
|
||||
@@ -0,0 +1,40 @@
|
||||
import React, { MouseEvent } from 'react';
|
||||
import ReactFlow, { Node, Edge, useNodesState, useEdgesState } from 'reactflow';
|
||||
|
||||
import DragHandleNode from './DragHandleNode';
|
||||
|
||||
const nodeTypes = {
|
||||
dragHandleNode: DragHandleNode,
|
||||
};
|
||||
|
||||
const initialNodes: Node[] = [
|
||||
{
|
||||
id: '2',
|
||||
type: 'dragHandleNode',
|
||||
dragHandle: '.custom-drag-handle',
|
||||
style: { border: '1px solid #ddd', padding: '20px 40px' },
|
||||
position: { x: 200, y: 200 },
|
||||
data: null,
|
||||
},
|
||||
];
|
||||
|
||||
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
|
||||
|
||||
const initialEdges: Edge[] = [];
|
||||
|
||||
const DragHandleFlow = () => {
|
||||
const [nodes, , onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges] = useEdgesState(initialEdges);
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
onNodesChange={onNodesChange}
|
||||
edges={edges}
|
||||
nodeTypes={nodeTypes}
|
||||
onNodeClick={onNodeClick}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default DragHandleFlow;
|
||||
@@ -0,0 +1,40 @@
|
||||
import React, { DragEvent } from 'react';
|
||||
import styles from './dnd.module.css';
|
||||
|
||||
const onDragStart = (event: DragEvent, nodeType: string) => {
|
||||
event.dataTransfer.setData('application/reactflow', nodeType);
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
};
|
||||
|
||||
const Sidebar = () => {
|
||||
return (
|
||||
<aside className={styles.aside}>
|
||||
<div className={styles.description}>
|
||||
You can drag these nodes to the pane on the left.
|
||||
</div>
|
||||
<div
|
||||
className='react-flow__node-input'
|
||||
onDragStart={(event: DragEvent) => onDragStart(event, 'input')}
|
||||
draggable
|
||||
>
|
||||
Input Node
|
||||
</div>
|
||||
<div
|
||||
className='react-flow__node-default'
|
||||
onDragStart={(event: DragEvent) => onDragStart(event, 'default')}
|
||||
draggable
|
||||
>
|
||||
Default Node
|
||||
</div>
|
||||
<div
|
||||
className='react-flow__node-output'
|
||||
onDragStart={(event: DragEvent) => onDragStart(event, 'output')}
|
||||
draggable
|
||||
>
|
||||
Output Node
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
@@ -0,0 +1,37 @@
|
||||
.dndflow {
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.aside {
|
||||
border-right: 1px solid #eee;
|
||||
padding: 15px 10px;
|
||||
font-size: 12px;
|
||||
background: #fcfcfc;
|
||||
}
|
||||
|
||||
.aside > * {
|
||||
margin-bottom: 10px;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.description {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.aside {
|
||||
width: 20%;
|
||||
max-width: 180px;
|
||||
}
|
||||
|
||||
.dndflow {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import React, { useState, DragEvent } from 'react';
|
||||
import ReactFlow, {
|
||||
ReactFlowProvider,
|
||||
addEdge,
|
||||
ReactFlowInstance,
|
||||
Connection,
|
||||
Edge,
|
||||
Node,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
Controls,
|
||||
NodeOrigin,
|
||||
} from 'reactflow';
|
||||
|
||||
import Sidebar from './Sidebar';
|
||||
|
||||
import styles from './dnd.module.css';
|
||||
|
||||
const initialNodes: Node[] = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'input',
|
||||
data: { label: 'input node' },
|
||||
position: { x: 250, y: 5 },
|
||||
},
|
||||
];
|
||||
|
||||
const onDragOver = (event: DragEvent) => {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = 'move';
|
||||
};
|
||||
|
||||
let id = 0;
|
||||
const getId = () => `dndnode_${id++}`;
|
||||
|
||||
const nodeOrigin: NodeOrigin = [0.5, 0.5];
|
||||
|
||||
const DnDFlow = () => {
|
||||
const [reactFlowInstance, setReactFlowInstance] = useState<ReactFlowInstance>();
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
||||
|
||||
const onConnect = (params: Connection | Edge) => setEdges((eds) => addEdge(params, eds));
|
||||
const onInit = (rfi: ReactFlowInstance) => setReactFlowInstance(rfi);
|
||||
|
||||
const onDrop = (event: DragEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (reactFlowInstance) {
|
||||
const type = event.dataTransfer.getData('application/reactflow');
|
||||
const position = reactFlowInstance.project({
|
||||
x: event.clientX,
|
||||
y: event.clientY - 40,
|
||||
});
|
||||
const newNode: Node = {
|
||||
id: getId(),
|
||||
type,
|
||||
position,
|
||||
data: { label: `${type} node` },
|
||||
};
|
||||
|
||||
setNodes((nds) => nds.concat(newNode));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.dndflow}>
|
||||
<ReactFlowProvider>
|
||||
<div className={styles.wrapper}>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onNodesChange={onNodesChange}
|
||||
onConnect={onConnect}
|
||||
onInit={onInit}
|
||||
onDrop={onDrop}
|
||||
onDragOver={onDragOver}
|
||||
nodeOrigin={nodeOrigin}
|
||||
>
|
||||
<Controls />
|
||||
</ReactFlow>
|
||||
</div>
|
||||
<Sidebar />
|
||||
</ReactFlowProvider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DnDFlow;
|
||||
@@ -0,0 +1,134 @@
|
||||
import React from 'react';
|
||||
import ReactFlow, { Node, Edge, Position, MarkerType } from 'reactflow';
|
||||
|
||||
const nodes: Node[] = [
|
||||
// LTR
|
||||
{
|
||||
id: '1',
|
||||
position: { x: 50, y: -100 },
|
||||
data: { label: 'Source' },
|
||||
sourcePosition: Position.Right,
|
||||
targetPosition: Position.Right,
|
||||
style: { background: 'rgba(255,255,255,0.5)' },
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
position: { x: -100, y: 0 },
|
||||
data: { label: 'Target' },
|
||||
sourcePosition: Position.Left,
|
||||
targetPosition: Position.Left,
|
||||
style: { background: 'rgba(255,255,255,0.5)' },
|
||||
},
|
||||
// Right Right
|
||||
{
|
||||
id: '3',
|
||||
position: { x: -100, y: 250 },
|
||||
data: { label: 'Source' },
|
||||
sourcePosition: Position.Right,
|
||||
targetPosition: Position.Right,
|
||||
style: { background: 'rgba(255,255,255,0.5)' },
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
position: { x: 50, y: 150 },
|
||||
data: { label: 'Target' },
|
||||
sourcePosition: Position.Right,
|
||||
targetPosition: Position.Right,
|
||||
style: { background: 'rgba(255,255,255,0.5)' },
|
||||
},
|
||||
// Right Top
|
||||
{
|
||||
id: '5',
|
||||
position: { x: -100, y: 450 },
|
||||
data: { label: 'Source' },
|
||||
sourcePosition: Position.Right,
|
||||
targetPosition: Position.Right,
|
||||
style: { background: 'rgba(255,255,255,0.5)' },
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
position: { x: 100, y: 400 },
|
||||
data: { label: 'Target' },
|
||||
sourcePosition: Position.Top,
|
||||
targetPosition: Position.Top,
|
||||
style: { background: 'rgba(255,255,255,0.5)' },
|
||||
},
|
||||
// Right Bottom
|
||||
{
|
||||
id: '7',
|
||||
position: { x: 100, y: 700 },
|
||||
data: { label: 'Source' },
|
||||
sourcePosition: Position.Right,
|
||||
targetPosition: Position.Right,
|
||||
style: { background: 'rgba(255,255,255,0.5)' },
|
||||
},
|
||||
{
|
||||
id: '8',
|
||||
position: { x: -100, y: 600 },
|
||||
data: { label: 'Target' },
|
||||
sourcePosition: Position.Bottom,
|
||||
targetPosition: Position.Bottom,
|
||||
style: { background: 'rgba(255,255,255,0.5)' },
|
||||
},
|
||||
];
|
||||
|
||||
const edges: Edge[] = [
|
||||
{
|
||||
id: 'e1-2',
|
||||
source: '1',
|
||||
target: '2',
|
||||
type: 'smoothstep',
|
||||
markerEnd: {
|
||||
type: MarkerType.ArrowClosed,
|
||||
},
|
||||
pathOptions: {
|
||||
offset: 30,
|
||||
},
|
||||
interactionWidth: 0,
|
||||
},
|
||||
{
|
||||
id: 'e3-4',
|
||||
source: '3',
|
||||
target: '4',
|
||||
type: 'smoothstep',
|
||||
markerEnd: {
|
||||
type: MarkerType.ArrowClosed,
|
||||
},
|
||||
pathOptions: {
|
||||
borderRadius: 2,
|
||||
},
|
||||
interactionWidth: 0,
|
||||
},
|
||||
|
||||
{
|
||||
id: 'e4-5',
|
||||
source: '5',
|
||||
target: '6',
|
||||
type: 'smoothstep',
|
||||
markerEnd: {
|
||||
type: MarkerType.ArrowClosed,
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
id: 'e7-8',
|
||||
source: '7',
|
||||
target: '8',
|
||||
type: 'smoothstep',
|
||||
markerEnd: {
|
||||
type: MarkerType.ArrowClosed,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const defaultEdgeOptions = {
|
||||
style: {
|
||||
strokeWidth: 1,
|
||||
},
|
||||
};
|
||||
|
||||
const SimpleEdge = () => {
|
||||
return <ReactFlow defaultNodes={nodes} defaultEdges={edges} fitView defaultEdgeOptions={defaultEdgeOptions} />;
|
||||
};
|
||||
|
||||
export default SimpleEdge;
|
||||
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Example for checking the different edge types and source and target positions
|
||||
*/
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import ReactFlow, {
|
||||
Background,
|
||||
Controls,
|
||||
MiniMap,
|
||||
addEdge,
|
||||
ReactFlowInstance,
|
||||
Connection,
|
||||
Edge,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
} from 'reactflow';
|
||||
|
||||
import { getElements } from './utils';
|
||||
|
||||
const onInit = (reactFlowInstance: ReactFlowInstance) => {
|
||||
reactFlowInstance.fitView();
|
||||
console.log(reactFlowInstance.getNodes());
|
||||
};
|
||||
|
||||
const { nodes: initialNodes, edges: initialEdges } = getElements();
|
||||
|
||||
const multiSelectionKeyCode = ['ShiftLeft', 'ShiftRight'];
|
||||
const deleteKeyCode = ['AltLeft+KeyD', 'Backspace'];
|
||||
|
||||
const EdgeTypesFlow = () => {
|
||||
const [nodes, , onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||
const onConnect = useCallback((params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onInit={onInit}
|
||||
onConnect={onConnect}
|
||||
minZoom={0.2}
|
||||
selectionKeyCode="a+s"
|
||||
multiSelectionKeyCode={multiSelectionKeyCode}
|
||||
deleteKeyCode={deleteKeyCode}
|
||||
zoomActivationKeyCode="z"
|
||||
>
|
||||
<MiniMap />
|
||||
<Controls />
|
||||
<Background />
|
||||
</ReactFlow>
|
||||
);
|
||||
};
|
||||
|
||||
export default EdgeTypesFlow;
|
||||
@@ -0,0 +1,114 @@
|
||||
import { Edge, Node, Position } from 'reactflow';
|
||||
|
||||
const nodeWidth = 80;
|
||||
const nodeGapWidth = nodeWidth * 2;
|
||||
const nodeStyle = { width: nodeWidth, fontSize: 11, color: 'white' };
|
||||
|
||||
const sourceTargetPositions = [
|
||||
{ source: Position.Bottom, target: Position.Top },
|
||||
{ source: Position.Right, target: Position.Left },
|
||||
];
|
||||
const nodeColors = [
|
||||
['#0c5956', '#1e9e99', '#4cb3ac', '#6ec9c0', '#8ddfd4'],
|
||||
['#0f4c75', '#1b5d8b', '#276fa1', '#3282b8', '#4fa6e0'],
|
||||
];
|
||||
const edgeTypes = ['default', 'step', 'smoothstep', 'straight', 'simplebezier'];
|
||||
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 = (): string => (id++).toString();
|
||||
|
||||
export function getElements(): { nodes: Node[]; edges: Edge[] } {
|
||||
const initialElements = { nodes: [] as Node[], edges: [] as Edge[] };
|
||||
|
||||
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: Node = {
|
||||
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: Node = {
|
||||
id: targetId,
|
||||
style,
|
||||
data: targetData,
|
||||
position: targetPosition,
|
||||
sourcePosition: currSourceTargetPos.source,
|
||||
targetPosition: currSourceTargetPos.target,
|
||||
};
|
||||
|
||||
initialElements.nodes.push(sourceNode);
|
||||
initialElements.nodes.push(targetNode);
|
||||
|
||||
initialElements.edges.push({
|
||||
id: `${sourceId}-${targetId}`,
|
||||
source: sourceId,
|
||||
target: targetId,
|
||||
type: currEdgeType,
|
||||
} as Edge);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return initialElements;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import React, { FC } from 'react';
|
||||
import { EdgeProps, getBezierPath } from 'reactflow';
|
||||
|
||||
const CustomEdge: FC<EdgeProps> = ({
|
||||
id,
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
data,
|
||||
}) => {
|
||||
const edgePath = getBezierPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<path id={id} className="react-flow__edge-path" d={edgePath} />
|
||||
<text>
|
||||
<textPath href={`#${id}`} style={{ fontSize: '12px' }} startOffset="50%" textAnchor="middle">
|
||||
{data.text}
|
||||
</textPath>
|
||||
</text>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomEdge;
|
||||
@@ -0,0 +1,48 @@
|
||||
import React, { FC } from 'react';
|
||||
import { EdgeProps, getBezierPath, EdgeText, getBezierEdgeCenter } from 'reactflow';
|
||||
|
||||
const CustomEdge: FC<EdgeProps> = ({
|
||||
id,
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
data,
|
||||
}) => {
|
||||
const edgePath = getBezierPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition,
|
||||
});
|
||||
const [centerX, centerY] = getBezierEdgeCenter({
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<path id={id} className="react-flow__edge-path" d={edgePath} />
|
||||
<EdgeText
|
||||
x={centerX}
|
||||
y={centerY}
|
||||
label={data.text}
|
||||
labelStyle={{ fill: 'white' }}
|
||||
labelShowBg
|
||||
labelBgStyle={{ fill: 'red' }}
|
||||
labelBgPadding={[2, 4]}
|
||||
labelBgBorderRadius={2}
|
||||
onClick={() => console.log(data)}
|
||||
/>
|
||||
;
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomEdge;
|
||||
@@ -0,0 +1,206 @@
|
||||
import React, { MouseEvent, useCallback } from 'react';
|
||||
import ReactFlow, {
|
||||
Controls,
|
||||
Background,
|
||||
MiniMap,
|
||||
addEdge,
|
||||
Connection,
|
||||
Edge,
|
||||
EdgeTypes,
|
||||
MarkerType,
|
||||
Node,
|
||||
useEdgesState,
|
||||
useNodesState,
|
||||
} from 'reactflow';
|
||||
|
||||
import CustomEdge from './CustomEdge';
|
||||
import CustomEdge2 from './CustomEdge2';
|
||||
|
||||
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
|
||||
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
|
||||
const onEdgeClick = (_: MouseEvent, edge: Edge) => console.log('click', edge);
|
||||
const onEdgeDoubleClick = (_: MouseEvent, edge: Edge) => console.log('dblclick', edge);
|
||||
const onEdgeMouseEnter = (_: MouseEvent, edge: Edge) => console.log('enter', edge);
|
||||
const onEdgeMouseMove = (_: MouseEvent, edge: Edge) => console.log('move', edge);
|
||||
const onEdgeMouseLeave = (_: MouseEvent, edge: Edge) => console.log('leave', edge);
|
||||
|
||||
const initialNodes: Node[] = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'input',
|
||||
data: { label: 'Input 1' },
|
||||
position: { x: 250, y: 0 },
|
||||
},
|
||||
{ id: '2', data: { label: 'Node 2' }, position: { x: 150, y: 100 } },
|
||||
{ id: '2a', data: { label: 'Node 2a' }, position: { x: 0, y: 180 } },
|
||||
{ id: '2b', data: { label: 'Node 2b' }, position: { x: -40, y: 300 } },
|
||||
{ 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: 250, y: 400 } },
|
||||
{
|
||||
id: '6',
|
||||
type: 'output',
|
||||
data: { label: 'Output 6' },
|
||||
position: { x: 50, y: 550 },
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
type: 'output',
|
||||
data: { label: 'Output 7' },
|
||||
position: { x: 250, y: 550 },
|
||||
},
|
||||
{
|
||||
id: '8',
|
||||
type: 'output',
|
||||
data: { label: 'Output 8' },
|
||||
position: { x: 525, y: 600 },
|
||||
},
|
||||
{
|
||||
id: '9',
|
||||
type: 'output',
|
||||
data: { label: 'Output 9' },
|
||||
position: { x: 675, y: 500 },
|
||||
},
|
||||
];
|
||||
|
||||
const initialEdges: Edge[] = [
|
||||
{
|
||||
id: 'e1-2',
|
||||
source: '1',
|
||||
target: '2',
|
||||
label: 'bezier edge (default)',
|
||||
className: 'normal-edge',
|
||||
},
|
||||
{
|
||||
id: 'e2-2a',
|
||||
source: '2',
|
||||
target: '2a',
|
||||
type: 'smoothstep',
|
||||
label: 'smoothstep edge',
|
||||
},
|
||||
{
|
||||
id: 'e2a-2b',
|
||||
source: '2a',
|
||||
target: '2b',
|
||||
type: 'simplebezier',
|
||||
label: 'simple bezier edge',
|
||||
},
|
||||
{ id: 'e2-3', source: '2', target: '3', type: 'step', label: 'step edge' },
|
||||
{
|
||||
id: 'e3-4',
|
||||
source: '3',
|
||||
target: '4',
|
||||
type: 'straight',
|
||||
label: 'straight edge',
|
||||
},
|
||||
{
|
||||
id: 'e3-3a',
|
||||
source: '3',
|
||||
target: '3a',
|
||||
type: 'straight',
|
||||
label: 'label only edge',
|
||||
style: { stroke: 'none' },
|
||||
},
|
||||
{
|
||||
id: 'e3-5',
|
||||
source: '4',
|
||||
target: '5',
|
||||
animated: true,
|
||||
label: 'animated styled edge',
|
||||
style: { stroke: 'red' },
|
||||
},
|
||||
{
|
||||
id: 'e5-7',
|
||||
source: '5',
|
||||
target: '7',
|
||||
label: 'label with styled bg',
|
||||
labelBgPadding: [8, 4],
|
||||
labelBgBorderRadius: 4,
|
||||
labelBgStyle: { fill: '#FFCC00', color: '#fff', fillOpacity: 0.7 },
|
||||
markerEnd: {
|
||||
type: MarkerType.ArrowClosed,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'e5-8',
|
||||
source: '5',
|
||||
target: '8',
|
||||
type: 'custom',
|
||||
data: { text: 'custom edge' },
|
||||
},
|
||||
{
|
||||
id: 'e5-9',
|
||||
source: '5',
|
||||
target: '9',
|
||||
type: 'custom2',
|
||||
data: { text: 'custom edge 2' },
|
||||
},
|
||||
{
|
||||
id: 'e5-6',
|
||||
source: '5',
|
||||
target: '6',
|
||||
label: (
|
||||
<>
|
||||
<tspan>i am using</tspan>
|
||||
<tspan dy={10} x={0}>
|
||||
{'<tspan>'}
|
||||
</tspan>
|
||||
</>
|
||||
),
|
||||
labelStyle: { fill: 'red', fontWeight: 700 },
|
||||
style: { stroke: '#ffcc00' },
|
||||
markerEnd: {
|
||||
type: MarkerType.Arrow,
|
||||
color: '#FFCC00',
|
||||
markerUnits: 'userSpaceOnUse',
|
||||
width: 20,
|
||||
height: 20,
|
||||
strokeWidth: 2,
|
||||
},
|
||||
markerStart: {
|
||||
type: MarkerType.ArrowClosed,
|
||||
color: '#FFCC00',
|
||||
orient: 'auto-start-reverse',
|
||||
markerUnits: 'userSpaceOnUse',
|
||||
width: 20,
|
||||
height: 20,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const edgeTypes: EdgeTypes = {
|
||||
custom: CustomEdge,
|
||||
custom2: CustomEdge2,
|
||||
};
|
||||
|
||||
const EdgesFlow = () => {
|
||||
const [nodes, , onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||
const onConnect = useCallback((params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onNodeClick={onNodeClick}
|
||||
onConnect={onConnect}
|
||||
onNodeDragStop={onNodeDragStop}
|
||||
snapToGrid={true}
|
||||
edgeTypes={edgeTypes}
|
||||
onEdgeClick={onEdgeClick}
|
||||
onEdgeDoubleClick={onEdgeDoubleClick}
|
||||
onEdgeMouseEnter={onEdgeMouseEnter}
|
||||
onEdgeMouseMove={onEdgeMouseMove}
|
||||
onEdgeMouseLeave={onEdgeMouseLeave}
|
||||
>
|
||||
<MiniMap />
|
||||
<Controls />
|
||||
<Background />
|
||||
</ReactFlow>
|
||||
);
|
||||
};
|
||||
|
||||
export default EdgesFlow;
|
||||
@@ -0,0 +1,67 @@
|
||||
import React, { MouseEvent, CSSProperties, useCallback } from 'react';
|
||||
|
||||
import ReactFlow, {
|
||||
Background,
|
||||
BackgroundVariant,
|
||||
Controls,
|
||||
addEdge,
|
||||
Node,
|
||||
Connection,
|
||||
Edge,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
ReactFlowInstance,
|
||||
} from 'reactflow';
|
||||
|
||||
const onInit = (reactFlowInstance: ReactFlowInstance) => console.log('flow loaded:', reactFlowInstance);
|
||||
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
|
||||
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
|
||||
|
||||
const buttonStyle: CSSProperties = {
|
||||
position: 'absolute',
|
||||
left: 10,
|
||||
top: 10,
|
||||
zIndex: 4,
|
||||
};
|
||||
|
||||
const EmptyFlow = () => {
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
||||
|
||||
const onConnect = useCallback((params: Connection | Edge) => setEdges((els) => addEdge(params, els)), [setEdges]);
|
||||
const addRandomNode = () => {
|
||||
const nodeId = (nodes.length + 1).toString();
|
||||
const newNode: Node = {
|
||||
id: nodeId,
|
||||
data: { label: `Node: ${nodeId}` },
|
||||
position: {
|
||||
x: Math.random() * window.innerWidth,
|
||||
y: Math.random() * window.innerHeight,
|
||||
},
|
||||
};
|
||||
setNodes((nds) => nds.concat(newNode));
|
||||
};
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onInit={onInit}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onNodeClick={onNodeClick}
|
||||
onConnect={(p) => onConnect(p)}
|
||||
onNodeDragStop={onNodeDragStop}
|
||||
onlyRenderVisibleElements={false}
|
||||
>
|
||||
<Controls />
|
||||
<Background variant={BackgroundVariant.Lines} />
|
||||
|
||||
<button type="button" onClick={addRandomNode} style={buttonStyle}>
|
||||
add node
|
||||
</button>
|
||||
</ReactFlow>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmptyFlow;
|
||||
@@ -0,0 +1,42 @@
|
||||
import React, { FC } from 'react';
|
||||
import { getBezierPath, ConnectionLineComponentProps, Node } from 'reactflow';
|
||||
|
||||
import { getEdgeParams } from './utils';
|
||||
|
||||
const FloatingConnectionLine: FC<ConnectionLineComponentProps> = ({
|
||||
targetX,
|
||||
targetY,
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
sourceNode,
|
||||
}) => {
|
||||
if (!sourceNode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const targetNode = {
|
||||
id: 'connection-target',
|
||||
width: 1,
|
||||
height: 1,
|
||||
position: { x: targetX, y: targetY },
|
||||
} as Node;
|
||||
|
||||
const { sx, sy } = getEdgeParams(sourceNode, targetNode);
|
||||
const d = getBezierPath({
|
||||
sourceX: sx,
|
||||
sourceY: sy,
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
targetX,
|
||||
targetY,
|
||||
});
|
||||
|
||||
return (
|
||||
<g>
|
||||
<path fill="none" stroke="#222" strokeWidth={1.5} className="animated" d={d} />
|
||||
<circle cx={targetX} cy={targetY} fill="#fff" r={3} stroke="#222" strokeWidth={1.5} />
|
||||
</g>
|
||||
);
|
||||
};
|
||||
|
||||
export default FloatingConnectionLine;
|
||||
@@ -0,0 +1,36 @@
|
||||
import React, { FC, useMemo, CSSProperties } from 'react';
|
||||
import { EdgeProps, useStore, getBezierPath, ReactFlowState } from 'reactflow';
|
||||
|
||||
import { getEdgeParams } from './utils';
|
||||
|
||||
const nodeSelector = (s: ReactFlowState) => s.nodeInternals;
|
||||
|
||||
const FloatingEdge: FC<EdgeProps> = ({ id, source, target, style }) => {
|
||||
const nodeInternals = useStore(nodeSelector);
|
||||
|
||||
const sourceNode = useMemo(() => nodeInternals.get(source), [source, nodeInternals]);
|
||||
const targetNode = useMemo(() => nodeInternals.get(target), [target, nodeInternals]);
|
||||
|
||||
if (!sourceNode || !targetNode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { sx, sy, tx, ty, sourcePos, targetPos } = getEdgeParams(sourceNode, targetNode);
|
||||
|
||||
const d = getBezierPath({
|
||||
sourceX: sx,
|
||||
sourceY: sy,
|
||||
sourcePosition: sourcePos,
|
||||
targetPosition: targetPos,
|
||||
targetX: tx,
|
||||
targetY: ty,
|
||||
});
|
||||
|
||||
return (
|
||||
<g className="react-flow__connection">
|
||||
<path id={id} className="react-flow__edge-path" d={d} style={style as CSSProperties} />
|
||||
</g>
|
||||
);
|
||||
};
|
||||
|
||||
export default FloatingEdge;
|
||||
@@ -0,0 +1,53 @@
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import ReactFlow, {
|
||||
Background,
|
||||
addEdge,
|
||||
ReactFlowInstance,
|
||||
EdgeTypes,
|
||||
Connection,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
} from 'reactflow';
|
||||
|
||||
import styles from './style.module.css';
|
||||
|
||||
import FloatingConnectionLine from './FloatingConnectionLine';
|
||||
import FloatingEdge from './FloatingEdge';
|
||||
import { createElements } from './utils';
|
||||
|
||||
const onInit = (reactFlowInstance: ReactFlowInstance) => reactFlowInstance.fitView();
|
||||
|
||||
const { nodes: initialNodes, edges: initialEdges } =
|
||||
typeof window !== 'undefined' ? createElements() : { nodes: [], edges: [] };
|
||||
const edgeTypes: EdgeTypes = {
|
||||
floating: FloatingEdge,
|
||||
};
|
||||
|
||||
const FloatingEdges = () => {
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||
|
||||
const onConnect = useCallback((connection: Connection) => {
|
||||
setEdges((eds) => addEdge(connection, eds));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles.floatingedges}>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
onInit={onInit}
|
||||
edgeTypes={edgeTypes}
|
||||
connectionLineComponent={FloatingConnectionLine}
|
||||
>
|
||||
<Background />
|
||||
</ReactFlow>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FloatingEdges;
|
||||
@@ -0,0 +1,9 @@
|
||||
.floatingedges {
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.floatingedges :global .react-flow__handle {
|
||||
opacity: 0;
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import { Position, XYPosition, Node, Edge } from 'reactflow';
|
||||
|
||||
// this helper function returns the intersection point
|
||||
// of the line between the center of the intersectionNode and the target node
|
||||
function getNodeIntersection(intersectionNode: Node, targetNode: Node): XYPosition {
|
||||
// https://math.stackexchange.com/questions/1724792/an-algorithm-for-finding-the-intersection-point-between-a-center-of-vision-and-a
|
||||
|
||||
const {
|
||||
width: intersectionNodeWidth,
|
||||
height: intersectionNodeHeight,
|
||||
position: intersectionNodePosition,
|
||||
} = intersectionNode;
|
||||
const targetPosition = targetNode.position;
|
||||
|
||||
const w = (intersectionNodeWidth ?? 0) / 2;
|
||||
const h = (intersectionNodeHeight ?? 0) / 2;
|
||||
|
||||
const x2 = intersectionNodePosition.x + w;
|
||||
const y2 = intersectionNodePosition.y + h;
|
||||
const x1 = targetPosition.x + w;
|
||||
const y1 = targetPosition.y + h;
|
||||
|
||||
const xx1 = (x1 - x2) / (2 * w) - (y1 - y2) / (2 * h);
|
||||
const yy1 = (x1 - x2) / (2 * w) + (y1 - y2) / (2 * h);
|
||||
const a = 1 / (Math.abs(xx1) + Math.abs(yy1));
|
||||
const xx3 = a * xx1;
|
||||
const yy3 = a * yy1;
|
||||
const x = w * (xx3 + yy3) + x2;
|
||||
const y = h * (-xx3 + yy3) + y2;
|
||||
|
||||
return { x, y };
|
||||
}
|
||||
|
||||
// returns the position (top,right,bottom or right) passed node compared to the intersection point
|
||||
function getEdgePosition(node: Node, intersectionPoint: XYPosition) {
|
||||
const n = { ...node.position, ...node };
|
||||
const nx = Math.round(n.x);
|
||||
const ny = Math.round(n.y);
|
||||
const px = Math.round(intersectionPoint.x);
|
||||
const py = Math.round(intersectionPoint.y);
|
||||
|
||||
if (px <= nx + 1) {
|
||||
return Position.Left;
|
||||
}
|
||||
if (px >= nx + (n.width ?? 0) - 1) {
|
||||
return Position.Right;
|
||||
}
|
||||
if (py <= ny + 1) {
|
||||
return Position.Top;
|
||||
}
|
||||
if (py >= n.y + (n.height ?? 0) - 1) {
|
||||
return Position.Bottom;
|
||||
}
|
||||
|
||||
return Position.Top;
|
||||
}
|
||||
|
||||
// returns the parameters (sx, sy, tx, ty, sourcePos, targetPos) you need to create an edge
|
||||
export function getEdgeParams(source: Node, target: Node) {
|
||||
const sourceIntersectionPoint = getNodeIntersection(source, target);
|
||||
const targetIntersectionPoint = getNodeIntersection(target, source);
|
||||
|
||||
const sourcePos = getEdgePosition(source, sourceIntersectionPoint);
|
||||
const targetPos = getEdgePosition(target, targetIntersectionPoint);
|
||||
|
||||
return {
|
||||
sx: sourceIntersectionPoint.x,
|
||||
sy: sourceIntersectionPoint.y,
|
||||
tx: targetIntersectionPoint.x,
|
||||
ty: targetIntersectionPoint.y,
|
||||
sourcePos,
|
||||
targetPos,
|
||||
};
|
||||
}
|
||||
|
||||
type NodesAndEdges = {
|
||||
nodes: Node[];
|
||||
edges: Edge[];
|
||||
};
|
||||
|
||||
export function createElements(): NodesAndEdges {
|
||||
const nodes: Node[] = [];
|
||||
const edges: Edge[] = [];
|
||||
|
||||
const center = { x: window.innerWidth / 2, y: window.innerHeight / 2 };
|
||||
|
||||
nodes.push({ id: 'target', data: { label: 'Target' }, position: center });
|
||||
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const degrees = i * (360 / 8);
|
||||
const radians = degrees * (Math.PI / 180);
|
||||
const x = 250 * Math.cos(radians) + center.x;
|
||||
const y = 250 * Math.sin(radians) + center.y;
|
||||
|
||||
nodes.push({ id: `${i}`, data: { label: 'Source' }, position: { x, y } });
|
||||
|
||||
edges.push({
|
||||
id: `edge-${i}`,
|
||||
target: 'target',
|
||||
source: `${i}`,
|
||||
type: 'floating',
|
||||
});
|
||||
}
|
||||
|
||||
return { nodes, edges };
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
|
||||
import ReactFlow, { addEdge, Connection, Edge, Node, useNodesState, useEdgesState, MiniMap, Controls } from 'reactflow';
|
||||
|
||||
const initialNodes: Node[] = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'input',
|
||||
hidden: true,
|
||||
data: { label: 'Node 1' },
|
||||
position: { x: 250, y: 5 },
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
hidden: true,
|
||||
data: { label: 'Node 2' },
|
||||
position: { x: 100, y: 100 },
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
hidden: true,
|
||||
data: { label: 'Node 3' },
|
||||
position: { x: 400, y: 100 },
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
hidden: true,
|
||||
data: { label: 'Node 4' },
|
||||
position: { x: 400, y: 200 },
|
||||
},
|
||||
];
|
||||
|
||||
const initialEdges: Edge[] = [
|
||||
{ id: 'e1-2', source: '1', target: '2' },
|
||||
{ id: 'e1-3', source: '1', target: '3' },
|
||||
{ id: 'e3-4', source: '3', target: '4' },
|
||||
];
|
||||
|
||||
const setHidden = (hidden: boolean) => (els: any[]) =>
|
||||
els.map((e: any) => {
|
||||
e.hidden = hidden;
|
||||
return e;
|
||||
});
|
||||
|
||||
const HiddenFlow = () => {
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||
|
||||
const [isHidden, setIsHidden] = useState<boolean>(true);
|
||||
|
||||
const onConnect = useCallback(
|
||||
(connection: Connection) => {
|
||||
setEdges((eds) => addEdge(connection, eds));
|
||||
},
|
||||
[setEdges]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setNodes(setHidden(isHidden));
|
||||
setEdges(setHidden(isHidden));
|
||||
}, [isHidden, setEdges, setNodes]);
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onConnect={onConnect}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
>
|
||||
<MiniMap />
|
||||
<Controls />
|
||||
|
||||
<div style={{ position: 'absolute', left: 10, top: 10, zIndex: 4 }}>
|
||||
<div>
|
||||
<label htmlFor="ishidden">
|
||||
isHidden
|
||||
<input
|
||||
id="ishidden"
|
||||
type="checkbox"
|
||||
checked={isHidden}
|
||||
onChange={(event) => setIsHidden(event.target.checked)}
|
||||
className="react-flow__ishidden"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</ReactFlow>
|
||||
);
|
||||
};
|
||||
|
||||
export default HiddenFlow;
|
||||
@@ -0,0 +1,240 @@
|
||||
import React, { useState, MouseEvent as ReactMouseEvent, WheelEvent } from 'react';
|
||||
import ReactFlow, {
|
||||
addEdge,
|
||||
Node,
|
||||
Connection,
|
||||
Edge,
|
||||
PanOnScrollMode,
|
||||
Viewport,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
Controls,
|
||||
MiniMap,
|
||||
} from 'reactflow';
|
||||
|
||||
const initialNodes: Node[] = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'input',
|
||||
data: { label: 'Node 1' },
|
||||
position: { x: 250, y: 5 },
|
||||
},
|
||||
{ id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 } },
|
||||
{ id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 } },
|
||||
{ id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 } },
|
||||
];
|
||||
|
||||
const initialEdges: Edge[] = [
|
||||
{ id: 'e1-2', source: '1', target: '2', animated: true },
|
||||
{ id: 'e1-3', source: '1', target: '3' },
|
||||
];
|
||||
|
||||
const onNodeDragStart = (_: ReactMouseEvent, node: Node) => console.log('drag start', node);
|
||||
const onNodeDragStop = (_: ReactMouseEvent, node: Node) => console.log('drag stop', node);
|
||||
const onNodeClick = (_: ReactMouseEvent, node: Node) => console.log('click', node);
|
||||
const onEdgeClick = (_: ReactMouseEvent, edge: Edge) => console.log('click', edge);
|
||||
const onPaneClick = (event: ReactMouseEvent) => console.log('onPaneClick', event);
|
||||
const onPaneScroll = (event?: WheelEvent) => console.log('onPaneScroll', event);
|
||||
const onPaneContextMenu = (event: ReactMouseEvent) => console.log('onPaneContextMenu', event);
|
||||
const onMoveEnd = (_: TouchEvent | MouseEvent, viewport: Viewport) => console.log('onMoveEnd', viewport);
|
||||
|
||||
const InteractionFlow = () => {
|
||||
const [nodes, , onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||
const onConnect = (params: Connection | Edge) => setEdges((els) => addEdge(params, els));
|
||||
|
||||
const [isSelectable, setIsSelectable] = useState<boolean>(false);
|
||||
const [isDraggable, setIsDraggable] = useState<boolean>(false);
|
||||
const [isConnectable, setIsConnectable] = useState<boolean>(false);
|
||||
const [zoomOnScroll, setZoomOnScroll] = useState<boolean>(false);
|
||||
const [zoomOnPinch, setZoomOnPinch] = useState<boolean>(false);
|
||||
const [panOnScroll, setPanOnScroll] = useState<boolean>(false);
|
||||
const [panOnScrollMode, setPanOnScrollMode] = useState<PanOnScrollMode>(PanOnScrollMode.Free);
|
||||
const [zoomOnDoubleClick, setZoomOnDoubleClick] = useState<boolean>(false);
|
||||
const [panOnDrag, setPanOnDrag] = useState<boolean>(true);
|
||||
const [captureZoomClick, setCaptureZoomClick] = useState<boolean>(false);
|
||||
const [captureZoomScroll, setCaptureZoomScroll] = useState<boolean>(false);
|
||||
const [captureElementClick, setCaptureElementClick] = useState<boolean>(false);
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
elementsSelectable={isSelectable}
|
||||
nodesConnectable={isConnectable}
|
||||
nodesDraggable={isDraggable}
|
||||
zoomOnScroll={zoomOnScroll}
|
||||
zoomOnPinch={zoomOnPinch}
|
||||
panOnScroll={panOnScroll}
|
||||
panOnScrollMode={panOnScrollMode}
|
||||
zoomOnDoubleClick={zoomOnDoubleClick}
|
||||
onConnect={onConnect}
|
||||
onNodeClick={captureElementClick ? onNodeClick : undefined}
|
||||
onEdgeClick={captureElementClick ? onEdgeClick : undefined}
|
||||
onNodeDragStart={onNodeDragStart}
|
||||
onNodeDragStop={onNodeDragStop}
|
||||
panOnDrag={panOnDrag}
|
||||
onPaneClick={captureZoomClick ? onPaneClick : undefined}
|
||||
onPaneScroll={captureZoomScroll ? onPaneScroll : undefined}
|
||||
onPaneContextMenu={captureZoomClick ? onPaneContextMenu : undefined}
|
||||
onMoveEnd={onMoveEnd}
|
||||
>
|
||||
<MiniMap />
|
||||
<Controls />
|
||||
|
||||
<div style={{ position: 'absolute', left: 10, top: 10, zIndex: 4 }}>
|
||||
<div>
|
||||
<label htmlFor="draggable">
|
||||
nodesDraggable
|
||||
<input
|
||||
id="draggable"
|
||||
type="checkbox"
|
||||
checked={isDraggable}
|
||||
onChange={(event) => setIsDraggable(event.target.checked)}
|
||||
className="react-flow__draggable"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="connectable">
|
||||
nodesConnectable
|
||||
<input
|
||||
id="connectable"
|
||||
type="checkbox"
|
||||
checked={isConnectable}
|
||||
onChange={(event) => setIsConnectable(event.target.checked)}
|
||||
className="react-flow__connectable"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="selectable">
|
||||
elementsSelectable
|
||||
<input
|
||||
id="selectable"
|
||||
type="checkbox"
|
||||
checked={isSelectable}
|
||||
onChange={(event) => setIsSelectable(event.target.checked)}
|
||||
className="react-flow__selectable"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="zoomonscroll">
|
||||
zoomOnScroll
|
||||
<input
|
||||
id="zoomonscroll"
|
||||
type="checkbox"
|
||||
checked={zoomOnScroll}
|
||||
onChange={(event) => setZoomOnScroll(event.target.checked)}
|
||||
className="react-flow__zoomonscroll"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="zoomonpinch">
|
||||
zoomOnPinch
|
||||
<input
|
||||
id="zoomonpinch"
|
||||
type="checkbox"
|
||||
checked={zoomOnPinch}
|
||||
onChange={(event) => setZoomOnPinch(event.target.checked)}
|
||||
className="react-flow__zoomonpinch"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="panonscroll">
|
||||
panOnScroll
|
||||
<input
|
||||
id="panonscroll"
|
||||
type="checkbox"
|
||||
checked={panOnScroll}
|
||||
onChange={(event) => setPanOnScroll(event.target.checked)}
|
||||
className="react-flow__panonscroll"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="panonscrollmode">
|
||||
panOnScrollMode
|
||||
<select
|
||||
id="panonscrollmode"
|
||||
value={panOnScrollMode}
|
||||
onChange={(event) => setPanOnScrollMode(event.target.value as PanOnScrollMode)}
|
||||
className="react-flow__panonscrollmode"
|
||||
>
|
||||
<option value="free">free</option>
|
||||
<option value="horizontal">horizontal</option>
|
||||
<option value="vertical">vertical</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="zoomondbl">
|
||||
zoomOnDoubleClick
|
||||
<input
|
||||
id="zoomondbl"
|
||||
type="checkbox"
|
||||
checked={zoomOnDoubleClick}
|
||||
onChange={(event) => setZoomOnDoubleClick(event.target.checked)}
|
||||
className="react-flow__zoomondbl"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="panondrag">
|
||||
panOnDrag
|
||||
<input
|
||||
id="panondrag"
|
||||
type="checkbox"
|
||||
checked={panOnDrag}
|
||||
onChange={(event) => setPanOnDrag(event.target.checked)}
|
||||
className="react-flow__panondrag"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="capturezoompaneclick">
|
||||
capture onPaneClick
|
||||
<input
|
||||
id="capturezoompaneclick"
|
||||
type="checkbox"
|
||||
checked={captureZoomClick}
|
||||
onChange={(event) => setCaptureZoomClick(event.target.checked)}
|
||||
className="react-flow__capturezoompaneclick"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="capturezoompanescroll">
|
||||
capture onPaneScroll
|
||||
<input
|
||||
id="capturezoompanescroll"
|
||||
type="checkbox"
|
||||
checked={captureZoomScroll}
|
||||
onChange={(event) => setCaptureZoomScroll(event.target.checked)}
|
||||
className="react-flow__capturezoompanescroll"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="captureelementclick">
|
||||
capture onElementClick
|
||||
<input
|
||||
id="captureelementclick"
|
||||
type="checkbox"
|
||||
checked={captureElementClick}
|
||||
onChange={(event) => setCaptureElementClick(event.target.checked)}
|
||||
className="react-flow__captureelementclick"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</ReactFlow>
|
||||
);
|
||||
};
|
||||
|
||||
export default InteractionFlow;
|
||||
@@ -0,0 +1,115 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import ReactFlow, {
|
||||
Controls,
|
||||
ReactFlowProvider,
|
||||
addEdge,
|
||||
Connection,
|
||||
CoordinateExtent,
|
||||
Position,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
MarkerType,
|
||||
EdgeMarker,
|
||||
} from 'reactflow';
|
||||
|
||||
import dagre from 'dagre';
|
||||
|
||||
import initialItems from './initial-elements';
|
||||
|
||||
import styles from './layouting.module.css';
|
||||
|
||||
const dagreGraph = new dagre.graphlib.Graph();
|
||||
dagreGraph.setDefaultEdgeLabel(() => ({}));
|
||||
|
||||
const nodeExtent: CoordinateExtent = [
|
||||
[0, 0],
|
||||
[1000, 1000],
|
||||
];
|
||||
|
||||
const LayoutFlow = () => {
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialItems.nodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialItems.edges);
|
||||
|
||||
const onConnect = useCallback(
|
||||
(connection: Connection) => {
|
||||
setEdges((eds) => addEdge(connection, eds));
|
||||
},
|
||||
[setEdges]
|
||||
);
|
||||
|
||||
const onLayout = (direction: string) => {
|
||||
const isHorizontal = direction === 'LR';
|
||||
dagreGraph.setGraph({ rankdir: direction });
|
||||
|
||||
nodes.forEach((node) => {
|
||||
dagreGraph.setNode(node.id, { width: 150, height: 50 });
|
||||
});
|
||||
|
||||
edges.forEach((edge) => {
|
||||
dagreGraph.setEdge(edge.source, edge.target);
|
||||
});
|
||||
|
||||
dagre.layout(dagreGraph);
|
||||
|
||||
const layoutedNodes = nodes.map((node) => {
|
||||
const nodeWithPosition = dagreGraph.node(node.id);
|
||||
node.targetPosition = isHorizontal ? Position.Left : Position.Top;
|
||||
node.sourcePosition = isHorizontal ? Position.Right : Position.Bottom;
|
||||
// we need to pass a slightly different position in order to notify react flow about the change
|
||||
// @TODO how can we change the position handling so that we dont need this hack?
|
||||
node.position = {
|
||||
x: nodeWithPosition.x + Math.random() / 1000,
|
||||
y: nodeWithPosition.y,
|
||||
};
|
||||
|
||||
return node;
|
||||
});
|
||||
|
||||
setNodes(layoutedNodes);
|
||||
};
|
||||
|
||||
const unselect = () => {
|
||||
setNodes((nds) => nds.map((n) => ({ ...n, selected: false })));
|
||||
};
|
||||
|
||||
const changeMarker = () => {
|
||||
setEdges((eds) =>
|
||||
eds.map((e) => ({
|
||||
...e,
|
||||
markerEnd: {
|
||||
type: (e.markerEnd as EdgeMarker)?.type === MarkerType.Arrow ? MarkerType.ArrowClosed : MarkerType.Arrow,
|
||||
},
|
||||
}))
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.layoutflow}>
|
||||
<ReactFlowProvider>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onConnect={onConnect}
|
||||
nodeExtent={nodeExtent}
|
||||
onInit={() => onLayout('TB')}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
>
|
||||
<Controls />
|
||||
</ReactFlow>
|
||||
<div className={styles.controls}>
|
||||
<button onClick={() => onLayout('TB')} style={{ marginRight: 10 }}>
|
||||
vertical layout
|
||||
</button>
|
||||
<button onClick={() => onLayout('LR')} style={{ marginRight: 10 }}>
|
||||
horizontal layout
|
||||
</button>
|
||||
<button onClick={() => unselect()}>unselect nodes</button>
|
||||
<button onClick={() => changeMarker()}>change marker</button>
|
||||
</div>
|
||||
</ReactFlowProvider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LayoutFlow;
|
||||
@@ -0,0 +1,135 @@
|
||||
import { Node, Edge, XYPosition, MarkerType } from 'reactflow';
|
||||
|
||||
const position: XYPosition = { x: 0, y: 0 };
|
||||
|
||||
const nodes: Node[] = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'input',
|
||||
data: { label: 'input' },
|
||||
position,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
data: { label: 'node 2' },
|
||||
position,
|
||||
},
|
||||
{
|
||||
id: '2a',
|
||||
data: { label: 'node 2a' },
|
||||
position,
|
||||
},
|
||||
{
|
||||
id: '2b',
|
||||
data: { label: 'node 2b' },
|
||||
position,
|
||||
},
|
||||
{
|
||||
id: '2c',
|
||||
data: { label: 'node 2c' },
|
||||
position,
|
||||
},
|
||||
{
|
||||
id: '2d',
|
||||
data: { label: 'node 2d' },
|
||||
position,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
data: { label: 'node 3' },
|
||||
position,
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
data: { label: 'node 4' },
|
||||
position,
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
data: { label: 'node 5' },
|
||||
position,
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
type: 'output',
|
||||
data: { label: 'output' },
|
||||
position,
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
type: 'output',
|
||||
data: { label: 'output' },
|
||||
position: { x: 400, y: 450 },
|
||||
},
|
||||
];
|
||||
|
||||
const edges: Edge[] = [
|
||||
{
|
||||
id: 'e12',
|
||||
source: '1',
|
||||
target: '2',
|
||||
type: 'smoothstep',
|
||||
markerEnd: { type: MarkerType.Arrow },
|
||||
},
|
||||
{
|
||||
id: 'e13',
|
||||
source: '1',
|
||||
target: '3',
|
||||
type: 'smoothstep',
|
||||
markerEnd: { type: MarkerType.Arrow },
|
||||
},
|
||||
{
|
||||
id: 'e22a',
|
||||
source: '2',
|
||||
target: '2a',
|
||||
type: 'smoothstep',
|
||||
markerEnd: { type: MarkerType.Arrow },
|
||||
},
|
||||
{
|
||||
id: 'e22b',
|
||||
source: '2',
|
||||
target: '2b',
|
||||
type: 'smoothstep',
|
||||
markerEnd: { type: MarkerType.Arrow },
|
||||
},
|
||||
{
|
||||
id: 'e22c',
|
||||
source: '2',
|
||||
target: '2c',
|
||||
type: 'smoothstep',
|
||||
markerEnd: { type: MarkerType.Arrow },
|
||||
},
|
||||
{
|
||||
id: 'e2c2d',
|
||||
source: '2c',
|
||||
target: '2d',
|
||||
type: 'smoothstep',
|
||||
markerEnd: { type: MarkerType.Arrow },
|
||||
},
|
||||
|
||||
{
|
||||
id: 'e45',
|
||||
source: '4',
|
||||
target: '5',
|
||||
type: 'smoothstep',
|
||||
markerEnd: { type: MarkerType.Arrow },
|
||||
},
|
||||
{
|
||||
id: 'e56',
|
||||
source: '5',
|
||||
target: '6',
|
||||
type: 'smoothstep',
|
||||
markerEnd: { type: MarkerType.Arrow },
|
||||
},
|
||||
{
|
||||
id: 'e57',
|
||||
source: '5',
|
||||
target: '7',
|
||||
type: 'smoothstep',
|
||||
markerEnd: { type: MarkerType.Arrow },
|
||||
},
|
||||
];
|
||||
|
||||
const nodesAndEdges = { nodes, edges };
|
||||
|
||||
export default nodesAndEdges;
|
||||
@@ -0,0 +1,11 @@
|
||||
.layoutflow {
|
||||
flex-grow: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.controls {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
z-index: 10;
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import React, { FC } from 'react';
|
||||
|
||||
import ReactFlow, {
|
||||
Background,
|
||||
addEdge,
|
||||
Node,
|
||||
Edge,
|
||||
Connection,
|
||||
ReactFlowProvider,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
MarkerType,
|
||||
} from 'reactflow';
|
||||
|
||||
import styles from './multiflows.module.css';
|
||||
|
||||
const initialNodes: Node[] = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'input',
|
||||
data: { label: 'Node 1' },
|
||||
position: { x: 250, y: 5 },
|
||||
className: 'light',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
data: { label: 'Node 2' },
|
||||
position: { x: 100, y: 100 },
|
||||
className: 'light',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
data: { label: 'Node 3' },
|
||||
position: { x: 400, y: 100 },
|
||||
className: 'light',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
data: { label: 'Node 4' },
|
||||
position: { x: 400, y: 200 },
|
||||
className: 'light',
|
||||
},
|
||||
];
|
||||
|
||||
const initialEdges: Edge[] = [
|
||||
{
|
||||
id: 'e1-2',
|
||||
source: '1',
|
||||
target: '2',
|
||||
animated: true,
|
||||
markerEnd: { type: MarkerType.Arrow },
|
||||
},
|
||||
{ id: 'e1-3', source: '1', target: '3' },
|
||||
];
|
||||
|
||||
const Flow: FC<{ id: string }> = ({ id }) => {
|
||||
const [nodes, , onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||
|
||||
const onConnect = (params: Edge | Connection) => setEdges((eds) => addEdge(params, eds));
|
||||
|
||||
return (
|
||||
<ReactFlowProvider>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
id={id}
|
||||
>
|
||||
<Background />
|
||||
</ReactFlow>
|
||||
</ReactFlowProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const MultiFlows: FC = () => (
|
||||
<div className={styles.multiflows}>
|
||||
<Flow id="flow-a" />
|
||||
<Flow id="flow-b" />
|
||||
</div>
|
||||
);
|
||||
|
||||
export default MultiFlows;
|
||||
@@ -0,0 +1,13 @@
|
||||
.multiflows {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.multiflows :global .react-flow {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.multiflows :global .react-flow:first-child {
|
||||
border-right: 2px solid #333;
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
import React, { useState, MouseEvent, useCallback } from 'react';
|
||||
import ReactFlow, {
|
||||
Controls,
|
||||
MiniMap,
|
||||
Background,
|
||||
addEdge,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
Node,
|
||||
Edge,
|
||||
ReactFlowInstance,
|
||||
Connection,
|
||||
} from 'reactflow';
|
||||
|
||||
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
|
||||
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
|
||||
const onEdgeClick = (_: MouseEvent, edge: Edge) => console.log('click', edge);
|
||||
|
||||
const initialNodes: Node[] = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'input',
|
||||
data: { label: 'Node 1' },
|
||||
position: { x: 250, y: 5 },
|
||||
className: 'light',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
data: { label: 'Node 2' },
|
||||
position: { x: 100, y: 100 },
|
||||
className: 'light',
|
||||
style: { backgroundColor: 'rgba(255, 0, 0, 0.8)', width: 200, height: 200 },
|
||||
},
|
||||
{
|
||||
id: '2a',
|
||||
data: { label: 'Node 2a' },
|
||||
position: { x: 10, y: 50 },
|
||||
parentNode: '2',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
data: { label: 'Node 3' },
|
||||
position: { x: 320, y: 100 },
|
||||
className: 'light',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
data: { label: 'Node 4' },
|
||||
position: { x: 320, y: 200 },
|
||||
className: 'light',
|
||||
style: { backgroundColor: 'rgba(255, 0, 0, 0.7)', width: 300, height: 300 },
|
||||
},
|
||||
{
|
||||
id: '4a',
|
||||
data: { label: 'Node 4a' },
|
||||
position: { x: 15, y: 65 },
|
||||
className: 'light',
|
||||
parentNode: '4',
|
||||
extent: 'parent',
|
||||
},
|
||||
{
|
||||
id: '4b',
|
||||
data: { label: 'Node 4b' },
|
||||
position: { x: 15, y: 120 },
|
||||
className: 'light',
|
||||
style: {
|
||||
backgroundColor: 'rgba(255, 0, 255, 0.7)',
|
||||
height: 150,
|
||||
width: 270,
|
||||
},
|
||||
parentNode: '4',
|
||||
},
|
||||
{
|
||||
id: '4b1',
|
||||
data: { label: 'Node 4b1' },
|
||||
position: { x: 20, y: 40 },
|
||||
className: 'light',
|
||||
parentNode: '4b',
|
||||
},
|
||||
{
|
||||
id: '4b2',
|
||||
data: { label: 'Node 4b2' },
|
||||
position: { x: 100, y: 100 },
|
||||
className: 'light',
|
||||
parentNode: '4b',
|
||||
},
|
||||
];
|
||||
|
||||
const initialEdges: Edge[] = [
|
||||
{ id: 'e1-2', source: '1', target: '2', animated: true },
|
||||
{ id: 'e1-3', source: '1', target: '3' },
|
||||
{ id: 'e2a-4a', source: '2a', target: '4a' },
|
||||
{ id: 'e3-4', source: '3', target: '4' },
|
||||
{ id: 'e3-4b', source: '3', target: '4b' },
|
||||
{ id: 'e4a-4b1', source: '4a', target: '4b1' },
|
||||
{ id: 'e4a-4b2', source: '4a', target: '4b2' },
|
||||
{ id: 'e4b1-4b2', source: '4b1', target: '4b2' },
|
||||
];
|
||||
|
||||
const NestedFlow = () => {
|
||||
const [rfInstance, setRfInstance] = useState<ReactFlowInstance | null>(null);
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||
|
||||
const onConnect = useCallback(
|
||||
(connection: Connection) => {
|
||||
setEdges((eds) => addEdge(connection, eds));
|
||||
},
|
||||
[setEdges]
|
||||
);
|
||||
const onInit = useCallback((reactFlowInstance: ReactFlowInstance) => setRfInstance(reactFlowInstance), []);
|
||||
|
||||
const updatePos = () => {
|
||||
setNodes((nds) => {
|
||||
return nds.map((n) => {
|
||||
n.position = {
|
||||
x: Math.random() * 400,
|
||||
y: Math.random() * 400,
|
||||
};
|
||||
|
||||
return n;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const logToObject = () => console.log(rfInstance?.toObject());
|
||||
const resetTransform = () => rfInstance?.setViewport({ x: 0, y: 0, zoom: 1 });
|
||||
|
||||
const toggleClassnames = () => {
|
||||
setNodes((nds) => {
|
||||
return nds.map((n) => {
|
||||
n.className = n.className === 'light' ? 'dark' : 'light';
|
||||
return n;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const toggleChildNodes = () => {
|
||||
setNodes((nds) => {
|
||||
return nds.map((n) => {
|
||||
n.hidden = !!n.parentNode && !n.hidden;
|
||||
return n;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onInit={onInit}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onNodeClick={onNodeClick}
|
||||
onEdgeClick={onEdgeClick}
|
||||
onConnect={onConnect}
|
||||
onNodeDragStop={onNodeDragStop}
|
||||
className="react-flow-basic-example"
|
||||
minZoom={0.2}
|
||||
maxZoom={4}
|
||||
onlyRenderVisibleElements={false}
|
||||
>
|
||||
<MiniMap />
|
||||
<Controls />
|
||||
<Background />
|
||||
|
||||
<div style={{ position: 'absolute', right: 10, top: 10, zIndex: 4 }}>
|
||||
<button onClick={resetTransform} style={{ marginRight: 5 }}>
|
||||
reset transform
|
||||
</button>
|
||||
<button onClick={updatePos} style={{ marginRight: 5 }}>
|
||||
change pos
|
||||
</button>
|
||||
<button onClick={toggleClassnames} style={{ marginRight: 5 }}>
|
||||
toggle classnames
|
||||
</button>
|
||||
<button style={{ marginRight: 5 }} onClick={toggleChildNodes}>
|
||||
toggleChildNodes
|
||||
</button>
|
||||
<button onClick={logToObject}>toObject</button>
|
||||
</div>
|
||||
</ReactFlow>
|
||||
);
|
||||
};
|
||||
|
||||
export default NestedFlow;
|
||||
@@ -0,0 +1,67 @@
|
||||
import React, { CSSProperties, useCallback } from 'react';
|
||||
|
||||
import ReactFlow, { addEdge, Node, Position, Connection, Edge, useNodesState, useEdgesState } from 'reactflow';
|
||||
|
||||
const initialNodes: Node[] = [
|
||||
{
|
||||
id: '1',
|
||||
sourcePosition: Position.Right,
|
||||
type: 'input',
|
||||
data: { label: 'Input' },
|
||||
position: { x: 0, y: 80 },
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'output',
|
||||
sourcePosition: Position.Right,
|
||||
targetPosition: Position.Left,
|
||||
data: { label: 'A Node' },
|
||||
position: { x: 250, y: 0 },
|
||||
},
|
||||
];
|
||||
|
||||
const initialEdges: Edge[] = [{ id: 'e1-2', source: '1', type: 'smoothstep', target: '2', animated: true }];
|
||||
|
||||
const buttonStyle: CSSProperties = {
|
||||
position: 'absolute',
|
||||
right: 10,
|
||||
top: 30,
|
||||
zIndex: 4,
|
||||
};
|
||||
|
||||
const NodeTypeChangeFlow = () => {
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||
const onConnect = useCallback((params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
|
||||
const changeType = () => {
|
||||
setNodes((nds) =>
|
||||
nds.map((node) => {
|
||||
if (node.type === 'input') {
|
||||
return node;
|
||||
}
|
||||
|
||||
return {
|
||||
...node,
|
||||
type: node.type === 'default' ? 'output' : 'default',
|
||||
};
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
fitView
|
||||
>
|
||||
<button onClick={changeType} style={buttonStyle}>
|
||||
change type
|
||||
</button>
|
||||
</ReactFlow>
|
||||
);
|
||||
};
|
||||
|
||||
export default NodeTypeChangeFlow;
|
||||
@@ -0,0 +1,88 @@
|
||||
import React, { useState, CSSProperties, FC, useCallback } from 'react';
|
||||
import ReactFlow, {
|
||||
addEdge,
|
||||
Node,
|
||||
Position,
|
||||
Connection,
|
||||
Edge,
|
||||
NodeProps,
|
||||
NodeTypes,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
} from 'reactflow';
|
||||
|
||||
const initialNodes: Node[] = [
|
||||
{
|
||||
id: '1',
|
||||
sourcePosition: Position.Right,
|
||||
type: 'input',
|
||||
data: { label: 'Input' },
|
||||
position: { x: 0, y: 80 },
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'a',
|
||||
sourcePosition: Position.Right,
|
||||
targetPosition: Position.Left,
|
||||
data: { label: 'A Node' },
|
||||
position: { x: 250, y: 0 },
|
||||
},
|
||||
];
|
||||
|
||||
const buttonStyle: CSSProperties = {
|
||||
position: 'absolute',
|
||||
right: 10,
|
||||
top: 30,
|
||||
zIndex: 4,
|
||||
};
|
||||
|
||||
const nodeStyles: CSSProperties = {
|
||||
padding: '10px 15px',
|
||||
border: '1px solid #ddd',
|
||||
};
|
||||
|
||||
const NodeA: FC<NodeProps> = () => {
|
||||
return <div style={nodeStyles}>A</div>;
|
||||
};
|
||||
|
||||
const NodeB: FC<NodeProps> = () => {
|
||||
return <div style={nodeStyles}>B</div>;
|
||||
};
|
||||
|
||||
type NodeTypesObject = {
|
||||
[key: string]: NodeTypes;
|
||||
};
|
||||
|
||||
const nodeTypesObjects: NodeTypesObject = {
|
||||
a: {
|
||||
a: NodeA,
|
||||
},
|
||||
b: {
|
||||
b: NodeB,
|
||||
},
|
||||
};
|
||||
|
||||
const NodeTypeChangeFlow = () => {
|
||||
const [nodeTypesId, setNodeTypesId] = useState<string>('a');
|
||||
const [nodes, , onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
||||
const onConnect = useCallback((params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
|
||||
const changeType = () => setNodeTypesId((nt) => (nt === 'a' ? 'b' : 'a'));
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
nodeTypes={nodeTypesObjects[nodeTypesId]}
|
||||
>
|
||||
<button onClick={changeType} style={buttonStyle}>
|
||||
change type
|
||||
</button>
|
||||
</ReactFlow>
|
||||
);
|
||||
};
|
||||
|
||||
export default NodeTypeChangeFlow;
|
||||
@@ -0,0 +1,237 @@
|
||||
import React, { MouseEvent as ReactMouseEvent, CSSProperties, useCallback } from 'react';
|
||||
import ReactFlow, {
|
||||
addEdge,
|
||||
Node,
|
||||
Viewport,
|
||||
SnapGrid,
|
||||
Connection,
|
||||
Edge,
|
||||
ReactFlowInstance,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
OnSelectionChangeParams,
|
||||
Controls,
|
||||
Background,
|
||||
MiniMap,
|
||||
} from 'reactflow';
|
||||
|
||||
const onNodeDragStart = (_: ReactMouseEvent, node: Node, nodes: Node[]) => console.log('drag start', node, nodes);
|
||||
const onNodeDrag = (_: ReactMouseEvent, node: Node, nodes: Node[]) => console.log('drag', node, nodes);
|
||||
const onNodeDragStop = (_: ReactMouseEvent, node: Node, nodes: Node[]) => console.log('drag stop', node, nodes);
|
||||
const onNodeDoubleClick = (_: ReactMouseEvent, node: Node) => console.log('node double click', node);
|
||||
const onPaneClick = (event: ReactMouseEvent) => console.log('pane click', event);
|
||||
const onPaneScroll = (event?: ReactMouseEvent) => console.log('pane scroll', event);
|
||||
const onPaneContextMenu = (event: ReactMouseEvent) => console.log('pane context menu', event);
|
||||
const onSelectionDrag = (_: ReactMouseEvent, nodes: Node[]) => console.log('selection drag', nodes);
|
||||
const onSelectionDragStart = (_: ReactMouseEvent, nodes: Node[]) => console.log('selection drag start', nodes);
|
||||
const onSelectionDragStop = (_: ReactMouseEvent, nodes: Node[]) => console.log('selection drag stop', nodes);
|
||||
const onSelectionContextMenu = (event: ReactMouseEvent, nodes: Node[]) => {
|
||||
event.preventDefault();
|
||||
console.log('selection context menu', nodes);
|
||||
};
|
||||
const onNodeClick = (_: ReactMouseEvent, node: Node) => console.log('node click:', node);
|
||||
|
||||
const onSelectionChange = ({ nodes, edges }: OnSelectionChangeParams) => console.log('selection change', nodes, edges);
|
||||
const onInit = (reactFlowInstance: ReactFlowInstance) => {
|
||||
console.log('pane ready:', reactFlowInstance);
|
||||
};
|
||||
|
||||
const onMoveStart = (_: MouseEvent | TouchEvent, viewport: Viewport) => console.log('zoom/move start', viewport);
|
||||
const onMoveEnd = (_: MouseEvent | TouchEvent, viewport: Viewport) => console.log('zoom/move end', viewport);
|
||||
const onEdgeContextMenu = (_: ReactMouseEvent, edge: Edge) => console.log('edge context menu', edge);
|
||||
const onEdgeMouseEnter = (_: ReactMouseEvent, edge: Edge) => console.log('edge mouse enter', edge);
|
||||
const onEdgeMouseMove = (_: ReactMouseEvent, edge: Edge) => console.log('edge mouse move', edge);
|
||||
const onEdgeMouseLeave = (_: ReactMouseEvent, edge: Edge) => console.log('edge mouse leave', edge);
|
||||
const onEdgeDoubleClick = (_: ReactMouseEvent, edge: Edge) => console.log('edge double click', edge);
|
||||
const onNodesDelete = (nodes: Node[]) => console.log('nodes delete', nodes);
|
||||
const onEdgesDelete = (edges: Edge[]) => console.log('edges delete', edges);
|
||||
const onPaneMouseMove = (e: ReactMouseEvent) => console.log('pane move', e.clientX, e.clientY);
|
||||
|
||||
const initialNodes: Node[] = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'input',
|
||||
data: {
|
||||
label: (
|
||||
<>
|
||||
Welcome to <strong>React Flow!</strong>
|
||||
</>
|
||||
),
|
||||
},
|
||||
position: { x: 250, y: 0 },
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
data: {
|
||||
label: (
|
||||
<>
|
||||
This is a <strong>default node</strong>
|
||||
</>
|
||||
),
|
||||
},
|
||||
position: { x: 100, y: 100 },
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
data: {
|
||||
label: (
|
||||
<>
|
||||
This one has a <strong>custom style</strong>
|
||||
</>
|
||||
),
|
||||
},
|
||||
position: { x: 400, y: 100 },
|
||||
style: {
|
||||
background: '#D6D5E6',
|
||||
color: '#333',
|
||||
border: '1px solid #222138',
|
||||
width: 180,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
position: { x: 250, y: 200 },
|
||||
data: {
|
||||
label: (
|
||||
<>
|
||||
You can find the docs on{' '}
|
||||
<a href="https://github.com/wbkd/react-flow" target="_blank" rel="noopener noreferrer">
|
||||
Github
|
||||
</a>
|
||||
</>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
data: {
|
||||
label: (
|
||||
<>
|
||||
Or check out the other <strong>examples</strong>
|
||||
</>
|
||||
),
|
||||
},
|
||||
position: { x: 250, y: 325 },
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
type: 'output',
|
||||
data: {
|
||||
label: (
|
||||
<>
|
||||
An <strong>output node (not deletable)</strong>
|
||||
</>
|
||||
),
|
||||
},
|
||||
position: { x: 100, y: 480 },
|
||||
deletable: false,
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
type: 'output',
|
||||
data: { label: 'Another output node' },
|
||||
position: { x: 400, y: 450 },
|
||||
},
|
||||
];
|
||||
|
||||
const initialEdges: Edge[] = [
|
||||
{ id: 'e1-2', source: '1', target: '2', label: 'this is an edge label' },
|
||||
{ id: 'e1-3', source: '1', target: '3' },
|
||||
{
|
||||
id: 'e3-4',
|
||||
source: '3',
|
||||
target: '4',
|
||||
animated: true,
|
||||
label: 'animated edge',
|
||||
},
|
||||
{ id: 'e4-5', source: '4', target: '5', label: 'edge with arrow head' },
|
||||
{
|
||||
id: 'e5-6',
|
||||
source: '5',
|
||||
target: '6',
|
||||
type: 'smoothstep',
|
||||
deletable: false,
|
||||
label: 'smooth step edge (not deletable)',
|
||||
},
|
||||
{
|
||||
id: 'e5-7',
|
||||
source: '5',
|
||||
target: '7',
|
||||
type: 'step',
|
||||
style: { stroke: '#f6ab6c' },
|
||||
label: 'a step edge',
|
||||
animated: true,
|
||||
labelStyle: { fill: '#f6ab6c', fontWeight: 700 },
|
||||
},
|
||||
];
|
||||
|
||||
const connectionLineStyle: CSSProperties = { stroke: '#ddd' };
|
||||
const snapGrid: SnapGrid = [25, 25];
|
||||
|
||||
const nodeStrokeColor = (n: Node): string => {
|
||||
if (n.style?.background) return n.style.background as string;
|
||||
if (n.type === 'input') return '#0041d0';
|
||||
if (n.type === 'output') return '#ff0072';
|
||||
if (n.type === 'default') return '#1a192b';
|
||||
|
||||
return '#eee';
|
||||
};
|
||||
|
||||
const nodeColor = (n: Node): string => {
|
||||
if (n.style?.background) return n.style.background as string;
|
||||
|
||||
return '#fff';
|
||||
};
|
||||
|
||||
const OverviewFlow = () => {
|
||||
const [nodes, , onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||
const onConnect = useCallback((params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onNodeClick={onNodeClick}
|
||||
onConnect={onConnect}
|
||||
onPaneClick={onPaneClick}
|
||||
onPaneScroll={onPaneScroll}
|
||||
onPaneContextMenu={onPaneContextMenu}
|
||||
onNodeDragStart={onNodeDragStart}
|
||||
onNodeDrag={onNodeDrag}
|
||||
onNodeDragStop={onNodeDragStop}
|
||||
onNodeDoubleClick={onNodeDoubleClick}
|
||||
onSelectionDragStart={onSelectionDragStart}
|
||||
onSelectionDrag={onSelectionDrag}
|
||||
onSelectionDragStop={onSelectionDragStop}
|
||||
onSelectionContextMenu={onSelectionContextMenu}
|
||||
onSelectionChange={onSelectionChange}
|
||||
onMoveStart={onMoveStart}
|
||||
onMoveEnd={onMoveEnd}
|
||||
onInit={onInit}
|
||||
connectionLineStyle={connectionLineStyle}
|
||||
snapToGrid={true}
|
||||
snapGrid={snapGrid}
|
||||
onEdgeContextMenu={onEdgeContextMenu}
|
||||
onEdgeMouseEnter={onEdgeMouseEnter}
|
||||
onEdgeMouseMove={onEdgeMouseMove}
|
||||
onEdgeMouseLeave={onEdgeMouseLeave}
|
||||
onEdgeDoubleClick={onEdgeDoubleClick}
|
||||
fitView
|
||||
fitViewOptions={{ padding: 0.2 }}
|
||||
attributionPosition="top-right"
|
||||
maxZoom={Infinity}
|
||||
onNodesDelete={onNodesDelete}
|
||||
onEdgesDelete={onEdgesDelete}
|
||||
onPaneMouseMove={onPaneMouseMove}
|
||||
>
|
||||
<MiniMap nodeStrokeColor={nodeStrokeColor} nodeColor={nodeColor} nodeBorderRadius={2} />
|
||||
<Controls />
|
||||
<Background color="#aaa" gap={25} />
|
||||
</ReactFlow>
|
||||
);
|
||||
};
|
||||
|
||||
export default OverviewFlow;
|
||||
@@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import { useStore, useStoreApi } from 'reactflow';
|
||||
|
||||
import styles from './provider.module.css';
|
||||
|
||||
const Sidebar = () => {
|
||||
const store = useStoreApi();
|
||||
const nodeInternals = useStore((store) => store.nodeInternals);
|
||||
const transform = useStore((store) => store.transform);
|
||||
|
||||
const selectAll = () => {
|
||||
nodeInternals.forEach((node) => (node.selected = true));
|
||||
store.setState({ nodeInternals: new Map(nodeInternals) });
|
||||
};
|
||||
|
||||
return (
|
||||
<aside className={styles.aside}>
|
||||
<div className={styles.description}>
|
||||
This is an example of how you can access the internal state outside of the ReactFlow component.
|
||||
</div>
|
||||
<div className={styles.title}>Zoom & pan transform</div>
|
||||
<div className={styles.transform}>
|
||||
[{transform[0].toFixed(2)}, {transform[1].toFixed(2)}, {transform[2].toFixed(2)}]
|
||||
</div>
|
||||
<div className={styles.title}>Nodes</div>
|
||||
{Array.from(nodeInternals).map(([, node]) => (
|
||||
<div key={node.id}>
|
||||
Node {node.id} - x: {node.position.x.toFixed(2)}, y: {node.position.y.toFixed(2)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className={styles.selectall}>
|
||||
<button onClick={selectAll}>select all nodes</button>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
@@ -0,0 +1,67 @@
|
||||
import React, { MouseEvent, useCallback } from 'react';
|
||||
import ReactFlow, {
|
||||
Controls,
|
||||
ReactFlowProvider,
|
||||
addEdge,
|
||||
Node,
|
||||
Connection,
|
||||
Edge,
|
||||
ConnectionMode,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
ReactFlowInstance,
|
||||
} from 'reactflow';
|
||||
|
||||
import Sidebar from './Sidebar';
|
||||
|
||||
import styles from './provider.module.css';
|
||||
|
||||
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
|
||||
const onInit = (reactFlowInstance: ReactFlowInstance) => console.log('pane ready:', reactFlowInstance);
|
||||
|
||||
const initialNodes: Node[] = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'input',
|
||||
data: { label: 'Node 1' },
|
||||
position: { x: 250, y: 5 },
|
||||
},
|
||||
{ id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 } },
|
||||
{ id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 } },
|
||||
{ id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 } },
|
||||
];
|
||||
|
||||
const initialEdges: Edge[] = [
|
||||
{ id: 'e1-2', source: '1', target: '2', animated: true },
|
||||
{ id: 'e1-3', source: '1', target: '3' },
|
||||
];
|
||||
|
||||
const ProviderFlow = () => {
|
||||
const [nodes, , onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||
const onConnect = useCallback((params: Edge | Connection) => setEdges((els) => addEdge(params, els)), [setEdges]);
|
||||
|
||||
return (
|
||||
<div className={styles.providerflow}>
|
||||
<ReactFlowProvider>
|
||||
<Sidebar />
|
||||
<div className={styles.wrapper}>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onNodeClick={onNodeClick}
|
||||
onConnect={onConnect}
|
||||
onInit={onInit}
|
||||
connectionMode={ConnectionMode.Loose}
|
||||
>
|
||||
<Controls />
|
||||
</ReactFlow>
|
||||
</div>
|
||||
</ReactFlowProvider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProviderFlow;
|
||||
@@ -0,0 +1,45 @@
|
||||
.providerflow {
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.aside {
|
||||
border-right: 1px solid #eee;
|
||||
padding: 15px 10px;
|
||||
font-size: 12px;
|
||||
background: #fcfcfc;
|
||||
}
|
||||
|
||||
.description {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 700;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.transform {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.selectall {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.providerflow {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.aside {
|
||||
width: 20%;
|
||||
max-width: 250px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import React, { memo, useCallback, Dispatch, FC } from 'react';
|
||||
import { useReactFlow, Edge, Node, ReactFlowJsonObject } from 'reactflow';
|
||||
import localforage from 'localforage';
|
||||
|
||||
import styles from './save.module.css';
|
||||
|
||||
localforage.config({
|
||||
name: 'react-flow',
|
||||
storeName: 'flows',
|
||||
});
|
||||
|
||||
const flowKey = 'example-flow';
|
||||
|
||||
const getNodeId = () => `randomnode_${+new Date()}`;
|
||||
|
||||
type ControlsProps = {
|
||||
setNodes: Dispatch<React.SetStateAction<Node<any>[]>>;
|
||||
setEdges: Dispatch<React.SetStateAction<Edge<any>[]>>;
|
||||
};
|
||||
|
||||
const Controls: FC<ControlsProps> = ({ setNodes, setEdges }) => {
|
||||
const { setViewport, toObject } = useReactFlow();
|
||||
|
||||
const onSave = useCallback(() => {
|
||||
const flow = toObject();
|
||||
localforage.setItem(flowKey, flow);
|
||||
}, [toObject]);
|
||||
|
||||
const onRestore = useCallback(() => {
|
||||
const restoreFlow = async () => {
|
||||
const flow: ReactFlowJsonObject | null = await localforage.getItem(flowKey);
|
||||
|
||||
if (flow) {
|
||||
const { x, y, zoom } = flow.viewport;
|
||||
|
||||
setNodes(flow.nodes || []);
|
||||
setEdges(flow.edges || []);
|
||||
setViewport({ x, y, zoom: zoom || 0 });
|
||||
}
|
||||
};
|
||||
|
||||
restoreFlow();
|
||||
}, [setNodes, setEdges, setViewport]);
|
||||
|
||||
const onAdd = useCallback(() => {
|
||||
const newNode = {
|
||||
id: `random_node-${getNodeId()}`,
|
||||
data: { label: 'Added node' },
|
||||
position: {
|
||||
x: Math.random() * window.innerWidth - 100,
|
||||
y: Math.random() * window.innerHeight,
|
||||
},
|
||||
};
|
||||
setNodes((nds) => nds.concat(newNode));
|
||||
}, [setNodes]);
|
||||
|
||||
return (
|
||||
<div className={styles.controls}>
|
||||
<button className={styles.button} onClick={onSave}>
|
||||
save
|
||||
</button>
|
||||
<button className={styles.button} onClick={onRestore}>
|
||||
restore
|
||||
</button>
|
||||
<button className={styles.button} onClick={onAdd}>
|
||||
add node
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Controls);
|
||||
@@ -0,0 +1,32 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import ReactFlow, { ReactFlowProvider, Node, addEdge, Connection, Edge, useNodesState, useEdgesState } from 'reactflow';
|
||||
import Controls from './Controls';
|
||||
|
||||
const initialNodes: Node[] = [
|
||||
{ id: '1', data: { label: 'Node 1' }, position: { x: 100, y: 100 } },
|
||||
{ id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 200 } },
|
||||
];
|
||||
|
||||
const initialEdges: Edge[] = [{ id: 'e1-2', source: '1', target: '2' }];
|
||||
|
||||
const SaveRestore = () => {
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||
const onConnect = useCallback((params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
|
||||
|
||||
return (
|
||||
<ReactFlowProvider>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
>
|
||||
<Controls setNodes={setNodes} setEdges={setEdges} />
|
||||
</ReactFlow>
|
||||
</ReactFlowProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default SaveRestore;
|
||||
@@ -0,0 +1,11 @@
|
||||
.controls {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
z-index: 4;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.button {
|
||||
margin-left: 5px;
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import React, { useState, CSSProperties, useCallback } from 'react';
|
||||
import ReactFlow, {
|
||||
ReactFlowInstance,
|
||||
Edge,
|
||||
Node,
|
||||
NodeChange,
|
||||
applyNodeChanges,
|
||||
Connection,
|
||||
addEdge,
|
||||
applyEdgeChanges,
|
||||
EdgeChange,
|
||||
Controls,
|
||||
Background,
|
||||
MiniMap,
|
||||
} from 'reactflow';
|
||||
|
||||
import { getNodesAndEdges } from './utils';
|
||||
|
||||
const buttonWrapperStyles: CSSProperties = {
|
||||
position: 'absolute',
|
||||
right: 10,
|
||||
top: 10,
|
||||
zIndex: 4,
|
||||
};
|
||||
|
||||
const onInit = (reactFlowInstance: ReactFlowInstance) => {
|
||||
reactFlowInstance.fitView();
|
||||
console.log(reactFlowInstance.getNodes());
|
||||
};
|
||||
|
||||
const { nodes: initialNodes, edges: initialEdges } = getNodesAndEdges(25, 25);
|
||||
|
||||
const StressFlow = () => {
|
||||
const [nodes, setNodes] = useState<Node[]>(initialNodes);
|
||||
const [edges, setEdges] = useState<Edge[]>(initialEdges);
|
||||
const onConnect = useCallback((connection: Connection) => {
|
||||
setEdges((eds) => addEdge(connection, eds));
|
||||
}, []);
|
||||
const updatePos = () => {
|
||||
setNodes((nds) => {
|
||||
return nds.map((n) => {
|
||||
return {
|
||||
...n,
|
||||
position: {
|
||||
x: Math.random() * window.innerWidth,
|
||||
y: Math.random() * window.innerHeight,
|
||||
},
|
||||
};
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const updateElements = () => {
|
||||
const grid = Math.ceil(Math.random() * 10);
|
||||
const initialElements = getNodesAndEdges(grid, grid);
|
||||
setNodes(initialElements.nodes);
|
||||
setEdges(initialElements.edges);
|
||||
};
|
||||
|
||||
const onNodesChange = useCallback((changes: NodeChange[]) => {
|
||||
setNodes((ns) => applyNodeChanges(changes, ns));
|
||||
}, []);
|
||||
|
||||
const onEdgeChange = useCallback((changes: EdgeChange[]) => {
|
||||
setEdges((es) => applyEdgeChanges(changes, es));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onInit={onInit}
|
||||
onConnect={onConnect}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgeChange}
|
||||
>
|
||||
<MiniMap />
|
||||
<Controls />
|
||||
<Background />
|
||||
|
||||
<div style={buttonWrapperStyles}>
|
||||
<button onClick={updatePos} style={{ marginRight: 5 }}>
|
||||
change pos
|
||||
</button>
|
||||
<button onClick={updateElements}>update elements</button>
|
||||
</div>
|
||||
</ReactFlow>
|
||||
);
|
||||
};
|
||||
|
||||
export default StressFlow;
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Node, Edge } from 'reactflow';
|
||||
|
||||
type ElementsCollection = {
|
||||
nodes: Node[];
|
||||
edges: Edge[];
|
||||
};
|
||||
|
||||
export function getNodesAndEdges(xElements = 10, yElements = 10): ElementsCollection {
|
||||
const initialNodes = [];
|
||||
const initialEdges: Edge[] = [];
|
||||
let nodeId = 1;
|
||||
let recentNodeId = null;
|
||||
|
||||
for (let y = 0; y < yElements; y++) {
|
||||
for (let x = 0; x < xElements; x++) {
|
||||
const position = { x: x * 100, y: y * 50 };
|
||||
const data = { label: `Node ${nodeId}` };
|
||||
const node = {
|
||||
id: nodeId.toString(),
|
||||
style: { width: 50, fontSize: 11 },
|
||||
data,
|
||||
position,
|
||||
};
|
||||
initialNodes.push(node);
|
||||
|
||||
if (recentNodeId && nodeId <= xElements * yElements) {
|
||||
initialEdges.push({
|
||||
id: `${x}-${y}`,
|
||||
source: recentNodeId.toString(),
|
||||
target: nodeId.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
recentNodeId = nodeId;
|
||||
nodeId++;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
nodes: initialNodes,
|
||||
edges: initialEdges,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import React, { memo, FC, CSSProperties } from 'react';
|
||||
|
||||
import { Handle, NodeProps, Position } from 'reactflow';
|
||||
|
||||
const infoStyle: CSSProperties = { fontSize: 11 };
|
||||
const idStyle: CSSProperties = {
|
||||
fontSize: 10,
|
||||
color: '#888899',
|
||||
position: 'absolute',
|
||||
top: 2,
|
||||
left: 2,
|
||||
};
|
||||
|
||||
const DebugNode: FC<NodeProps> = ({ zIndex, xPos, yPos, id }) => {
|
||||
return (
|
||||
<>
|
||||
<Handle type="target" position={Position.Top} />
|
||||
<div style={idStyle}>{id}</div>
|
||||
<div style={infoStyle}>
|
||||
x:{Math.round(xPos || 0)} y:{Math.round(yPos || 0)} z:{zIndex}
|
||||
</div>
|
||||
<Handle type="source" position={Position.Bottom} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(DebugNode);
|
||||
@@ -0,0 +1,232 @@
|
||||
import React, { useState, MouseEvent, useCallback } from 'react';
|
||||
import ReactFlow, {
|
||||
addEdge,
|
||||
Node,
|
||||
Edge,
|
||||
ReactFlowInstance,
|
||||
Connection,
|
||||
MarkerType,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
Controls,
|
||||
MiniMap,
|
||||
Background,
|
||||
} from 'reactflow';
|
||||
|
||||
import DebugNode from './DebugNode';
|
||||
|
||||
const onNodeDrag = (_: MouseEvent, node: Node, nodes: Node[]) => console.log('drag', node, nodes);
|
||||
const onNodeDragStop = (_: MouseEvent, node: Node, nodes: Node[]) => console.log('drag stop', node, nodes);
|
||||
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
|
||||
const onEdgeClick = (_: MouseEvent, edge: Edge) => console.log('click', edge);
|
||||
|
||||
const defaultViewport = { x: 0, y: 0, zoom: 1.5 };
|
||||
|
||||
const initialNodes: Node[] = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'input',
|
||||
data: { label: 'Node 1' },
|
||||
position: { x: 250, y: 5 },
|
||||
className: 'light',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
data: { label: 'Node 4' },
|
||||
position: { x: 100, y: 200 },
|
||||
className: 'light',
|
||||
style: {
|
||||
backgroundColor: 'rgba(255,50, 50, 0.5)',
|
||||
width: 500,
|
||||
height: 300,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '4a',
|
||||
data: { label: 'Node 4a' },
|
||||
position: { x: 15, y: 15 },
|
||||
className: 'light',
|
||||
parentNode: '4',
|
||||
extent: [
|
||||
[0, 0],
|
||||
[100, 100],
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '4b',
|
||||
data: { label: 'Node 4b' },
|
||||
position: { x: 100, y: 60 },
|
||||
className: 'light',
|
||||
style: {
|
||||
backgroundColor: 'rgba(50, 50, 255, 0.5)',
|
||||
height: 200,
|
||||
width: 300,
|
||||
},
|
||||
parentNode: '4',
|
||||
},
|
||||
{
|
||||
id: '4b1',
|
||||
data: { label: 'Node 4b1' },
|
||||
position: { x: 40, y: 20 },
|
||||
className: 'light',
|
||||
parentNode: '4b',
|
||||
},
|
||||
{
|
||||
id: '4b2',
|
||||
data: { label: 'Node 4b2' },
|
||||
position: { x: 20, y: 100 },
|
||||
className: 'light',
|
||||
parentNode: '4b',
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
type: 'group',
|
||||
data: { label: 'Node 5' },
|
||||
position: { x: 650, y: 250 },
|
||||
className: 'light',
|
||||
style: { width: 400, height: 150 },
|
||||
zIndex: 1000,
|
||||
},
|
||||
{
|
||||
id: '5a',
|
||||
data: { label: 'Node 5a' },
|
||||
position: { x: 25, y: 50 },
|
||||
className: 'light',
|
||||
parentNode: '5',
|
||||
extent: 'parent',
|
||||
},
|
||||
{
|
||||
id: '5b',
|
||||
data: { label: 'Node 5b' },
|
||||
position: { x: 225, y: 50 },
|
||||
className: 'light',
|
||||
parentNode: '5',
|
||||
expandParent: true,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
data: { label: 'Node 2' },
|
||||
position: { x: 100, y: 100 },
|
||||
className: 'light',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
data: { label: 'Node 3' },
|
||||
position: { x: 400, y: 100 },
|
||||
className: 'light',
|
||||
},
|
||||
];
|
||||
|
||||
const initialEdges: Edge[] = [
|
||||
{
|
||||
id: 'e1-2',
|
||||
source: '1',
|
||||
target: '2',
|
||||
markerEnd: {
|
||||
type: MarkerType.Arrow,
|
||||
strokeWidth: 2,
|
||||
width: 15,
|
||||
height: 15,
|
||||
color: '#f00',
|
||||
},
|
||||
},
|
||||
{ id: 'e1-3', source: '1', target: '3' },
|
||||
{ id: 'e3-4', source: '3', target: '4', zIndex: 100 },
|
||||
{ id: 'e3-4b', source: '3', target: '4b' },
|
||||
{ id: 'e4a-4b1', source: '4a', target: '4b1' },
|
||||
{ id: 'e4a-4b2', source: '4a', target: '4b2', zIndex: 100 },
|
||||
{ id: 'e4b1-4b2', source: '4b1', target: '4b2' },
|
||||
];
|
||||
|
||||
const nodeTypes = {
|
||||
default: DebugNode,
|
||||
};
|
||||
|
||||
const Subflow = () => {
|
||||
const [rfInstance, setRfInstance] = useState<ReactFlowInstance | null>(null);
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||
|
||||
const onConnect = useCallback((connection: Connection) => setEdges((eds) => addEdge(connection, eds)), [setEdges]);
|
||||
const onInit = useCallback((reactFlowInstance: ReactFlowInstance) => setRfInstance(reactFlowInstance), []);
|
||||
|
||||
const updatePos = () => {
|
||||
setNodes((nds) => {
|
||||
return nds.map((n) => {
|
||||
if (!n.parentNode) {
|
||||
n.position = {
|
||||
x: Math.random() * 400,
|
||||
y: Math.random() * 400,
|
||||
};
|
||||
}
|
||||
|
||||
return n;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const logToObject = () => console.log(rfInstance?.toObject());
|
||||
const resetTransform = () => rfInstance?.setViewport({ x: 0, y: 0, zoom: 1 });
|
||||
|
||||
const toggleClassnames = () => {
|
||||
setNodes((nds) => {
|
||||
return nds.map((n) => {
|
||||
n.className = n.className === 'light' ? 'dark' : 'light';
|
||||
return n;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const toggleChildNodes = () => {
|
||||
setNodes((nds) => {
|
||||
return nds.map((n) => {
|
||||
n.hidden = !!n.parentNode && !n.hidden;
|
||||
return n;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onInit={onInit}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onNodeClick={onNodeClick}
|
||||
onEdgeClick={onEdgeClick}
|
||||
onConnect={onConnect}
|
||||
onNodeDrag={onNodeDrag}
|
||||
onNodeDragStop={onNodeDragStop}
|
||||
className="react-flow-basic-example"
|
||||
defaultViewport={defaultViewport}
|
||||
minZoom={0.2}
|
||||
maxZoom={4}
|
||||
onlyRenderVisibleElements={false}
|
||||
nodeTypes={nodeTypes}
|
||||
fitView
|
||||
>
|
||||
<MiniMap />
|
||||
<Controls />
|
||||
<Background />
|
||||
|
||||
<div style={{ position: 'absolute', right: 10, top: 10, zIndex: 4 }}>
|
||||
<button onClick={resetTransform} style={{ marginRight: 5 }}>
|
||||
reset transform
|
||||
</button>
|
||||
<button onClick={updatePos} style={{ marginRight: 5 }}>
|
||||
change pos
|
||||
</button>
|
||||
<button onClick={toggleClassnames} style={{ marginRight: 5 }}>
|
||||
toggle classnames
|
||||
</button>
|
||||
<button style={{ marginRight: 5 }} onClick={toggleChildNodes}>
|
||||
toggleChildNodes
|
||||
</button>
|
||||
<button onClick={logToObject}>toObject</button>
|
||||
</div>
|
||||
</ReactFlow>
|
||||
);
|
||||
};
|
||||
|
||||
export default Subflow;
|
||||
@@ -0,0 +1,126 @@
|
||||
import React, { MouseEvent, useCallback } from 'react';
|
||||
import ReactFlow, { addEdge, Node, Connection, Edge, useNodesState, useEdgesState } from 'reactflow';
|
||||
|
||||
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
|
||||
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
|
||||
|
||||
const nodesA: Node[] = [
|
||||
{
|
||||
id: '1a',
|
||||
type: 'input',
|
||||
data: { label: 'Node 1' },
|
||||
position: { x: 250, y: 5 },
|
||||
className: 'light',
|
||||
ariaLabel: 'Input Node 1',
|
||||
},
|
||||
{
|
||||
id: '2a',
|
||||
data: { label: 'Node 2' },
|
||||
position: { x: 100, y: 100 },
|
||||
className: 'light',
|
||||
ariaLabel: 'Default Node 2',
|
||||
},
|
||||
{
|
||||
id: '3a',
|
||||
data: { label: 'Node 3' },
|
||||
position: { x: 400, y: 100 },
|
||||
className: 'light',
|
||||
},
|
||||
{
|
||||
id: '4a',
|
||||
data: { label: 'Node 4' },
|
||||
position: { x: 400, y: 200 },
|
||||
className: 'light',
|
||||
},
|
||||
];
|
||||
|
||||
const edgesA: Edge[] = [
|
||||
{ id: 'e1-2', source: '1a', target: '2a', ariaLabel: null },
|
||||
{ id: 'e1-3', source: '1a', target: '3a' },
|
||||
];
|
||||
|
||||
const nodesB: Node[] = [
|
||||
{
|
||||
id: 'inputb',
|
||||
type: 'input',
|
||||
data: { label: 'Input' },
|
||||
position: { x: 300, y: 5 },
|
||||
className: 'light',
|
||||
ariaLabel: 'Input Node',
|
||||
},
|
||||
{
|
||||
id: '1b',
|
||||
data: { label: 'Node 1' },
|
||||
position: { x: 0, y: 100 },
|
||||
className: 'light',
|
||||
ariaLabel: 'Node with id 1',
|
||||
},
|
||||
{
|
||||
id: '2b',
|
||||
data: { label: 'Node 2' },
|
||||
position: { x: 200, y: 100 },
|
||||
className: 'light',
|
||||
ariaLabel: 'Node with id 2',
|
||||
},
|
||||
{
|
||||
id: '3b',
|
||||
data: { label: 'Node 3' },
|
||||
position: { x: 400, y: 100 },
|
||||
className: 'light',
|
||||
},
|
||||
{
|
||||
id: '4b',
|
||||
data: { label: 'Node 4' },
|
||||
position: { x: 600, y: 100 },
|
||||
className: 'light',
|
||||
},
|
||||
];
|
||||
|
||||
const edgesB: Edge[] = [
|
||||
{ id: 'e1b', source: 'inputb', target: '1b', ariaLabel: 'edge to connect' },
|
||||
{ id: 'e2b', source: 'inputb', target: '2b' },
|
||||
{ id: 'e3b', source: 'inputb', target: '3b' },
|
||||
{ id: 'e4b', source: 'inputb', target: '4b' },
|
||||
];
|
||||
|
||||
const BasicFlow = () => {
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(nodesA);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(edgesA);
|
||||
|
||||
const onConnect = useCallback((params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onNodeClick={onNodeClick}
|
||||
onConnect={onConnect}
|
||||
onNodeDragStop={onNodeDragStop}
|
||||
disableKeyboardA11y
|
||||
>
|
||||
<div style={{ position: 'absolute', right: 10, top: 10, zIndex: 4 }}>
|
||||
<button
|
||||
onClick={() => {
|
||||
setNodes(nodesA);
|
||||
setEdges(edgesA);
|
||||
}}
|
||||
style={{ marginRight: 5 }}
|
||||
>
|
||||
flow a
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setNodes(nodesB);
|
||||
setEdges(edgesB);
|
||||
}}
|
||||
>
|
||||
flow b
|
||||
</button>
|
||||
</div>
|
||||
</ReactFlow>
|
||||
);
|
||||
};
|
||||
|
||||
export default BasicFlow;
|
||||
@@ -0,0 +1,50 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import ReactFlow, { Node, Edge, useNodesState, useEdgesState, Position, Connection, addEdge } from 'reactflow';
|
||||
|
||||
import styles from './touch-device.module.css';
|
||||
|
||||
const initialNodes: Node[] = [
|
||||
{
|
||||
id: '1',
|
||||
data: { label: 'Node 1' },
|
||||
position: { x: 100, y: 100 },
|
||||
sourcePosition: Position.Right,
|
||||
targetPosition: Position.Left,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
data: { label: 'Node 2' },
|
||||
position: { x: 300, y: 100 },
|
||||
sourcePosition: Position.Right,
|
||||
targetPosition: Position.Left,
|
||||
},
|
||||
];
|
||||
|
||||
const initialEdges: Edge[] = [];
|
||||
|
||||
const TouchDeviceFlow = () => {
|
||||
const [nodes, , onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||
const onConnect = useCallback((connection: Connection) => setEdges((eds) => addEdge(connection, eds)), [setEdges]);
|
||||
const onConnectStart = useCallback(() => console.log('connect start'), []);
|
||||
const onConnectEnd = useCallback(() => console.log('connect end'), []);
|
||||
const onClickConnectStart = useCallback(() => console.log('click connect start'), []);
|
||||
const onClickConnectEnd = useCallback(() => console.log('click connect end'), []);
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onConnect={onConnect}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnectStart={onConnectStart}
|
||||
onConnectEnd={onConnectEnd}
|
||||
onClickConnectStart={onClickConnectStart}
|
||||
onClickConnectEnd={onClickConnectEnd}
|
||||
className={styles.flow}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default TouchDeviceFlow;
|
||||
@@ -0,0 +1,19 @@
|
||||
.flow :global .react-flow__handle {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 3px;
|
||||
background-color: #9f7aea;
|
||||
}
|
||||
|
||||
.flow :global .react-flow__handle.connecting {
|
||||
animation: bounce 1600ms infinite ease-out;
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0% {
|
||||
transform: translate(0, -50%) scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: translate(0, -50%) scale(1.1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import React, { memo, FC, CSSProperties } from 'react';
|
||||
|
||||
import { Handle, Position, NodeProps } from 'reactflow';
|
||||
|
||||
const nodeStyles: CSSProperties = {
|
||||
padding: '10px 15px',
|
||||
border: '1px solid #ddd',
|
||||
};
|
||||
|
||||
const CustomNode: FC<NodeProps> = ({ id }) => {
|
||||
return (
|
||||
<div style={nodeStyles}>
|
||||
<div>node {id}</div>
|
||||
<Handle type="source" id="left" position={Position.Left} />
|
||||
<Handle type="source" id="right" position={Position.Right} />
|
||||
<Handle type="source" id="top" position={Position.Top} />
|
||||
<Handle type="source" id="bottom" position={Position.Bottom} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(CustomNode);
|
||||
@@ -0,0 +1,226 @@
|
||||
import React, { MouseEvent, useCallback } from 'react';
|
||||
import ReactFlow, {
|
||||
useReactFlow,
|
||||
NodeTypes,
|
||||
addEdge,
|
||||
ReactFlowProvider,
|
||||
Node,
|
||||
Connection,
|
||||
Edge,
|
||||
ConnectionLineType,
|
||||
ConnectionMode,
|
||||
updateEdge,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
} from 'reactflow';
|
||||
|
||||
import CustomNode from './CustomNode';
|
||||
|
||||
const initialNodes: Node[] = [
|
||||
{
|
||||
id: '00',
|
||||
type: 'custom',
|
||||
position: { x: 300, y: 250 },
|
||||
data: null,
|
||||
},
|
||||
{
|
||||
id: '01',
|
||||
type: 'custom',
|
||||
position: { x: 100, y: 50 },
|
||||
data: null,
|
||||
},
|
||||
{
|
||||
id: '02',
|
||||
type: 'custom',
|
||||
position: { x: 500, y: 50 },
|
||||
data: null,
|
||||
},
|
||||
{
|
||||
id: '03',
|
||||
type: 'custom',
|
||||
position: { x: 500, y: 500 },
|
||||
data: null,
|
||||
},
|
||||
{
|
||||
id: '04',
|
||||
type: 'custom',
|
||||
position: { x: 100, y: 500 },
|
||||
data: null,
|
||||
},
|
||||
{
|
||||
id: '10',
|
||||
type: 'custom',
|
||||
position: { x: 300, y: 5 },
|
||||
data: null,
|
||||
},
|
||||
{
|
||||
id: '20',
|
||||
type: 'custom',
|
||||
position: { x: 600, y: 250 },
|
||||
data: null,
|
||||
},
|
||||
{
|
||||
id: '30',
|
||||
type: 'custom',
|
||||
position: { x: 300, y: 600 },
|
||||
data: null,
|
||||
},
|
||||
{
|
||||
id: '40',
|
||||
type: 'custom',
|
||||
position: { x: 5, y: 250 },
|
||||
data: null,
|
||||
},
|
||||
];
|
||||
|
||||
const initialEdges: Edge[] = [
|
||||
{
|
||||
id: 'e0-1a',
|
||||
source: '00',
|
||||
target: '01',
|
||||
sourceHandle: 'left',
|
||||
targetHandle: 'bottom',
|
||||
type: 'smoothstep',
|
||||
},
|
||||
{
|
||||
id: 'e0-1b',
|
||||
source: '00',
|
||||
target: '01',
|
||||
sourceHandle: 'top',
|
||||
targetHandle: 'right',
|
||||
type: 'smoothstep',
|
||||
},
|
||||
{
|
||||
id: 'e0-2a',
|
||||
source: '00',
|
||||
target: '02',
|
||||
sourceHandle: 'top',
|
||||
targetHandle: 'left',
|
||||
type: 'smoothstep',
|
||||
},
|
||||
{
|
||||
id: 'e0-2b',
|
||||
source: '00',
|
||||
target: '02',
|
||||
sourceHandle: 'right',
|
||||
targetHandle: 'bottom',
|
||||
type: 'smoothstep',
|
||||
},
|
||||
{
|
||||
id: 'e0-3a',
|
||||
source: '00',
|
||||
target: '03',
|
||||
sourceHandle: 'right',
|
||||
targetHandle: 'top',
|
||||
type: 'smoothstep',
|
||||
},
|
||||
{
|
||||
id: 'e0-3b',
|
||||
source: '00',
|
||||
target: '03',
|
||||
sourceHandle: 'bottom',
|
||||
targetHandle: 'left',
|
||||
type: 'smoothstep',
|
||||
},
|
||||
{
|
||||
id: 'e0-4a',
|
||||
source: '00',
|
||||
target: '04',
|
||||
sourceHandle: 'bottom',
|
||||
targetHandle: 'right',
|
||||
type: 'smoothstep',
|
||||
},
|
||||
{
|
||||
id: 'e0-4b',
|
||||
source: '00',
|
||||
target: '04',
|
||||
sourceHandle: 'left',
|
||||
targetHandle: 'top',
|
||||
type: 'smoothstep',
|
||||
},
|
||||
{
|
||||
id: 'e0-10',
|
||||
source: '00',
|
||||
target: '10',
|
||||
sourceHandle: 'top',
|
||||
targetHandle: 'bottom',
|
||||
type: 'smoothstep',
|
||||
},
|
||||
{
|
||||
id: 'e0-20',
|
||||
source: '00',
|
||||
target: '20',
|
||||
sourceHandle: 'right',
|
||||
targetHandle: 'left',
|
||||
type: 'smoothstep',
|
||||
},
|
||||
{
|
||||
id: 'e0-30',
|
||||
source: '00',
|
||||
target: '30',
|
||||
sourceHandle: 'bottom',
|
||||
targetHandle: 'top',
|
||||
type: 'smoothstep',
|
||||
},
|
||||
{
|
||||
id: 'e0-40',
|
||||
source: '00',
|
||||
target: '40',
|
||||
sourceHandle: 'left',
|
||||
targetHandle: 'right',
|
||||
type: 'smoothstep',
|
||||
},
|
||||
];
|
||||
|
||||
const nodeTypes: NodeTypes = {
|
||||
custom: CustomNode,
|
||||
};
|
||||
|
||||
let id = 4;
|
||||
const getId = () => `${id++}`;
|
||||
|
||||
const UpdateNodeInternalsFlow = () => {
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||
|
||||
const onConnect = (params: Edge | Connection) => setEdges((els) => addEdge(params, els));
|
||||
const { project } = useReactFlow();
|
||||
const onEdgeUpdate = (oldEdge: Edge, newConnection: Connection) =>
|
||||
setEdges((els) => updateEdge(oldEdge, newConnection, els));
|
||||
|
||||
const onPaneClick = useCallback(
|
||||
(evt: MouseEvent) =>
|
||||
setNodes((nds) =>
|
||||
nds.concat({
|
||||
id: getId(),
|
||||
position: project({ x: evt.clientX, y: evt.clientY - 40 }),
|
||||
type: 'custom',
|
||||
data: null,
|
||||
})
|
||||
),
|
||||
[project, setNodes]
|
||||
);
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
nodeTypes={nodeTypes}
|
||||
onConnect={onConnect}
|
||||
onPaneClick={onPaneClick}
|
||||
connectionLineType={ConnectionLineType.Bezier}
|
||||
connectionMode={ConnectionMode.Loose}
|
||||
onEdgeUpdate={onEdgeUpdate}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const WrappedFlow = () => (
|
||||
<ReactFlowProvider>
|
||||
<UpdateNodeInternalsFlow />
|
||||
</ReactFlowProvider>
|
||||
);
|
||||
|
||||
export default WrappedFlow;
|
||||
@@ -0,0 +1,102 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import ReactFlow, {
|
||||
Controls,
|
||||
updateEdge,
|
||||
addEdge,
|
||||
applyNodeChanges,
|
||||
applyEdgeChanges,
|
||||
ReactFlowInstance,
|
||||
Connection,
|
||||
Edge,
|
||||
Node,
|
||||
NodeChange,
|
||||
EdgeChange,
|
||||
HandleType,
|
||||
} from 'reactflow';
|
||||
|
||||
const initialNodes: Node[] = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'input',
|
||||
data: {
|
||||
label: (
|
||||
<>
|
||||
Node <strong>A</strong>
|
||||
</>
|
||||
),
|
||||
},
|
||||
position: { x: 250, y: 0 },
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
data: {
|
||||
label: (
|
||||
<>
|
||||
Node <strong>B</strong>
|
||||
</>
|
||||
),
|
||||
},
|
||||
position: { x: 100, y: 100 },
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
data: {
|
||||
label: (
|
||||
<>
|
||||
Node <strong>C</strong>
|
||||
</>
|
||||
),
|
||||
},
|
||||
position: { x: 400, y: 100 },
|
||||
style: {
|
||||
background: '#D6D5E6',
|
||||
color: '#333',
|
||||
border: '1px solid #222138',
|
||||
width: 180,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const initialEdges = [{ id: 'e1-2', source: '1', target: '2', label: 'This is a draggable edge' }];
|
||||
|
||||
const onInit = (reactFlowInstance: ReactFlowInstance) => reactFlowInstance.fitView();
|
||||
const onEdgeUpdateStart = (_: React.MouseEvent, edge: Edge, handleType: HandleType) =>
|
||||
console.log(`start update ${handleType} handle`, edge);
|
||||
const onEdgeUpdateEnd = (_: MouseEvent, edge: Edge, handleType: HandleType) =>
|
||||
console.log(`end update ${handleType} handle`, edge);
|
||||
|
||||
const UpdatableEdge = () => {
|
||||
const [nodes, setNodes] = useState<Node[]>(initialNodes);
|
||||
const [edges, setEdges] = useState<Edge[]>(initialEdges);
|
||||
const onEdgeUpdate = (oldEdge: Edge, newConnection: Connection) =>
|
||||
setEdges((els) => updateEdge(oldEdge, newConnection, els));
|
||||
const onConnect = (connection: Connection) => setEdges((els) => addEdge(connection, els));
|
||||
|
||||
const onNodesChange = useCallback((changes: NodeChange[]) => {
|
||||
console.log(changes);
|
||||
setNodes((ns) => applyNodeChanges(changes, ns));
|
||||
}, []);
|
||||
|
||||
const onEdgesChange = useCallback((changes: EdgeChange[]) => {
|
||||
setEdges((es) => applyEdgeChanges(changes, es));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onInit={onInit}
|
||||
snapToGrid={true}
|
||||
onEdgeUpdate={onEdgeUpdate}
|
||||
onConnect={onConnect}
|
||||
// onEdgeUpdateStart={onEdgeUpdateStart}
|
||||
// onEdgeUpdateEnd={onEdgeUpdateEnd}
|
||||
>
|
||||
<Controls />
|
||||
</ReactFlow>
|
||||
);
|
||||
};
|
||||
|
||||
export default UpdatableEdge;
|
||||
@@ -0,0 +1,88 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import ReactFlow, { Node, Edge, useNodesState, useEdgesState } from 'reactflow';
|
||||
|
||||
import styles from './updatenode.module.css';
|
||||
|
||||
const initialNodes: Node[] = [
|
||||
{ id: '1', data: { label: '-' }, position: { x: 100, y: 100 } },
|
||||
{ id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 200 } },
|
||||
];
|
||||
|
||||
const initialEdges: Edge[] = [{ id: 'e1-2', source: '1', target: '2' }];
|
||||
|
||||
const UpdateNode = () => {
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||
|
||||
const [nodeName, setNodeName] = useState<string>('Node 1');
|
||||
const [nodeBg, setNodeBg] = useState<string>('#eee');
|
||||
const [nodeHidden, setNodeHidden] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
setNodes((nds) =>
|
||||
nds.map((n) => {
|
||||
if (n.id === '1') {
|
||||
// it's important that you create a new object here in order to notify react flow about the change
|
||||
n.data = {
|
||||
...n.data,
|
||||
label: nodeName,
|
||||
};
|
||||
}
|
||||
|
||||
return n;
|
||||
})
|
||||
);
|
||||
}, [nodeName]);
|
||||
|
||||
useEffect(() => {
|
||||
setNodes((nds) =>
|
||||
nds.map((n) => {
|
||||
if (n.id === '1') {
|
||||
// it's important that you create a new object here in order to notify react flow about the change
|
||||
n.style = { ...n.style, backgroundColor: nodeBg };
|
||||
}
|
||||
|
||||
return n;
|
||||
})
|
||||
);
|
||||
}, [nodeBg]);
|
||||
|
||||
useEffect(() => {
|
||||
setNodes((nds) =>
|
||||
nds.map((n) => {
|
||||
if (n.id === '1' || n.id === 'e1-2') {
|
||||
// when you update a simple type you can just update the value
|
||||
n.hidden = nodeHidden;
|
||||
}
|
||||
|
||||
return n;
|
||||
})
|
||||
);
|
||||
}, [nodeHidden]);
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
minZoom={0.2}
|
||||
maxZoom={4}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
>
|
||||
<div className={styles.controls}>
|
||||
<label>label:</label>
|
||||
<input value={nodeName} onChange={(evt) => setNodeName(evt.target.value)} />
|
||||
|
||||
<label className={styles.bgLabel}>background:</label>
|
||||
<input value={nodeBg} onChange={(evt) => setNodeBg(evt.target.value)} />
|
||||
|
||||
<div className="updatenode__checkboxwrapper">
|
||||
<label>hidden:</label>
|
||||
<input type="checkbox" checked={nodeHidden} onChange={(evt) => setNodeHidden(evt.target.checked)} />
|
||||
</div>
|
||||
</div>
|
||||
</ReactFlow>
|
||||
);
|
||||
};
|
||||
|
||||
export default UpdateNode;
|
||||
@@ -0,0 +1,21 @@
|
||||
.controls {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
z-index: 4;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.controlsLabel {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.bgLabel {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.checkboxWrapper {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import { useKeyPress } from 'reactflow';
|
||||
|
||||
const UseKeyPressComponent = () => {
|
||||
const metaPressed = useKeyPress(['Meta']);
|
||||
|
||||
console.log({ metaPressed });
|
||||
|
||||
return <div />;
|
||||
};
|
||||
|
||||
export default UseKeyPressComponent;
|
||||
@@ -0,0 +1,153 @@
|
||||
import React, { useCallback, MouseEvent, useEffect } from 'react';
|
||||
import ReactFlow, {
|
||||
Background,
|
||||
MiniMap,
|
||||
Node,
|
||||
addEdge,
|
||||
useReactFlow,
|
||||
ReactFlowProvider,
|
||||
Connection,
|
||||
Edge,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
} from 'reactflow';
|
||||
|
||||
const initialNodes: Node[] = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'input',
|
||||
data: { label: 'Node 1' },
|
||||
position: { x: 250, y: 5 },
|
||||
className: 'light',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
data: { label: 'Node 2' },
|
||||
position: { x: 100, y: 100 },
|
||||
className: 'light',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
data: { label: 'Node 3' },
|
||||
position: { x: 400, y: 100 },
|
||||
className: 'light',
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
data: { label: 'Node 4' },
|
||||
position: { x: 400, y: 200 },
|
||||
className: 'light',
|
||||
},
|
||||
];
|
||||
|
||||
const initialEdges: Edge[] = [
|
||||
{ id: 'e1-2', source: '1', target: '2', animated: true },
|
||||
{ id: 'e1-3', source: '1', target: '3' },
|
||||
];
|
||||
|
||||
let id = 5;
|
||||
|
||||
const getId = () => `${id++}`;
|
||||
|
||||
const UseZoomPanHelperFlow = () => {
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||
const onConnect = (params: Connection | Edge) => setEdges((eds) => addEdge(params, eds));
|
||||
const {
|
||||
project,
|
||||
setCenter,
|
||||
zoomIn,
|
||||
zoomOut,
|
||||
fitView,
|
||||
addNodes,
|
||||
setNodes: setNodesHook,
|
||||
addEdges,
|
||||
getNodes,
|
||||
getEdges,
|
||||
} = useReactFlow();
|
||||
|
||||
const onPaneClick = useCallback(
|
||||
(evt: MouseEvent) => {
|
||||
const projectedPosition = project({
|
||||
x: evt.clientX,
|
||||
y: evt.clientY - 40,
|
||||
});
|
||||
|
||||
setNodes((nds) =>
|
||||
nds.concat({
|
||||
id: getId(),
|
||||
position: projectedPosition,
|
||||
data: {
|
||||
label: `${projectedPosition.x}-${projectedPosition.y}`,
|
||||
},
|
||||
})
|
||||
);
|
||||
},
|
||||
[project, setNodes]
|
||||
);
|
||||
|
||||
const onNodeClick = useCallback(
|
||||
(_: MouseEvent, node: Node) => {
|
||||
const { x, y } = node.position;
|
||||
setCenter(x, y, { zoom: 1, duration: 1200 });
|
||||
},
|
||||
[setCenter]
|
||||
);
|
||||
|
||||
const onAddNode = useCallback(() => {
|
||||
const newNode = {
|
||||
id: getId(),
|
||||
position: { x: Math.random() * 500, y: Math.random() * 500 },
|
||||
data: {
|
||||
label: 'New Node',
|
||||
},
|
||||
};
|
||||
|
||||
addNodes(newNode);
|
||||
}, [addNodes]);
|
||||
|
||||
const logNodes = useCallback(() => {
|
||||
console.log('nodes', getNodes());
|
||||
console.log('edges', getEdges());
|
||||
}, [getNodes, getEdges]);
|
||||
|
||||
useEffect(() => {
|
||||
addEdges({ id: 'e3-4', source: '3', target: '4' });
|
||||
}, [addEdges]);
|
||||
|
||||
const onResetNodes = useCallback(() => setNodesHook(initialNodes), [setNodesHook]);
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onNodeClick={onNodeClick}
|
||||
onConnect={onConnect}
|
||||
onPaneClick={onPaneClick}
|
||||
fitView
|
||||
fitViewOptions={{ duration: 1200, padding: 0.2 }}
|
||||
maxZoom={Infinity}
|
||||
>
|
||||
<div style={{ position: 'absolute', left: 0, top: 0, zIndex: 100 }}>
|
||||
<button onClick={() => zoomIn({ duration: 1200 })}>zoomIn</button>
|
||||
<button onClick={() => zoomOut({ duration: 0 })}>zoomOut</button>
|
||||
<button onClick={() => fitView({ duration: 1200, padding: 0.3 })}>fitView</button>
|
||||
<button onClick={onAddNode}>add node</button>
|
||||
<button onClick={onResetNodes}>reset nodes</button>
|
||||
<button onClick={logNodes}>useNodes</button>
|
||||
</div>
|
||||
<Background />
|
||||
<MiniMap />
|
||||
</ReactFlow>
|
||||
);
|
||||
};
|
||||
|
||||
const WrappedFlow = () => (
|
||||
<ReactFlowProvider>
|
||||
<UseZoomPanHelperFlow />
|
||||
</ReactFlowProvider>
|
||||
);
|
||||
|
||||
export default WrappedFlow;
|
||||
@@ -0,0 +1,36 @@
|
||||
import React, { useState, memo, FC, useMemo, CSSProperties } from 'react';
|
||||
import { Handle, Position, NodeProps, useUpdateNodeInternals } from 'reactflow';
|
||||
|
||||
const nodeStyles: CSSProperties = { padding: 10, border: '1px solid #ddd' };
|
||||
|
||||
const CustomNode: FC<NodeProps> = ({ id }) => {
|
||||
const [handleCount, setHandleCount] = useState(1);
|
||||
const updateNodeInternals = useUpdateNodeInternals();
|
||||
|
||||
const handles = useMemo(
|
||||
() =>
|
||||
Array.from({ length: handleCount }, (x, i) => {
|
||||
const handleId = `handle-${i}`;
|
||||
return <Handle key={handleId} type="source" position={Position.Right} id={handleId} style={{ top: 10 * i }} />;
|
||||
}),
|
||||
[handleCount]
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={nodeStyles}>
|
||||
<Handle type="target" position={Position.Left} />
|
||||
<div>output handle count: {handleCount}</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
setHandleCount((c) => c + 1);
|
||||
updateNodeInternals(id);
|
||||
}}
|
||||
>
|
||||
add handle
|
||||
</button>
|
||||
{handles}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(CustomNode);
|
||||
@@ -0,0 +1,73 @@
|
||||
import { useCallback, MouseEvent } from 'react';
|
||||
import ReactFlow, {
|
||||
NodeTypes,
|
||||
addEdge,
|
||||
useReactFlow,
|
||||
ReactFlowProvider,
|
||||
Node,
|
||||
Connection,
|
||||
Edge,
|
||||
Position,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
} from 'reactflow';
|
||||
|
||||
import CustomNode from './CustomNode';
|
||||
|
||||
const initialNodes: Node[] = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'custom',
|
||||
data: { label: 'Node 1' },
|
||||
position: { x: 250, y: 5 },
|
||||
},
|
||||
];
|
||||
|
||||
const nodeTypes: NodeTypes = {
|
||||
custom: CustomNode,
|
||||
};
|
||||
|
||||
let id = 5;
|
||||
const getId = (): string => `${id++}`;
|
||||
|
||||
const UpdateNodeInternalsFlow = () => {
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
||||
const onConnect = useCallback((params: Edge | Connection) => setEdges((els) => addEdge(params, els)), [setEdges]);
|
||||
|
||||
const { project } = useReactFlow();
|
||||
|
||||
const onPaneClick = useCallback(
|
||||
(evt: MouseEvent) =>
|
||||
setNodes((nds) =>
|
||||
nds.concat({
|
||||
id: getId(),
|
||||
position: project({ x: evt.clientX, y: evt.clientY - 40 }),
|
||||
data: { label: 'new node' },
|
||||
targetPosition: Position.Left,
|
||||
sourcePosition: Position.Right,
|
||||
})
|
||||
),
|
||||
[project, setNodes]
|
||||
);
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
nodeTypes={nodeTypes}
|
||||
onConnect={onConnect}
|
||||
onPaneClick={onPaneClick}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const WrappedFlow = () => (
|
||||
<ReactFlowProvider>
|
||||
<UpdateNodeInternalsFlow />
|
||||
</ReactFlowProvider>
|
||||
);
|
||||
|
||||
export default WrappedFlow;
|
||||
@@ -0,0 +1,93 @@
|
||||
import React, { FC, useCallback, useState } from 'react';
|
||||
import ReactFlow, {
|
||||
addEdge,
|
||||
Handle,
|
||||
Connection,
|
||||
Position,
|
||||
Node,
|
||||
Edge,
|
||||
NodeProps,
|
||||
NodeTypes,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
OnConnectStartParams,
|
||||
} from 'reactflow';
|
||||
|
||||
import styles from './validation.module.css';
|
||||
|
||||
const initialNodes: Node[] = [
|
||||
{ id: '0', type: 'custominput', position: { x: 0, y: 150 }, data: null },
|
||||
{ id: 'A', type: 'customnode', position: { x: 250, y: 0 }, data: null },
|
||||
{ id: 'B', type: 'customnode', position: { x: 250, y: 150 }, data: null },
|
||||
{ id: 'C', type: 'customnode', position: { x: 250, y: 300 }, data: null },
|
||||
];
|
||||
|
||||
const isValidConnection = (connection: Connection) => connection.target === 'B';
|
||||
|
||||
const CustomInput: FC<NodeProps> = () => (
|
||||
<>
|
||||
<div>Only connectable with B</div>
|
||||
<Handle type="source" position={Position.Right} isValidConnection={isValidConnection} />
|
||||
</>
|
||||
);
|
||||
|
||||
const CustomNode: FC<NodeProps> = ({ id }) => (
|
||||
<>
|
||||
<Handle type="target" position={Position.Left} isValidConnection={isValidConnection} />
|
||||
<div>{id}</div>
|
||||
<Handle type="source" position={Position.Right} isValidConnection={isValidConnection} />
|
||||
</>
|
||||
);
|
||||
|
||||
const nodeTypes: NodeTypes = {
|
||||
custominput: CustomInput,
|
||||
customnode: CustomNode,
|
||||
};
|
||||
|
||||
const ValidationFlow = () => {
|
||||
const [value, setValue] = useState(0);
|
||||
const [nodes, , onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
||||
|
||||
const onConnectStart = useCallback(
|
||||
(event: React.MouseEvent, params: OnConnectStartParams) => {
|
||||
console.log('on connect start', params, event, value);
|
||||
setValue(1);
|
||||
},
|
||||
[value]
|
||||
);
|
||||
|
||||
const onConnect = useCallback(
|
||||
(params: Connection | Edge) => {
|
||||
console.log('on connect', params);
|
||||
setEdges((eds) => addEdge(params, eds));
|
||||
},
|
||||
[setEdges]
|
||||
);
|
||||
|
||||
const onConnectEnd = useCallback(
|
||||
(event: MouseEvent) => {
|
||||
console.log('on connect end', event, value);
|
||||
setValue(0);
|
||||
},
|
||||
[value]
|
||||
);
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
selectNodesOnDrag={false}
|
||||
className={styles.validationflow}
|
||||
nodeTypes={nodeTypes}
|
||||
onConnectStart={onConnectStart}
|
||||
onConnectEnd={onConnectEnd}
|
||||
fitView
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ValidationFlow;
|
||||
@@ -0,0 +1,33 @@
|
||||
.validationflow {
|
||||
}
|
||||
|
||||
.validationflow :global .react-flow__node {
|
||||
width: 150px;
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
color: #555;
|
||||
border: 1px solid #ddd;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.validationflow :global .react-flow__node-customnode {
|
||||
background: #e6e6e9;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.validationflow :global .react-flow__node-custominput .react-flow__handle {
|
||||
background: #e6e6e9;
|
||||
}
|
||||
|
||||
.validationflow :global .react-flow__node-custominput {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.validationflow :global .react-flow__handle-connecting {
|
||||
background: #ff6060;
|
||||
}
|
||||
|
||||
.validationflow :global .react-flow__handle-valid {
|
||||
background: #55dd99;
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
color: #111;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
header {
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
display: flex;
|
||||
font-weight: 700;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
header a,
|
||||
header a:focus,
|
||||
header a:active,
|
||||
header a:visited {
|
||||
color: #111;
|
||||
}
|
||||
|
||||
header a:hover {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
header select {
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
.overview-example__add {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.react-flow__node a {
|
||||
font-weight: 700;
|
||||
color: #111;
|
||||
}
|
||||
|
||||
.react-flow__node.dark-node {
|
||||
background: #0041d0;
|
||||
color: #f8f8f8;
|
||||
}
|
||||
|
||||
.react-flow__node.dark {
|
||||
background: #557;
|
||||
color: #f8f8f8;
|
||||
}
|
||||
|
||||
.react-flow__node-selectorNode {
|
||||
font-size: 12px;
|
||||
background: #f0f2f3;
|
||||
border: 1px solid 555;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.react-flow__node-selectorNode .react-flow__handle {
|
||||
border-color: #f0f2f3;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.overview-example__add {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import App from './App';
|
||||
|
||||
import './index.css';
|
||||
import 'reactflow/dist/style.css';
|
||||
|
||||
createRoot(document.getElementById('root') as HTMLElement).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>
|
||||
);
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
fs: {
|
||||
// Allow serving files from one level up to the project root
|
||||
allow: ['../..'],
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user