├── .DS_Store
├── talend-scripts.json
├── src
├── reducers
│ ├── index.ts
│ ├── nodeType.reducer.ts
│ ├── link.reducer.test.ts
│ ├── port.reducer.test.ts
│ ├── flow.reducer.test.ts
│ ├── node.reducer.test.ts
│ ├── flow.reducer.ts
│ ├── port.reducer.ts
│ └── node.reducer.ts
├── components
│ ├── link
│ │ ├── __snapshots__
│ │ │ ├── LinksRenderer.test.tsx.snap
│ │ │ └── LinkHandle.test.tsx.snap
│ │ ├── LinkHandle.test.tsx
│ │ ├── LinksRenderer.component.tsx
│ │ ├── LinkHandle.component.tsx
│ │ ├── LinksRenderer.test.tsx
│ │ └── AbstractLink.component.tsx
│ ├── port
│ │ ├── __snapshots__
│ │ │ ├── PortsRenderer.test.tsx.snap
│ │ │ └── AbstractPort.test.tsx.snap
│ │ ├── PortsRenderer.component.tsx
│ │ ├── AbstractPort.test.tsx
│ │ ├── PortsRenderer.test.tsx
│ │ └── AbstractPort.component.tsx
│ ├── node
│ │ ├── __snapshots__
│ │ │ ├── AbstractNode.snapshot.test.tsx.snap
│ │ │ └── NodesRenderer.test.tsx.snap
│ │ ├── AbstractNode.snapshot.test.tsx
│ │ ├── NodesRenderer.component.tsx
│ │ ├── NodesRenderer.test.tsx
│ │ ├── AbstractNode.test.tsx
│ │ └── AbstractNode.component.tsx
│ ├── grid
│ │ ├── Grid.test.tsx
│ │ ├── __snapshots__
│ │ │ └── Grid.test.tsx.snap
│ │ └── Grid.component.tsx
│ ├── configuration
│ │ ├── LinkType.component.ts
│ │ ├── NodeType.component.ts
│ │ ├── PortType.component.ts
│ │ └── NodeType.test.tsx
│ ├── __snapshots__
│ │ ├── ZoomHandler.test.tsx.snap
│ │ └── FlowDesigner.container.test.tsx.snap
│ ├── ZoomHandler.test.tsx
│ ├── FlowDesigner.container.test.tsx
│ ├── ZoomHandler.component.tsx
│ └── FlowDesigner.container.tsx
├── actions
│ ├── __snapshots__
│ │ ├── flow.actions.test.ts.snap
│ │ ├── port.actions.test.ts.snap
│ │ ├── link.actions.test.ts.snap
│ │ └── node.actions.test.ts.snap
│ ├── nodeType.actions.ts
│ ├── nodeType.actions.test.ts
│ ├── flow.actions.test.ts
│ ├── flow.actions.ts
│ ├── port.actions.test.ts
│ ├── port.actions.ts
│ ├── link.actions.ts
│ ├── link.actions.test.ts
│ ├── node.actions.test.ts
│ └── node.actions.ts
├── api
│ ├── index.ts
│ ├── throwInDev.ts
│ ├── readme.md
│ ├── data
│ │ ├── data.ts
│ │ └── data.test.ts
│ ├── size
│ │ ├── size.ts
│ │ └── size.test.ts
│ ├── position
│ │ ├── position.ts
│ │ └── position.test.ts
│ └── link
│ │ └── link.ts
├── constants
│ ├── flowdesigner.proptypes.ts
│ ├── flowdesigner.constants.ts
│ └── flowdesigner.model.ts
├── selectors
│ ├── __snapshots__
│ │ └── nodeSelectors.test.ts.snap
│ ├── nodeSelectors.ts
│ ├── linkSelectors.ts
│ ├── portSelectors.test.ts
│ ├── portSelectors.ts
│ └── nodeSelectors.test.ts
├── index.ts
└── customTypings
│ └── index.d.ts
├── .gitignore
├── test
├── styleMock.js
├── fileMock.js
└── test-setup.js
├── .npmignore
├── CHANGELOG.md
├── .changeset
├── config.json
└── README.md
├── .prettierrc
├── .editorconfig
├── tsconfig.json
├── .eslintrc.js
├── licence.js
├── .github
├── workflows
│ ├── ci.yml
│ ├── changeset.yml
│ └── update-yarn-talend.yml
└── PULL_REQUEST_TEMPLATE.md
├── package.json
├── README.md
└── jenkins
└── veracode-scan.groovy
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Talend/react-flow-designer/HEAD/.DS_Store
--------------------------------------------------------------------------------
/talend-scripts.json:
--------------------------------------------------------------------------------
1 | {
2 | "preset": "@talend/scripts-preset-react-lib"
3 | }
4 |
--------------------------------------------------------------------------------
/src/reducers/index.ts:
--------------------------------------------------------------------------------
1 | import flowDesignerReducer from './flow.reducer';
2 |
3 | export default flowDesignerReducer;
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | npm-debug.log
2 | yarn-error.log
3 | jsconfig.json
4 | node_modules/
5 | coverage/
6 | lib/
7 | docs/
8 | dist/
9 |
--------------------------------------------------------------------------------
/test/styleMock.js:
--------------------------------------------------------------------------------
1 | // test/styleMock.js
2 | // // Return an object to emulate css modules (if you are using them)
3 | module.exports = {};
4 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | npm-debug.log
2 | node_modules/
3 | coverage/
4 | src/
5 | test/
6 | .babelrc
7 | .eslintrc
8 | .gitignore
9 | .travis.yml
10 | webpack.config.babel.js
11 |
--------------------------------------------------------------------------------
/test/fileMock.js:
--------------------------------------------------------------------------------
1 | // test/fileMock.js
2 | // Return an empty string or other mock path to emulate the url that
3 | // Webpack provides via the file-loader
4 | module.exports = '';
5 |
--------------------------------------------------------------------------------
/src/components/link/__snapshots__/LinksRenderer.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` renders correctly 1`] = `
4 |
5 |
6 | MockLink
7 |
8 |
9 | `;
10 |
--------------------------------------------------------------------------------
/src/components/port/__snapshots__/PortsRenderer.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` renders correctly 1`] = `
4 |
5 |
6 | MockPort
7 |
8 |
9 | `;
10 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # react-flow-designer
2 |
3 | ## 4.2.2
4 |
5 | ### Patch Changes
6 |
7 | - 798b4b5: re align redux dependencies and prepare auto upgrade PR
8 |
9 | ## 4.2.1
10 |
11 | ### Patch Changes
12 |
13 | - 99bd1c5: upgrade dependencies
14 |
--------------------------------------------------------------------------------
/src/components/link/__snapshots__/LinkHandle.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` renders correctly renders correctly 1`] = `
4 |
7 |
8 |
9 | `;
10 |
--------------------------------------------------------------------------------
/src/components/port/__snapshots__/AbstractPort.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` renders correctly renders correctly 1`] = `
4 |
5 |
8 |
9 | `;
10 |
--------------------------------------------------------------------------------
/src/components/node/__snapshots__/AbstractNode.snapshot.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` renders correctly 1`] = `
4 |
5 |
9 |
10 |
11 |
12 | `;
13 |
--------------------------------------------------------------------------------
/.changeset/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://unpkg.com/@changesets/config@1.6.1/schema.json",
3 | "changelog": "@changesets/cli/changelog",
4 | "commit": false,
5 | "linked": [],
6 | "access": "restricted",
7 | "baseBranch": "master",
8 | "updateInternalDependencies": "patch",
9 | "ignore": []
10 | }
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 100,
3 | "tabWidth": 4,
4 | "singleQuote": true,
5 | "trailingComma": "all",
6 | "semi": true,
7 | "useTabs": true,
8 | "arrowParens": "avoid",
9 | "overrides": [
10 | {
11 | "files": "**/*.json",
12 | "options": {
13 | "tabWidth": 2,
14 | "useTabs": false
15 | }
16 | }
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/node/__snapshots__/NodesRenderer.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` renders correctly 1`] = `
4 |
5 |
6 | MockNodes
7 |
8 |
9 | `;
10 |
11 | exports[` renders correctly if nested 1`] = `
12 |
13 |
14 | MockNodes
15 |
16 |
17 | `;
18 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 | [*]
4 | end_of_line = lf
5 | charset = utf-8
6 | trim_trailing_whitespace = true
7 | insert_final_newline = true
8 | [*.js, *.jsx, *.css, *.scss]
9 | indent_style = tab
10 | # special rule for json expecially package.json wich npm reset to space for any modification
11 | [*.json]
12 | indent_style = space
13 | indent_size = 2
14 |
--------------------------------------------------------------------------------
/src/components/grid/Grid.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import renderer from 'react-test-renderer';
3 |
4 | import Grid from './Grid.component';
5 |
6 | describe('Grid.component', () => {
7 | it('should render a grid by default', () => {
8 | const tree = renderer.create();
9 | expect(tree).toMatchSnapshot();
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./node_modules/@talend/scripts-config-typescript/tsconfig.json",
3 | "include": ["src/**/*"],
4 | "compilerOptions": {
5 | "declaration": true,
6 | "allowJs": false,
7 | "incremental": true,
8 | "jsx": "react",
9 | "module": "CommonJs",
10 | "typeRoots": ["src/customTypings/index.d", "node_modules/@types"]
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/actions/__snapshots__/flow.actions.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`setZoom if transform object is not well set return null 1`] = `null`;
4 |
5 | exports[`setZoom should generate an action with proper shape 1`] = `
6 | Object {
7 | "transform": Object {
8 | "k": 0,
9 | "x": 0,
10 | "y": 0,
11 | },
12 | "type": "FLOWDESIGNER.FLOW_SET_ZOOM",
13 | }
14 | `;
15 |
--------------------------------------------------------------------------------
/src/actions/nodeType.actions.ts:
--------------------------------------------------------------------------------
1 | import { Map } from 'immutable';
2 | import { FLOWDESIGNER_NODETYPE_SET } from '../constants/flowdesigner.constants';
3 |
4 | /**
5 | * Ask to set a map for nodeTypes
6 | * @param {Map} nodeTypes
7 | */
8 | export const setNodeTypes = (nodeTypes: Map) => ({
9 | type: FLOWDESIGNER_NODETYPE_SET,
10 | nodeTypes,
11 | });
12 |
13 | export default setNodeTypes;
14 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | const talendEslintRcPath = require.resolve('@talend/scripts-config-eslint/.eslintrc');
2 |
3 | module.exports = {
4 | extends: talendEslintRcPath,
5 | parser: '@typescript-eslint/parser',
6 | rules: {
7 | 'react/prop-types': 'off',
8 | 'import/no-unresolved': 'off',
9 | 'no-shadow': 'off',
10 | '@typescript-eslint/no-use-before-define': 'off',
11 | '@typescript-eslint/no-unused-vars': 'off',
12 | },
13 | };
14 |
--------------------------------------------------------------------------------
/src/api/index.ts:
--------------------------------------------------------------------------------
1 | import * as port from './port/port';
2 | import * as link from './link/link';
3 | import * as node from './node/node';
4 | import * as position from './position/position';
5 | import * as size from './size/size';
6 | import * as data from './data/data';
7 |
8 | export const Port = port;
9 | export const Link = link;
10 | export const Node = node;
11 | export const Position = position;
12 | export const Size = size;
13 | export const Data = data;
14 |
--------------------------------------------------------------------------------
/src/reducers/nodeType.reducer.ts:
--------------------------------------------------------------------------------
1 | import { Map } from 'immutable';
2 |
3 | import { FLOWDESIGNER_NODETYPE_SET } from '../constants/flowdesigner.constants';
4 |
5 | const defaultState = Map();
6 | const nodeTypeReducer = (state = defaultState, action: any) => {
7 | switch (action.type) {
8 | case FLOWDESIGNER_NODETYPE_SET:
9 | return state.mergeIn(['nodeTypes'], action.nodeTypes);
10 | default:
11 | return state;
12 | }
13 | };
14 |
15 | export default nodeTypeReducer;
16 |
--------------------------------------------------------------------------------
/src/actions/nodeType.actions.test.ts:
--------------------------------------------------------------------------------
1 | import { Map } from 'immutable';
2 | import * as nodeTypeActions from './nodeType.actions';
3 |
4 | describe('Check that nodeType action creators generate the right action objects', () => {
5 | it('setNodeTypes', () => {
6 | const nodeTypes = Map().set('anything', { something: true });
7 | expect(nodeTypeActions.setNodeTypes(nodeTypes)).toEqual({
8 | type: 'FLOWDESIGNER_NODETYPE_SET',
9 | nodeTypes,
10 | });
11 | });
12 | });
13 |
--------------------------------------------------------------------------------
/src/components/configuration/LinkType.component.ts:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import invariant from 'invariant';
3 |
4 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
5 | function LinkType({ type, component }: { type: string; component: React.ReactNode }) {
6 | invariant(
7 | false,
8 | ' elements are for DataFlow configuration only and should not be rendered',
9 | );
10 | return null;
11 | }
12 |
13 | LinkType.displayName = 'LinkType';
14 |
15 | export default LinkType;
16 |
--------------------------------------------------------------------------------
/src/components/configuration/NodeType.component.ts:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import invariant from 'invariant';
3 |
4 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
5 | function NodeType({ type, component }: { type: string; component: React.ReactNode }) {
6 | invariant(
7 | false,
8 | ' elements are for DataFlow configuration only and should not be rendered',
9 | );
10 | return null;
11 | }
12 |
13 | NodeType.displayName = 'NodeType';
14 |
15 | export default NodeType;
16 |
--------------------------------------------------------------------------------
/src/components/configuration/PortType.component.ts:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import invariant from 'invariant';
3 |
4 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
5 | function PortType({ type, component }: { type: string; component: React.ReactNode }) {
6 | invariant(
7 | false,
8 | ' elements are for DataFlow configuration only and should not be rendered',
9 | );
10 | return null;
11 | }
12 |
13 | PortType.displayName = 'PortType';
14 |
15 | export default PortType;
16 |
--------------------------------------------------------------------------------
/src/components/configuration/NodeType.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import NodeType from './NodeType.component';
4 |
5 | const mockComponent = () => ;
6 |
7 | describe('Testing ', () => {
8 | it('should log an error if rendered', () => {
9 | expect(() => {
10 | shallow();
11 | }).toThrowError(
12 | ' elements are for DataFlow configuration only and should not be rendered',
13 | );
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/.changeset/README.md:
--------------------------------------------------------------------------------
1 | # Changesets
2 |
3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
4 | with multi-package repos, or single-package repos to help you version and publish your code. You can
5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets)
6 |
7 | We have a quick list of common questions to get you started engaging with this project in
8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
9 |
--------------------------------------------------------------------------------
/licence.js:
--------------------------------------------------------------------------------
1 | const licenceTemplate = `============================================================================
2 |
3 | Copyright (C) 2006-${new Date().getFullYear()} Talend Inc. - www.talend.com
4 |
5 | This source code is available under agreement available at
6 | https://github.com/Talend/react-flow-designer/blob/master/LICENSE
7 |
8 | You should have received a copy of the agreement
9 | along with this program; if not, write to Talend SA
10 | 9 rue Pages 92150 Suresnes, France
11 |
12 | ============================================================================`;
13 |
14 | module.exports = licenceTemplate;
15 |
--------------------------------------------------------------------------------
/src/components/link/LinkHandle.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import renderer from 'react-test-renderer';
3 | import LinkHandle from './LinkHandle.component';
4 | import { PositionRecord } from '../../constants/flowdesigner.model';
5 |
6 | const MockComponent = () => ;
7 |
8 | describe(' renders correctly', () => {
9 | it(' renders correctly', () => {
10 | const children = ;
11 | const position = new PositionRecord({ x: 10, y: 10 });
12 | const tree = renderer
13 | .create()
14 | .toJSON();
15 | expect(tree).toMatchSnapshot();
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/src/components/port/PortsRenderer.component.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import get from 'lodash/get';
3 |
4 | import { Port } from '../../api';
5 | import { PortRecord, PortRecordMap } from '../../customTypings/index.d';
6 |
7 | function PortsRenderer({ ports, portTypeMap }: { ports: PortRecordMap; portTypeMap: Object }) {
8 | const renderPort = (port: PortRecord) => {
9 | const type = Port.getComponentType(port);
10 | const ConcretePort = get((portTypeMap as any)[type], 'component');
11 | return ;
12 | };
13 |
14 | return {ports.valueSeq().map(renderPort)};
15 | }
16 |
17 | export default PortsRenderer;
18 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: React Flow Designer CI
2 | on:
3 | pull_request:
4 | branches: [master]
5 |
6 | jobs:
7 | build:
8 | runs-on: ubuntu-latest
9 | name: Build and test
10 |
11 | steps:
12 | - name: Checkout sources
13 | uses: actions/checkout@v2
14 |
15 | - name: Use Node.js 14
16 | uses: actions/setup-node@v2
17 | with:
18 | node-version: 14
19 | registry-url: 'https://registry.npmjs.org/'
20 | scope: '@talend'
21 | cache: 'yarn'
22 |
23 | - name: Install
24 | run: yarn install --frozen-lockfile
25 |
26 | - name: Test
27 | run: yarn test
28 |
29 | - name: Lint
30 | run: yarn lint
31 |
--------------------------------------------------------------------------------
/src/actions/flow.actions.test.ts:
--------------------------------------------------------------------------------
1 | import * as flowActions from './flow.actions';
2 |
3 | describe('Check that flowActions generate proper action objects', () => {
4 | it('addFlowElements generate proper action object', () => {
5 | expect(flowActions.addFlowElements([])).toEqual({
6 | type: 'FLOWDESIGNER.FLOW_ADD_ELEMENTS',
7 | listOfActionCreation: [],
8 | });
9 | });
10 | });
11 |
12 | describe('setZoom', () => {
13 | it('should generate an action with proper shape', () => {
14 | expect(flowActions.setZoom({ k: 0, x: 0, y: 0 })).toMatchSnapshot();
15 | });
16 |
17 | it('if transform object is not well set return null', () => {
18 | expect(flowActions.setZoom({ k: 'yolo' as any, x: 0, y: 0 })).toMatchSnapshot();
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/src/constants/flowdesigner.proptypes.ts:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import { recordOf } from 'react-immutable-proptypes';
3 |
4 | export const NodeType = recordOf({
5 | id: PropTypes.string.isRequired,
6 | position: recordOf({
7 | x: PropTypes.number.isRequired,
8 | y: PropTypes.number.isRequired,
9 | }),
10 | });
11 |
12 | export const PortType = recordOf({
13 | id: PropTypes.string.isRequired,
14 | nodeId: PropTypes.string.isRequired,
15 | position: recordOf({
16 | x: PropTypes.number.isRequired,
17 | y: PropTypes.number.isRequired,
18 | }),
19 | });
20 |
21 | export const LinkType = recordOf({
22 | id: PropTypes.string.isRequired,
23 | sourceId: PropTypes.string.isRequired,
24 | targetId: PropTypes.string.isRequired,
25 | });
26 |
--------------------------------------------------------------------------------
/src/components/port/AbstractPort.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import renderer from 'react-test-renderer';
3 |
4 | import AbstractPort from './AbstractPort.component';
5 | import {
6 | PortRecord,
7 | PositionRecord,
8 | PortGraphicalAttributes,
9 | } from '../../constants/flowdesigner.model';
10 |
11 | describe(' renders correctly', () => {
12 | it(' renders correctly', () => {
13 | const port = new PortRecord({
14 | id: 'idPort',
15 | nodeId: 'nodeId',
16 | graphicalAttributes: new PortGraphicalAttributes({
17 | position: new PositionRecord({
18 | x: 100,
19 | y: 100,
20 | }),
21 | }),
22 | });
23 | const tree = renderer.create().toJSON();
24 | expect(tree).toMatchSnapshot();
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/src/components/grid/__snapshots__/Grid.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Grid.component should render a grid by default 1`] = `
4 |
5 |
6 |
17 |
21 |
24 |
25 |
26 |
38 |
39 | `;
40 |
--------------------------------------------------------------------------------
/test/test-setup.js:
--------------------------------------------------------------------------------
1 | jest.mock('react-i18next', () => ({
2 | // this mock makes sure any components using the translate HoC receive the t function as a prop
3 | withTranslation: () => Component => {
4 | Component.defaultProps = {
5 | ...Component.defaultProps,
6 | t: (key, options) =>
7 | (options.defaultValue || '').replace(/{{(\w+)}}/g, (_, k) => options[k]),
8 | };
9 | Component.displayName = `withI18nextTranslation(${
10 | Component.displayName || Component.name
11 | })`;
12 | return Component;
13 | },
14 | setI18n: () => {},
15 | getI18n: () => ({
16 | t: (key, options) =>
17 | (options.defaultValue || '').replace(/{{(\w+)}}/g, (_, k) => options[k]),
18 | }),
19 | }));
20 |
21 | jest.mock('i18next', () => ({
22 | t: (key, options) => (options.defaultValue || '').replace(/{{(\w+)}}/g, (_, k) => options[k]),
23 | createInstance: () => {},
24 | }));
25 |
--------------------------------------------------------------------------------
/src/components/__snapshots__/ZoomHandler.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`ZoomHandler renders correctly renders correctly renders correctly 1`] = `
4 |
10 |
22 |
32 |
42 |
43 | `;
44 |
--------------------------------------------------------------------------------
/src/components/__snapshots__/FlowDesigner.container.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[` renders correctly renders correctly 1`] = `
4 |
42 | `;
43 |
--------------------------------------------------------------------------------
/src/api/throwInDev.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Throw {message} only in dev mode
3 | * @param {string} message
4 | */
5 | export function throwInDev(message: string | TypeError) {
6 | if (!(process.env.NODE_ENV === 'production')) {
7 | throw message;
8 | }
9 | }
10 |
11 | /**
12 | * Throw a type error
13 | * @todo for ease of use param should be an object {
14 | * expected: 'Linkrecord',
15 | * given: link,
16 | * paramName: 'link',
17 | * module: 'Link'
18 | * }
19 | * @param {string} expected - describe expected type
20 | * @param {Object} given - the given param
21 | * @param {string} module - (optionnal) module to use
22 | */
23 | export function throwTypeError(expected: string, given: Object, module?: string | undefined) {
24 | throwInDev(
25 | new TypeError(`${expected || 'parameter'} should be a ${expected}, was given
26 | """
27 | ${typeof given}
28 | """
29 | ${given && given.toString()}
30 | """
31 | ${module && `you should use ${module} module functions to create and transform ${module}`}`),
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/src/components/port/PortsRenderer.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import renderer from 'react-test-renderer';
3 | import { Map } from 'immutable';
4 |
5 | import PortsRenderer from './PortsRenderer.component';
6 | import { PortRecord } from '../../constants/flowdesigner.model';
7 | import { Id, PortRecord as PortRecordType } from '../../customTypings/index.d';
8 |
9 | const MockPort = () => MockPort;
10 |
11 | describe('', () => {
12 | it('renders correctly', () => {
13 | const ports = Map().set(
14 | 'id',
15 | new PortRecord({
16 | id: 'id',
17 | nodeId: 'nodeId',
18 | graphicalAttributes: Map({
19 | portType: 'id',
20 | }),
21 | }),
22 | );
23 | const portTypeMap = {
24 | id: {
25 | id: 'id',
26 | component: MockPort,
27 | },
28 | };
29 | const tree = renderer
30 | .create()
31 | .toJSON();
32 | expect(tree).toMatchSnapshot();
33 | });
34 | });
35 |
--------------------------------------------------------------------------------
/src/selectors/__snapshots__/nodeSelectors.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Testing node selectors node1 should have 7 successors 1`] = `
4 | Immutable.Set [
5 | "id8",
6 | "id7",
7 | "id5",
8 | "id3",
9 | "id6",
10 | "id4",
11 | "id2",
12 | ]
13 | `;
14 |
15 | exports[`Testing node selectors node1 should not have any predecessors 1`] = `Immutable.Set []`;
16 |
17 | exports[`Testing node selectors node4 should have node1, node2 as predecessors 1`] = `
18 | Immutable.Set [
19 | "id1",
20 | "id2",
21 | ]
22 | `;
23 |
24 | exports[`Testing node selectors node4 should have node6, node7, node8 as successors 1`] = `
25 | Immutable.Set [
26 | "id8",
27 | "id7",
28 | "id6",
29 | ]
30 | `;
31 |
32 | exports[`Testing node selectors node8 should have 7 predecessors 1`] = `
33 | Immutable.Set [
34 | "id1",
35 | "id2",
36 | "id3",
37 | "id5",
38 | "id4",
39 | "id6",
40 | "id7",
41 | ]
42 | `;
43 |
44 | exports[`Testing node selectors node8 should not have any successors 1`] = `Immutable.Set []`;
45 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | **What is the problem this PR is trying to solve?**
2 |
3 | **What is the chosen solution to this problem?**
4 |
5 | **Please check if the PR fulfills these requirements**
6 |
7 | - [ ] The PR commit message follows our [guidelines](https://github.com/talend/tools/blob/master/tools-root-github/CONTRIBUTING.md#commit-message-format)
8 | - [ ] Tests for the changes have been added (for bug fixes / features)
9 | - [ ] Docs have been added / updated (for bug fixes / features)
10 | - [ ] Related design / discussions / pages, if any, are all linked or available in the PR
11 |
12 |
13 |
14 | **[ ] This PR introduces a breaking change**
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/selectors/nodeSelectors.ts:
--------------------------------------------------------------------------------
1 | import { Set } from 'immutable';
2 | import { State, Id } from '../customTypings/index.d';
3 |
4 | /**
5 | * @param state Map flow state
6 | * @param nodeId String
7 | * @param predecessors Set list of already determined predecessors
8 | */
9 | export function getPredecessors(state: State, nodeId: Id, predecessors?: Set) {
10 | return state.getIn(['parents', nodeId]).reduce(
11 | (accumulator: Set, parentId: Id) =>
12 | getPredecessors(state, parentId, accumulator).add(parentId),
13 | // eslint-disable-next-line new-cap
14 | predecessors || Set(),
15 | );
16 | }
17 |
18 | /**
19 | * @param state Map flow state
20 | * @param nodeId String
21 | * @param successors Set list of already determined successors
22 | */
23 | export function getSuccessors(state: State, nodeId: Id, successors?: Set) {
24 | return state.getIn(['childrens', nodeId]).reduce(
25 | (accumulator: Set, childrenId: Id) =>
26 | getSuccessors(state, childrenId, accumulator).add(childrenId),
27 | // eslint-disable-next-line new-cap
28 | successors || Set(),
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/src/components/ZoomHandler.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import renderer from 'react-test-renderer';
3 |
4 | import ZoomHandler, { transformToString } from './ZoomHandler.component';
5 |
6 | describe('ZoomHandler renders correctly', () => {
7 | describe(' renders correctly', () => {
8 | it(' renders correctly', () => {
9 | // given
10 | const transform = { x: 0, y: 0, k: 1 };
11 |
12 | // when
13 | const tree = renderer
14 | .create(
15 |
16 |
17 |
18 | ,
19 | )
20 | .toJSON();
21 |
22 | // expect
23 | expect(tree).toMatchSnapshot();
24 | });
25 | });
26 | describe('transformToString', () => {
27 | // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform
28 | it('generate proper transform string', () => {
29 | // given
30 | const transform = { x: 1, y: 2, k: -3 };
31 |
32 | // when
33 | const stringResult = transformToString(transform);
34 |
35 | // expect
36 | expect(stringResult).toEqual(
37 | `translate(${transform.x}, ${transform.y}) scale(${transform.k})`,
38 | );
39 | });
40 | });
41 | });
42 |
--------------------------------------------------------------------------------
/src/components/node/AbstractNode.snapshot.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import renderer from 'react-test-renderer';
3 |
4 | import AbstractNode from './AbstractNode.component';
5 | import {
6 | NodeGraphicalAttributes,
7 | NodeRecord,
8 | PositionRecord,
9 | SizeRecord,
10 | } from '../../constants/flowdesigner.model';
11 |
12 | jest.mock('d3', () => {
13 | const original = jest.requireActual('d3');
14 | return {
15 | ...original,
16 | select() {
17 | return { data() {}, call() {} };
18 | },
19 | };
20 | });
21 |
22 | const noOp = () => {};
23 |
24 | describe('', () => {
25 | it('renders correctly', () => {
26 | const node = new NodeRecord({
27 | id: 'id',
28 | graphicalAttributes: new NodeGraphicalAttributes({
29 | position: new PositionRecord({ x: 100, y: 100 }),
30 | nodeSize: new SizeRecord({ width: 125, height: 75 }),
31 | }),
32 | });
33 | const tree = renderer
34 | .create(
35 |
41 |
42 | ,
43 | )
44 | .toJSON();
45 | expect(tree).toMatchSnapshot();
46 | });
47 | });
48 |
--------------------------------------------------------------------------------
/src/components/link/LinksRenderer.component.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import invariant from 'invariant';
3 | import get from 'lodash/get';
4 | import { LinkRecordMap, PortRecordMap, LinkRecord } from '../../customTypings/index.d';
5 |
6 | type Props = {
7 | links: LinkRecordMap;
8 | ports: PortRecordMap;
9 | linkTypeMap: Object;
10 | };
11 |
12 | class LinksRender extends React.Component {
13 | constructor(props: Props) {
14 | super(props);
15 | this.renderLink = this.renderLink.bind(this);
16 | }
17 |
18 | renderLink(link: LinkRecord) {
19 | const ConcreteLink = get((this.props.linkTypeMap as any)[link.getLinkType()], 'component');
20 | const source = this.props.ports.get(link.sourceId);
21 | const target = this.props.ports.get(link.targetId);
22 | if (!ConcreteLink) {
23 | invariant(
24 | false,
25 | ` the defined link type in your graph model
26 | hasn't been mapped into the dataflow configuration,
27 | check LinkType documentation`,
28 | );
29 | }
30 | return ;
31 | }
32 |
33 | render() {
34 | return {this.props.links.valueSeq().map(this.renderLink)};
35 | }
36 | }
37 |
38 | export default LinksRender;
39 |
--------------------------------------------------------------------------------
/src/components/FlowDesigner.container.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import renderer from 'react-test-renderer';
3 | import { Map } from 'immutable';
4 |
5 | import { FlowDesigner } from './FlowDesigner.container';
6 | import NodeType from './configuration/NodeType.component';
7 | import { NodeRecord, Id, PortRecord, LinkRecord } from '../customTypings/index.d';
8 |
9 | jest.mock('./ZoomHandler.component');
10 | jest.mock('./grid/Grid.component', () => {
11 | return null;
12 | });
13 |
14 | const noOp = () => {};
15 |
16 | describe(' renders correctly', () => {
17 | it(' renders correctly', () => {
18 | const nodes = Map();
19 | const ports = Map();
20 | const links = Map();
21 | const tree = renderer
22 | .create(
23 |
33 |
34 |
35 | ,
36 | )
37 | .toJSON();
38 | expect(tree).toMatchSnapshot();
39 | });
40 | });
41 |
--------------------------------------------------------------------------------
/src/components/grid/Grid.component.tsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React from 'react';
3 | import get from 'lodash/get';
4 |
5 | import { GRID_SIZE } from '../../constants/flowdesigner.constants';
6 | import { Transform } from '../../customTypings/index.d';
7 |
8 | function Grid({ transformData }: { transformData?: Transform }) {
9 | const largeGridSize = GRID_SIZE * get(transformData, 'k', 1);
10 | return (
11 |
12 |
13 |
24 |
25 |
26 |
27 |
28 |
36 |
37 | );
38 | }
39 |
40 | Grid.propTypes = {
41 | transformData: PropTypes.shape({
42 | k: PropTypes.number.isRequired,
43 | x: PropTypes.number.isRequired,
44 | y: PropTypes.number.isRequired,
45 | }),
46 | };
47 |
48 | export default Grid;
49 |
--------------------------------------------------------------------------------
/.github/workflows/changeset.yml:
--------------------------------------------------------------------------------
1 | name: Changeset (Release)
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 |
8 | jobs:
9 | release:
10 | name: Release
11 | runs-on: ubuntu-latest
12 | steps:
13 | - name: Checkout Repo
14 | uses: actions/checkout@master
15 | with:
16 | # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits
17 | fetch-depth: 0
18 |
19 | - name: Setup Node.js 14.x
20 | uses: actions/setup-node@master
21 | with:
22 | node-version: 14.x
23 | registry-url: 'https://registry.npmjs.org/'
24 | cache: 'yarn'
25 |
26 | - name: Install Dependencies
27 | run: yarn && yarn pre-release
28 |
29 | - name: Create Release Pull Request or Publish to npm
30 | id: changesets
31 | uses: changesets/action@master
32 | with:
33 | # This expects you to have a script called release which does a build for your packages and calls changeset publish
34 | publish: yarn release
35 | commit: "chore: prepare release"
36 | title: "chore: prepare release"
37 | env:
38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
39 | NPM_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }}
40 | NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }}
41 |
--------------------------------------------------------------------------------
/src/components/port/AbstractPort.component.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { select } from 'd3';
3 |
4 | import { Port, Position } from '../../api';
5 | import { PortRecord } from '../../customTypings/index.d';
6 |
7 | type Props = {
8 | port?: PortRecord;
9 | onClick?: React.MouseEventHandler;
10 | children?: React.ReactChildren;
11 | };
12 |
13 | class AbstractPort extends React.Component {
14 | d3Node: any;
15 |
16 | node: any;
17 |
18 | constructor(props: Props) {
19 | super(props);
20 | this.onClick = this.onClick.bind(this);
21 | }
22 |
23 | componentDidMount() {
24 | this.d3Node = select(this.node);
25 | this.d3Node.on('click', this.onClick);
26 | }
27 |
28 | shouldComponentUpdate(nextProps: Props) {
29 | return nextProps.port !== this.props.port || nextProps.children !== this.props.children;
30 | }
31 |
32 | onClick(event: any) {
33 | if (this.props.onClick) {
34 | this.props.onClick(event);
35 | }
36 | }
37 |
38 | render() {
39 | const position = Port.getPosition(this.props.port);
40 | return (
41 |
42 | {
44 | this.node = c;
45 | }}
46 | transform={`translate(${Position.getXCoordinate(
47 | position,
48 | )},${Position.getYCoordinate(position)})`}
49 | >
50 | {this.props.children}
51 |
52 |
53 | );
54 | }
55 | }
56 |
57 | export default AbstractPort;
58 |
--------------------------------------------------------------------------------
/src/components/link/LinkHandle.component.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { drag, select } from 'd3';
3 | import { PositionRecord } from '../../customTypings/index.d';
4 |
5 | type Props = {
6 | position: PositionRecord;
7 | onDrag?: (event: any) => void;
8 | onDragEnd?: (event: any) => void;
9 | component: React.ReactElement;
10 | };
11 |
12 | class LinkHandle extends React.Component {
13 | d3Handle: any;
14 |
15 | handle: React.ElementRef<'g'> | null;
16 |
17 | constructor(props: Props) {
18 | super(props);
19 | this.drag = this.drag.bind(this);
20 | this.dragEnd = this.dragEnd.bind(this);
21 | this.handle = null;
22 | }
23 |
24 | componentDidMount() {
25 | this.d3Handle = select(this.handle);
26 | this.d3Handle.call(drag().on('drag', this.drag).on('end', this.dragEnd));
27 | }
28 |
29 | componentWillUnmount() {
30 | this.d3Handle.remove();
31 | }
32 |
33 | drag(event: any) {
34 | if (this.props.onDrag) {
35 | this.props.onDrag(event);
36 | }
37 | }
38 |
39 | dragEnd(event: any) {
40 | if (this.props.onDragEnd) {
41 | this.props.onDragEnd(event);
42 | }
43 | }
44 |
45 | render() {
46 | const position = this.props.position;
47 | return (
48 | {
50 | this.handle = c;
51 | }}
52 | transform={`translate(${position.get('x')},${position.get('y')})`}
53 | >
54 | {this.props.component}
55 |
56 | );
57 | }
58 | }
59 |
60 | export default LinkHandle;
61 |
--------------------------------------------------------------------------------
/src/actions/flow.actions.ts:
--------------------------------------------------------------------------------
1 | import {
2 | FLOWDESIGNER_FLOW_ADD_ELEMENTS,
3 | FLOWDESIGNER_FLOW_RESET,
4 | FLOWDESIGNER_FLOW_LOAD,
5 | FLOWDESIGNER_FLOW_SET_ZOOM,
6 | FLOWDESIGNER_FLOW_ZOOM_IN,
7 | FLOWDESIGNER_FLOW_ZOOM_OUT,
8 | } from '../constants/flowdesigner.constants';
9 |
10 | /**
11 | * Ask to sequentially add elements to the flow, each creation should be checked against store,
12 | * then applied via current reducers
13 | *
14 | * @params {array} listOfActionCreation
15 | */
16 | export const addFlowElements = (listOfActionCreation: any) => ({
17 | type: FLOWDESIGNER_FLOW_ADD_ELEMENTS,
18 | listOfActionCreation,
19 | });
20 |
21 | /**
22 | * ask for flow reset, emptying, nodes, links, ports collections
23 | */
24 | export const resetFlow = () => ({
25 | type: FLOWDESIGNER_FLOW_RESET,
26 | });
27 |
28 | /**
29 | * reset old flow, load elements for the new flow
30 | */
31 | export const loadFlow = (listOfActionCreation: any) => ({
32 | type: FLOWDESIGNER_FLOW_LOAD,
33 | listOfActionCreation,
34 | });
35 |
36 | export function setZoom(transform: { k: number; x: number; y: number }) {
37 | if (!isNaN(transform.k) && !isNaN(transform.x) && !isNaN(transform.y)) {
38 | return {
39 | type: FLOWDESIGNER_FLOW_SET_ZOOM,
40 | transform,
41 | };
42 | }
43 | return null;
44 | }
45 |
46 | export function zoomIn(scale: number) {
47 | return {
48 | type: FLOWDESIGNER_FLOW_ZOOM_IN,
49 | scale,
50 | };
51 | }
52 |
53 | export function zoomOut(scale: number) {
54 | return {
55 | type: FLOWDESIGNER_FLOW_ZOOM_OUT,
56 | scale,
57 | };
58 | }
59 |
--------------------------------------------------------------------------------
/src/components/node/NodesRenderer.component.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import invariant from 'invariant';
3 | import get from 'lodash/get';
4 | import { NodeRecordMap, NodeRecord, Id, Position } from '../../customTypings/index.d';
5 |
6 | type Props = {
7 | nodes: NodeRecordMap;
8 | nodeTypeMap: Object;
9 | startMoveNodeTo: (nodeId: Id, nodePosition: string) => void;
10 | moveNodeTo: (nodeId: Id, nodePosition: Position) => void;
11 | moveNodeToEnd: (nodeId: Id, nodePosition: Position) => void;
12 | snapToGrid: boolean;
13 | };
14 |
15 | class NodesRenderer extends React.Component {
16 | constructor(props: Props) {
17 | super(props);
18 | this.renderNode = this.renderNode.bind(this);
19 | }
20 |
21 | renderNode(node: NodeRecord) {
22 | const type = node.getNodeType();
23 | const ConcreteComponent = get((this.props.nodeTypeMap as any)[type], 'component');
24 | if (!ConcreteComponent) {
25 | invariant(
26 | false,
27 | ` the defined node type in your graph model hasn't been mapped into
28 | the dataflow configuration, check NodeType documentation`,
29 | );
30 | }
31 | return (
32 |
40 | );
41 | }
42 |
43 | render() {
44 | return {this.props.nodes.valueSeq().map(this.renderNode)};
45 | }
46 | }
47 |
48 | export default NodesRenderer;
49 |
--------------------------------------------------------------------------------
/.github/workflows/update-yarn-talend.yml:
--------------------------------------------------------------------------------
1 | name: Yarn talend auto upgrade
2 | on:
3 | workflow_dispatch:
4 | schedule:
5 | # Every tuesday
6 | - cron: '0 13 * * TUE'
7 |
8 | jobs:
9 | upgrade:
10 | name: Upgrade yarn @talend dependencies
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - uses: actions/checkout@master
15 | - name: Use Node.js 14
16 | uses: actions/setup-node@v2
17 | with:
18 | node-version: 14
19 | registry-url: 'https://registry.npmjs.org/'
20 | scope: '@talend'
21 | cache: 'yarn'
22 |
23 | # NODE_AUTH_TOKEN is not working with "npx" command..
24 | - name: Upgrade talend dependencies
25 | run: |
26 | yarn
27 | yarn talend-scripts upgrade:deps
28 | yarn talend-scripts upgrade:deps --latest --dry > dependencies-latest.md
29 | git add dependencies-latest.md
30 | env:
31 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
32 |
33 | - name: Create Pull Request
34 | uses: peter-evans/create-pull-request@v3
35 | with:
36 | assignees: tlnd-mhuchet
37 | reviewers: tlnd-mhuchet
38 | commit-message: 'chore(scripts): Upgrade dependencies'
39 | title: 'chore(scripts): Upgrade talend dependencies'
40 | branch: ci/chore/upgrade_webapp_talend_dependencies
41 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import FlowDesigner, { FlowDesigner as FlowDesignerComponent } from './components/FlowDesigner.container';
2 | import AbstractNode from './components/node/AbstractNode.component';
3 | import AbstractLink from './components/link/AbstractLink.component';
4 | import AbstractPort from './components/port/AbstractPort.component';
5 | import NodeType from './components/configuration/NodeType.component';
6 | import LinkType from './components/configuration/LinkType.component';
7 | import PortType from './components/configuration/PortType.component';
8 | import flowDesignerReducer from './reducers';
9 | import * as flowDesignerConstants from './constants/flowdesigner.constants';
10 | import * as flowActions from './actions/flow.actions';
11 | import * as nodeActions from './actions/node.actions';
12 | import * as portActions from './actions/port.actions';
13 | import * as linkActions from './actions/link.actions';
14 | import * as portSelectors from './selectors/portSelectors';
15 | import * as nodeSelectors from './selectors/nodeSelectors';
16 | import * as flowPropTypes from './constants/flowdesigner.proptypes';
17 | import * as flowModels from './constants/flowdesigner.model';
18 |
19 | export {
20 | flowDesignerReducer,
21 | FlowDesigner,
22 | FlowDesignerComponent,
23 | AbstractNode,
24 | AbstractLink,
25 | AbstractPort,
26 | NodeType,
27 | LinkType,
28 | PortType,
29 | flowDesignerConstants, // should i share ?
30 | flowActions,
31 | nodeActions,
32 | portActions,
33 | linkActions,
34 | portSelectors,
35 | nodeSelectors,
36 | flowPropTypes,
37 | flowModels,
38 | };
39 |
--------------------------------------------------------------------------------
/src/api/readme.md:
--------------------------------------------------------------------------------
1 | # Uncertainty
2 | How uncertainty is handled in this API.
3 |
4 | ## extracting values from a given value (get*)
5 | On dev mode:
6 | - if the parameters are not fitting, throw an exception
7 |
8 | On prod mode:
9 | - if parameter are not fitting, don't throw, but return null.
10 |
11 | ```javascript
12 | // dev
13 | expect(Port.getPosition(invalidPort)).toThrow();
14 | // throw
15 |
16 | // prod
17 | expect(Port.getPosition(invalidPort)).toBe(null);
18 | // return null
19 | ```
20 |
21 | ## questioning the given value (is*, has*)
22 | usualy questioning return a boolean. In this case the answer should never be something else than true or false
23 |
24 | ```javascript
25 | // dev
26 | expect(Port.isPort(invalidPort)).toThrow();
27 | // throw
28 |
29 | // prod
30 | expect(Port.isPort(invalidPort)).toBe(false);
31 | // return null
32 | ```
33 |
34 | ### transforming the given value (set*)
35 | to avoid undefined error, in case the transformation is not applicable the original value is returned
36 |
37 | ```javascript
38 | // dev
39 | expect(Port.setId(id, invalidPort)).toThrow();
40 | // throw
41 |
42 | // prod
43 | expect(Port.isPort(id, invalidPort)).toBe(invalidPort);
44 | // return null
45 | ```
46 |
47 | Why null returned when expected return is not a boolean ?
48 | Because "" for string or 0 or NaN doesn't capture well the intent of providing a default neutral value for number and string, same apply to object. Undefined is not suitable to because undefined error often are unhandled error in your code or a remote code.
49 |
50 | A better solution overall would be the use of Maybe wrapper.
51 |
--------------------------------------------------------------------------------
/src/components/link/LinksRenderer.test.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable new-cap */
2 | import React from 'react';
3 | import renderer from 'react-test-renderer';
4 | import { Map, OrderedMap } from 'immutable';
5 |
6 | import LinksRenderer from './LinksRenderer.component';
7 | import { LinkRecord, PortRecord, PositionRecord } from '../../constants/flowdesigner.model';
8 | import {
9 | Id,
10 | LinkRecord as LinkRecordType,
11 | PortRecord as PortRecordType,
12 | } from '../../customTypings/index.d';
13 |
14 | const MockLink = () => MockLink;
15 |
16 | describe('', () => {
17 | it('renders correctly', () => {
18 | const links = Map().set(
19 | 'id',
20 | new LinkRecord({
21 | id: 'id',
22 | sourceId: 'port1',
23 | targetId: 'port2',
24 | graphicalAttributes: Map({
25 | linkType: 'id',
26 | }),
27 | }),
28 | );
29 | const ports = OrderedMap()
30 | .set(
31 | 'port1',
32 | new PortRecord({
33 | id: 'port1',
34 | nodeId: 'nodeId',
35 | graphicalAttributes: Map({
36 | position: new PositionRecord({ x: 100, y: 100 }),
37 | }),
38 | }),
39 | )
40 | .set(
41 | 'port2',
42 | new PortRecord({
43 | id: 'port2',
44 | nodeId: 'nodeId',
45 | graphicalAttributes: Map({
46 | position: new PositionRecord({ x: 200, y: 200 }),
47 | }),
48 | }),
49 | );
50 | const linkTypeMap = { id: { id: 'id', component: MockLink } };
51 | const tree = renderer
52 | .create()
53 | .toJSON();
54 | expect(tree).toMatchSnapshot();
55 | });
56 | });
57 |
--------------------------------------------------------------------------------
/src/selectors/linkSelectors.ts:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect';
2 | import { Map } from 'immutable';
3 | import {
4 | State,
5 | PortRecordMap,
6 | LinkRecordMap,
7 | LinkRecord,
8 | PortRecord,
9 | Id,
10 | } from '../customTypings/index.d';
11 |
12 | const getPorts = (state: State): PortRecordMap => state.get('ports');
13 | const getLinks = (state: State): LinkRecordMap => state.get('links');
14 |
15 | export const getDetachedLinks = createSelector(
16 | [getLinks, getPorts],
17 | (links: LinkRecordMap, ports: PortRecordMap) =>
18 | links.filter(
19 | (link: LinkRecord) =>
20 | !ports.find((port: PortRecord) => port.id === link.sourceId) ||
21 | !ports.find((port: PortRecord) => port.id === link.targetId),
22 | ),
23 | );
24 |
25 | /**
26 | * get outgoing link from a port
27 | *
28 | * @return {Link}
29 | */
30 | export function portOutLink(state: State, portId: Id) {
31 | return state.get('links').filter((link: LinkRecord) => link.sourceId === portId) || Map();
32 | }
33 |
34 | /**
35 | * get ingoing link from a port
36 | *
37 | * @return {Link}
38 | */
39 | export function portInLink(state: State, portId: Id) {
40 | return state.get('links').filter((link: LinkRecord) => link.targetId === portId) || Map();
41 | }
42 |
43 | /**
44 | * get outgoing linkId from a node
45 | *
46 | * @return number
47 | */
48 | export function outLink(state: State, nodeId: Id) {
49 | return state
50 | .getIn(['out', nodeId])
51 | .reduce((reduction: PortRecordMap, port: PortRecord) => reduction.merge(port), Map());
52 | }
53 |
54 | /**
55 | * get inGoing linkId from a node
56 | *
57 | * @return number
58 | */
59 | export function inLink(state: State, nodeId: Id) {
60 | return state
61 | .getIn(['in', nodeId])
62 | .reduce((reduction: PortRecordMap, port: PortRecord) => reduction.merge(port), Map());
63 | }
64 |
65 | export default getDetachedLinks;
66 |
--------------------------------------------------------------------------------
/src/components/node/NodesRenderer.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import renderer from 'react-test-renderer';
3 | import { List, Map } from 'immutable';
4 |
5 | import NodesRenderer from './NodesRenderer.component';
6 | import { NestedNodeRecord, NodeRecord, NodeGraphicalAttributes } from '../../constants/flowdesigner.model';
7 | import { NodeRecord as NodeRecordType } from '../../customTypings/index.d';
8 |
9 | const MockNode = () => MockNodes;
10 |
11 | const noOp = () => {};
12 |
13 | describe('', () => {
14 | it('renders correctly', () => {
15 | const nodes = Map().set(
16 | 'id',
17 | new NodeRecord({
18 | id: 'id',
19 | type: 'id',
20 | graphicalAttributes: new NodeGraphicalAttributes({
21 | nodeType: 'id',
22 | }),
23 | }),
24 | );
25 | const nodeTypeMap = { id: { id: 'id', component: MockNode } };
26 | const tree = renderer
27 | .create(
28 | ,
36 | )
37 | .toJSON();
38 | expect(tree).toMatchSnapshot();
39 | });
40 | it('renders correctly if nested', () => {
41 | const nodes = Map().set(
42 | 'id',
43 | new NestedNodeRecord({
44 | id: 'id',
45 | type: 'id',
46 | graphicalAttributes: new NodeGraphicalAttributes({
47 | nodeType: 'id',
48 | }),
49 | components: List(),
50 | }),
51 | );
52 | const nodeTypeMap = { id: { id: 'id', component: MockNode } };
53 | const tree = renderer
54 | .create(
55 | ,
63 | )
64 | .toJSON();
65 | expect(tree).toMatchSnapshot();
66 | });
67 | });
68 |
--------------------------------------------------------------------------------
/src/actions/__snapshots__/port.actions.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Check that port action creators generate proper action objects and perform checking addPort 1`] = `
4 | Array [
5 | Object {
6 | "data": Object {
7 | "flowType": "string",
8 | },
9 | "graphicalAttributes": Object {
10 | "portType": "test",
11 | "properties": Object {
12 | "type": "INCOMING",
13 | },
14 | },
15 | "id": "portId",
16 | "nodeId": "nodeId",
17 | "type": "FLOWDESIGNER_PORT_ADD",
18 | },
19 | ]
20 | `;
21 |
22 | exports[`Check that port action creators generate proper action objects and perform checking removePort 1`] = `
23 | Array [
24 | Object {
25 | "portId": "portId",
26 | "type": "FLOWDESIGNER_PORT_REMOVE",
27 | },
28 | ]
29 | `;
30 |
31 | exports[`Check that port action creators generate proper action objects and perform checking removePortAttribute 1`] = `
32 | Array [
33 | Object {
34 | "graphicalAttributesKey": "selected",
35 | "portId": "id",
36 | "type": "FLOWDESIGNER_PORT_REMOVE_GRAPHICAL_ATTRIBUTES",
37 | },
38 | ]
39 | `;
40 |
41 | exports[`Check that port action creators generate proper action objects and perform checking removePortData 1`] = `
42 | Array [
43 | Object {
44 | "dataKey": "type",
45 | "portId": "id",
46 | "type": "FLOWDESIGNER_PORT_REMOVE_DATA",
47 | },
48 | ]
49 | `;
50 |
51 | exports[`Check that port action creators generate proper action objects and perform checking setPortData 1`] = `
52 | Array [
53 | Object {
54 | "data": Object {
55 | "type": "test",
56 | },
57 | "portId": "id",
58 | "type": "FLOWDESIGNER_PORT_SET_DATA",
59 | },
60 | ]
61 | `;
62 |
63 | exports[`Check that port action creators generate proper action objects and perform checking setPortGraphicalAttribute 1`] = `
64 | Array [
65 | Object {
66 | "graphicalAttributes": Object {
67 | "selected": true,
68 | },
69 | "portId": "id",
70 | "type": "FLOWDESIGNER_PORT_SET_GRAPHICAL_ATTRIBUTES",
71 | },
72 | ]
73 | `;
74 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-flow-designer",
3 | "description": "Flow designer for react and redux",
4 | "version": "4.2.2",
5 | "main": "lib/index.js",
6 | "mainSrc": "src/index.js",
7 | "scripts": {
8 | "build": "talend-scripts build:ts:lib",
9 | "lint": "talend-scripts lint:es",
10 | "test": "talend-scripts test --silent",
11 | "pre-release": "talend-scripts build:lib:umd",
12 | "prepare": "yarn build",
13 | "release": "yarn changeset publish"
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "https://github.com/Talend/react-flow-designer.git"
18 | },
19 | "author": "Talend ",
20 | "license": "Apache-2.0",
21 | "devDependencies": {
22 | "@changesets/cli": "^2.17.0",
23 | "@talend/scripts-config-jest": "^9.6.4",
24 | "@talend/scripts-core": "^11.0.1",
25 | "@talend/scripts-preset-react-lib": "^9.9.2",
26 | "@types/d3": "^6.7.5",
27 | "@types/enzyme": "^3.10.9",
28 | "@types/invariant": "^2.2.35",
29 | "@types/lodash": "^4.14.175",
30 | "@types/react": "^16.14.16",
31 | "@types/react-redux": "^5.0.22",
32 | "@types/react-test-renderer": "^16.9.5",
33 | "@types/redux-mock-store": "^1.0.3",
34 | "@types/redux-thunk": "^2.1.0",
35 | "@typescript-eslint/parser": "^4.32.0",
36 | "enzyme": "^3.11.0",
37 | "i18next": "^19.9.2",
38 | "immutable": "^3.8.2",
39 | "lodash": "^4.17.21",
40 | "prop-types": "^15.7.2",
41 | "react": "^16.14.0",
42 | "react-dom": "^16.14.0",
43 | "react-i18next": "^11.12.0",
44 | "react-redux": "^5.1.2",
45 | "react-test-renderer": "^17.0.2",
46 | "redux": "^3.7.2",
47 | "redux-mock-store": "^1.5.4",
48 | "redux-thunk": "^2.3.0",
49 | "reselect": "^4.0.0"
50 | },
51 | "peerDependencies": {
52 | "immutable": "3",
53 | "lodash": "4",
54 | "react": "15 || ^16.0.0",
55 | "react-dom": "15 || ^16.0.0",
56 | "react-redux": ">= 5",
57 | "redux": ">= 3",
58 | "reselect": ">= 2"
59 | },
60 | "dependencies": {
61 | "classnames": "^2.3.1",
62 | "d3": "^6.7.0",
63 | "invariant": "^2.2.4",
64 | "prop-types": "^15.7.2",
65 | "react-immutable-proptypes": "^2.2.0"
66 | },
67 | "files": [
68 | "/dist",
69 | "/lib",
70 | "/src"
71 | ],
72 | "publishConfig": {
73 | "access": "public"
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/selectors/portSelectors.test.ts:
--------------------------------------------------------------------------------
1 | import { Map } from 'immutable';
2 | import * as Selectors from './portSelectors';
3 | import { defaultState } from '../reducers/flow.reducer';
4 | import { LinkRecord } from '../constants/flowdesigner.model';
5 | import { PORT_SINK, PORT_SOURCE } from '../constants/flowdesigner.constants';
6 | import { Port } from '../api';
7 | import {
8 | State,
9 | Id,
10 | LinkRecord as LinkRecordType,
11 | PortRecord as PortRecordType,
12 | } from '../customTypings/index.d';
13 |
14 | const port1 = Port.create('id1', 'nodeId1', 0, PORT_SINK, 'reactComponentType');
15 | const port2 = Port.create('id2', 'nodeId1', 0, PORT_SOURCE, 'reactComponentType');
16 | const port3 = Port.create('id3', 'nodeId2', 0, PORT_SINK, 'reactComponentType');
17 | const port4 = Port.create('id4', 'nodeId2', 0, PORT_SOURCE, 'reactComponentType');
18 |
19 | const givenState: Partial = defaultState
20 | .set(
21 | 'links',
22 | Map().set(
23 | 'id1',
24 | new LinkRecord({
25 | id: 'id1',
26 | source: 'id1',
27 | target: 'id2',
28 | }),
29 | ),
30 | )
31 | .set(
32 | 'ports',
33 | Map()
34 | .set('id1', port1)
35 | .set('id2', port2)
36 | .set('id3', port3)
37 | .set('id4', port4),
38 | );
39 |
40 | describe('getEmitterPorts', () => {
41 | it('return a map with port id2 && id4', () => {
42 | // given
43 | // when
44 | const result = Selectors.getEmitterPorts(givenState);
45 | // expect
46 | expect(result.has('id2')).toBe(true);
47 | expect(result.has('id4')).toBe(true);
48 | });
49 | });
50 |
51 | describe('getSinkPorts', () => {
52 | it('return a map with port id1 & id3', () => {
53 | // given
54 | // when
55 | const result = Selectors.getSinkPorts(givenState);
56 | // expect
57 | expect(result.has('id1')).toBe(true);
58 | expect(result.has('id3')).toBe(true);
59 | });
60 | });
61 |
62 | describe('getEmitterPortsForNode', () => {
63 | it('return a map with port id2', () => {
64 | // given
65 | // when
66 | const result = Selectors.getEmitterPortsForNode(givenState)('nodeId1');
67 | // expect
68 | expect(result.has('id2')).toBe(true);
69 | });
70 | });
71 |
72 | describe('getSinkPortsForNode', () => {
73 | it('return a map with port id1', () => {
74 | // given
75 | // when
76 | const result = Selectors.getSinkPortsForNode(givenState)('nodeId1');
77 | // expect
78 | expect(result.has('id1')).toBe(true);
79 | });
80 | });
81 |
--------------------------------------------------------------------------------
/src/components/ZoomHandler.component.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { select, zoom as d3ZoomFactory, ZoomBehavior } from 'd3';
3 | import { Transform } from '../customTypings/index.d';
4 |
5 | export function transformToString(transform?: Transform) {
6 | if (transform) {
7 | return `translate(${transform.x}, ${transform.y}) scale(${transform.k})`;
8 | }
9 | return '';
10 | }
11 |
12 | type State = {
13 | transform?: Transform;
14 | transformToApply?: Transform;
15 | };
16 |
17 | type Props = {
18 | children?: any;
19 | setZoom?: (transform: Transform) => void;
20 | transform?: Transform;
21 | transformToApply?: Transform;
22 | };
23 |
24 | class ZoomHandler extends React.Component {
25 | zoom: ZoomBehavior;
26 |
27 | selection: any;
28 |
29 | zoomCatcher: any;
30 |
31 | constructor(props: Props) {
32 | super(props);
33 |
34 | this.zoom = d3ZoomFactory()
35 | .scaleExtent([1 / 4, 2])
36 | .on('zoom', this.onZoom)
37 | .on('end', this.onZoomEnd);
38 | }
39 |
40 | UNSAFE_componentWillMount() {
41 | this.setState({ transform: this.props.transform });
42 | }
43 |
44 | componentDidMount() {
45 | this.selection = select(this.zoomCatcher);
46 | this.selection.call(this.zoom);
47 | }
48 |
49 | UNSAFE_componentWillReceiveProps(nextProps: Props) {
50 | if (nextProps.transformToApply) {
51 | if (nextProps.transformToApply !== this.props.transformToApply) {
52 | this.selection
53 | .transition()
54 | .duration(230)
55 | .call(this.zoom.transform, nextProps.transformToApply);
56 | }
57 | }
58 | }
59 |
60 | onZoomEnd = (event: any) => {
61 | if (this.props.setZoom) this.props.setZoom(event.transform);
62 | };
63 |
64 | onZoom = (event: any) => {
65 | this.setState({ transform: event.transform });
66 | };
67 |
68 | render() {
69 | const { transform } = this.state;
70 | const childrens = React.Children.map(this.props.children, children =>
71 | React.cloneElement(children, {
72 | transformData: transform,
73 | transform: transformToString(transform),
74 | }),
75 | );
76 | return (
77 |
78 | {
80 | this.zoomCatcher = c;
81 | }}
82 | style={{ fill: 'none', pointerEvents: 'all' }}
83 | x="0"
84 | y="0"
85 | width="100%"
86 | height="100%"
87 | />
88 | {childrens}
89 |
90 | );
91 | }
92 | }
93 |
94 | export default ZoomHandler;
95 |
--------------------------------------------------------------------------------
/src/actions/__snapshots__/link.actions.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Check that link action creators generate proper action objects and perform checking addLink 1`] = `
4 | Array [
5 | Object {
6 | "data": Object {},
7 | "graphicalAttributes": Object {
8 | "selected": true,
9 | },
10 | "linkId": "linkId",
11 | "sourceId": "sourceId",
12 | "targetId": "targetId",
13 | "type": "FLOWDESIGNER_LINK_ADD",
14 | },
15 | ]
16 | `;
17 |
18 | exports[`Check that link action creators generate proper action objects and perform checking removeLink 1`] = `
19 | Array [
20 | Object {
21 | "linkId": "id",
22 | "type": "FLOWDESIGNER_LINK_REMOVE",
23 | },
24 | ]
25 | `;
26 |
27 | exports[`Check that link action creators generate proper action objects and perform checking removeLinkData 1`] = `
28 | Array [
29 | Object {
30 | "dataKey": "type",
31 | "linkId": "id",
32 | "type": "FLOWDESIGNER_LINK_REMOVE_DATA",
33 | },
34 | ]
35 | `;
36 |
37 | exports[`Check that link action creators generate proper action objects and perform checking removeLinkGrahicalAttribute 1`] = `
38 | Array [
39 | Object {
40 | "graphicalAttributesKey": "selected",
41 | "linkId": "id",
42 | "type": "FLOWDESIGNER_LINK_REMOVE_GRAPHICAL_ATTRIBUTES",
43 | },
44 | ]
45 | `;
46 |
47 | exports[`Check that link action creators generate proper action objects and perform checking setLinkData 1`] = `
48 | Array [
49 | Object {
50 | "data": Object {
51 | "type": "test",
52 | },
53 | "linkId": "id",
54 | "type": "FLOWDESIGNER_LINK_SET_DATA",
55 | },
56 | ]
57 | `;
58 |
59 | exports[`Check that link action creators generate proper action objects and perform checking setLinkGraphicalAttributes 1`] = `
60 | Array [
61 | Object {
62 | "graphicalAttributes": Object {
63 | "selected": true,
64 | },
65 | "linkId": "id",
66 | "type": "FLOWDESIGNER_LINK_SET_GRAPHICAL_ATTRIBUTES",
67 | },
68 | ]
69 | `;
70 |
71 | exports[`Check that link action creators generate proper action objects and perform checking setLinkSource 1`] = `
72 | Array [
73 | Object {
74 | "linkId": "linkId",
75 | "sourceId": "portId",
76 | "type": "FLOWDESIGNER_LINK_SET_SOURCE",
77 | },
78 | ]
79 | `;
80 |
81 | exports[`Check that link action creators generate proper action objects and perform checking setLinkTarget 1`] = `
82 | Array [
83 | Object {
84 | "linkId": "linkId",
85 | "targetId": "portId",
86 | "type": "FLOWDESIGNER_LINK_SET_TARGET",
87 | },
88 | ]
89 | `;
90 |
--------------------------------------------------------------------------------
/src/actions/port.actions.test.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/no-extraneous-dependencies */
2 | import configureMockStore from 'redux-mock-store';
3 | import { Map } from 'immutable';
4 |
5 | import * as portActions from './port.actions';
6 | import { PORT_SINK } from '../constants/flowdesigner.constants';
7 |
8 | const mockStore = configureMockStore();
9 |
10 | describe('Check that port action creators generate proper action objects and perform checking', () => {
11 | it('addPort', () => {
12 | const store = mockStore({
13 | flowDesigner: {
14 | nodes: Map({ nodeId: { id: 'nodeId', nodeType: 'type' } }),
15 | ports: Map(),
16 | },
17 | });
18 |
19 | store.dispatch(
20 | portActions.addPort('nodeId', 'portId', {
21 | graphicalAttributes: {
22 | portType: 'test',
23 | properties: {
24 | type: PORT_SINK,
25 | },
26 | },
27 | data: {
28 | flowType: 'string',
29 | },
30 | }),
31 | );
32 | expect(store.getActions()).toMatchSnapshot();
33 | });
34 |
35 | it('setPortGraphicalAttribute', () => {
36 | const store = mockStore({
37 | flowDesigner: {
38 | ports: Map({ id: { id: 'portId', portType: 'type' } }),
39 | },
40 | });
41 |
42 | store.dispatch(portActions.setPortGraphicalAttribute('id', { selected: true }));
43 |
44 | expect(store.getActions()).toMatchSnapshot();
45 | });
46 |
47 | it('removePortAttribute', () => {
48 | const store = mockStore({
49 | flowDesigner: {
50 | ports: Map({ id: { id: 'portId' } }),
51 | },
52 | });
53 |
54 | store.dispatch(portActions.removePortGraphicalAttribute('id', 'selected'));
55 |
56 | expect(store.getActions()).toMatchSnapshot();
57 | });
58 |
59 | it('setPortData', () => {
60 | const store = mockStore({
61 | flowDesigner: {
62 | ports: Map({ id: { id: 'portId', portType: 'type' } }),
63 | },
64 | });
65 |
66 | store.dispatch(portActions.setPortdata('id', { type: 'test' }));
67 |
68 | expect(store.getActions()).toMatchSnapshot();
69 | });
70 |
71 | it('removePortData', () => {
72 | const store = mockStore({
73 | flowDesigner: {
74 | ports: Map({ id: { id: 'portId' }, data: Map({ type: 'test' }) }),
75 | },
76 | });
77 |
78 | store.dispatch(portActions.removePortData('id', 'type'));
79 |
80 | expect(store.getActions()).toMatchSnapshot();
81 | });
82 |
83 | it('removePort', () => {
84 | const store = mockStore({
85 | flowDesigner: {
86 | ports: { portId: { id: 'portId' } },
87 | },
88 | });
89 |
90 | store.dispatch(portActions.removePort('portId'));
91 | expect(store.getActions()).toMatchSnapshot();
92 | });
93 | });
94 |
--------------------------------------------------------------------------------
/src/api/data/data.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This module is private and deal with updating a graph object internal Immutable.Map
3 | */
4 | import curry from 'lodash/curry';
5 | import isString from 'lodash/isString';
6 | import { Map } from 'immutable';
7 |
8 | import { throwInDev, throwTypeError } from '../throwInDev';
9 |
10 | /**
11 | * return true if the parameter is an Immutable.Map throw otherwise
12 | * @private
13 | * @param {any} map - the value to be checkd as Immutable.Map
14 | * @return {bool}
15 | */
16 | export function isMapElseThrow(map: Map) {
17 | const test = Map.isMap(map);
18 | if (!test) {
19 | throwTypeError('Immutable.Map', map, 'map');
20 | }
21 | return test;
22 | }
23 |
24 | /**
25 | * return true if the parameter key is a String throw otherwise
26 | * @private
27 | * @param {any} key - the value to be checked as String
28 | * @return {bool}
29 | */
30 | export function isKeyElseThrow(key: string | number) {
31 | const test = isString(key);
32 | if (!test) {
33 | throwInDev(
34 | `key should be a string, was given ${key && key.toString()} of type ${typeof key}`,
35 | );
36 | }
37 | return test;
38 | }
39 |
40 | /**
41 | * given a key and a value, add those to a map
42 | * @function
43 | * @param {string} key
44 | * @param {any} value
45 | * @param {Immutable.Map} map
46 | * @returns {Immutable.Map}
47 | */
48 | export const set = curry((key: any, value: any, map: Map) => {
49 | if (isKeyElseThrow(key) && isMapElseThrow(map)) {
50 | return map.set(key, value);
51 | }
52 | return map;
53 | });
54 |
55 | /**
56 | * given a key and a map return the value associated if exist
57 | * @function
58 | * @param {string} key
59 | * @param {Immutable.Map} map
60 | * @returns {any | null}
61 | */
62 | export const get = curry((key: any, map: Map) => {
63 | if (isKeyElseThrow(key) && isMapElseThrow(map)) {
64 | return map.get(key);
65 | }
66 | return null;
67 | });
68 |
69 | /**
70 | * Given a key and a map check if this key exist on the map
71 | * @function
72 | * @param {string} key
73 | * @param {Immutable.Map} map
74 | * @return {bool}
75 | */
76 | export const has = curry((key: any, map: Map) => {
77 | if (isKeyElseThrow(key) && isMapElseThrow(map)) {
78 | return map.has(key);
79 | }
80 | return false;
81 | });
82 |
83 | /**
84 | * remove given key and its value from the map
85 | * @function
86 | * @param {string} key
87 | * @param {Immutable.Map} map
88 | * @returns {Immutable.Map}
89 | */
90 | export const deleteKey = curry((key: any, map: Map) => {
91 | if (isKeyElseThrow(key) && isMapElseThrow(map)) {
92 | return map.delete(key);
93 | }
94 | return map;
95 | });
96 |
--------------------------------------------------------------------------------
/src/actions/port.actions.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Id,
3 | PortData,
4 | PortGraphicalAttributes,
5 | Port,
6 | PortAction,
7 | PortActionAdd,
8 | } from '../customTypings/index.d';
9 |
10 | /**
11 | * return an action to create a new port
12 | * @param {string} nodeId - identifier of the node to wich the created connector should be attached
13 | * @param {string} id
14 | * @param {Object} attributes
15 | */
16 | export function addPort(
17 | nodeId: Id,
18 | id: Id,
19 | {
20 | data,
21 | graphicalAttributes,
22 | }: { data?: PortData; graphicalAttributes?: PortGraphicalAttributes },
23 | ): PortActionAdd {
24 | return {
25 | type: 'FLOWDESIGNER_PORT_ADD',
26 | nodeId,
27 | id,
28 | data,
29 | graphicalAttributes,
30 | };
31 | }
32 |
33 | /**
34 | * @deprecated
35 | */
36 | export function addPorts(nodeId: Id, ports: Array): PortAction {
37 | return {
38 | type: 'FLOWDESIGNER_PORT_ADDS',
39 | nodeId,
40 | ports,
41 | };
42 | }
43 |
44 | /**
45 | * return an action to set port attributes
46 | * @deprecated
47 | * @param {string} portId
48 | * @param {Object} graphicalAttributes
49 | */
50 | export function setPortGraphicalAttribute(portId: Id, graphicalAttributes: {}): PortAction {
51 | return {
52 | type: 'FLOWDESIGNER_PORT_SET_GRAPHICAL_ATTRIBUTES',
53 | portId,
54 | graphicalAttributes,
55 | };
56 | }
57 |
58 | /**
59 | * Ask to remove an attribute on target port
60 | * @deprecated
61 | * @param {string} portId
62 | * @param {string} graphicalAttributesKey - the key of the attribute to be removed
63 | */
64 | export function removePortGraphicalAttribute(
65 | portId: Id,
66 | graphicalAttributesKey: string,
67 | ): PortAction {
68 | return {
69 | type: 'FLOWDESIGNER_PORT_REMOVE_GRAPHICAL_ATTRIBUTES',
70 | portId,
71 | graphicalAttributesKey,
72 | };
73 | }
74 |
75 | /**
76 | * return an action to set port attributes
77 | * @deprecated
78 | * @param {string} portId
79 | * @param {Object} graphicalAttributes
80 | */
81 | export function setPortdata(portId: Id, data: Object): PortAction {
82 | return {
83 | type: 'FLOWDESIGNER_PORT_SET_DATA',
84 | portId,
85 | data,
86 | };
87 | }
88 |
89 | /**
90 | * Ask to remove an attribute on target port
91 | * @deprecated
92 | * @param {string} portId
93 | * @param {string} datasKey - the key of the attribute to be removed
94 | */
95 | export function removePortData(portId: Id, dataKey: string): PortAction {
96 | return {
97 | type: 'FLOWDESIGNER_PORT_REMOVE_DATA',
98 | portId,
99 | dataKey,
100 | };
101 | }
102 |
103 | /**
104 | * return an action to remove port and all attached links
105 | * @deprecated use deletePort action
106 | * @param {string} portId
107 | */
108 | export function removePort(portId: Id): PortAction {
109 | return {
110 | type: 'FLOWDESIGNER_PORT_REMOVE',
111 | portId,
112 | };
113 | }
114 |
--------------------------------------------------------------------------------
/src/components/node/AbstractNode.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow, mount } from 'enzyme';
3 |
4 | import {
5 | NodeGraphicalAttributes,
6 | NodeRecord,
7 | PositionRecord,
8 | SizeRecord,
9 | } from '../../constants/flowdesigner.model';
10 | import AbstractNode, { ABSTRACT_NODE_INVARIANT } from './AbstractNode.component';
11 |
12 | const node = new NodeRecord({
13 | id: 'id',
14 | graphicalAttributes: new NodeGraphicalAttributes({
15 | position: new PositionRecord({ x: 100, y: 50 }),
16 | nodeSize: new SizeRecord({ width: 125, height: 75 }),
17 | }),
18 | });
19 |
20 | function noOp() {}
21 |
22 | describe('Testing ', () => {
23 | it('should create a bare node component with provided position', () => {
24 | const wrapper = mount(
25 |
26 |
27 | ,
28 | );
29 | const rect = wrapper.find('g[transform]');
30 | expect(rect.prop('transform')).toBe('translate(100, 50)');
31 | });
32 |
33 | it('call the injected onClick action when clicked', () => {
34 | const onClick = jest.fn();
35 | const wrapper = mount(
36 |
43 |
44 | ,
45 | );
46 | wrapper.find('g[transform]').simulate('click');
47 | expect(onClick).toHaveBeenCalledTimes(1);
48 | });
49 |
50 | // if anyone got a clue on how to test react + d3 events
51 |
52 | xit('call the injected onDragStart action when drag action start', done => {
53 | const evt = document.createEvent('HTMLEvents');
54 | evt.initEvent('click', false, true);
55 | const onDragStart = jest.fn();
56 | mount(
57 |
64 |
65 | ,
66 | { attachTo: document.body },
67 | );
68 |
69 | const element = document.querySelector('g g') || new HTMLDivElement();
70 |
71 | element.addEventListener('click', () => {
72 | done();
73 | });
74 | element.dispatchEvent(new window.MouseEvent('click'));
75 | expect(onDragStart.mock.calls.length).toEqual(1);
76 | fail();
77 | });
78 |
79 | xit('call the injected onDrag action when drag action start', () => {
80 | fail();
81 | });
82 |
83 | xit('call the injected onDragEnd action when drag action start', () => {
84 | fail();
85 | });
86 |
87 | it('should fire an error if its rendered without a children set up', () => {
88 | expect(() => {
89 | shallow(
90 | ,
96 | );
97 | }).toThrowError(ABSTRACT_NODE_INVARIANT);
98 | });
99 | });
100 |
--------------------------------------------------------------------------------
/src/components/link/AbstractLink.component.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { line, curveBasis, interpolateBasis } from 'd3';
4 |
5 | import LinkHandle from './LinkHandle.component';
6 | import { Position, PortRecord } from '../../customTypings/index.d';
7 |
8 | const concreteLine = line()
9 | .x((d: any) => d.x)
10 | .y((d: any) => d.y)
11 | .curve(curveBasis);
12 |
13 | function calculatePath(sourcePosition: Position, targetPosition: Position) {
14 | const pathCoords: Position[] = [];
15 | pathCoords[0] = targetPosition;
16 | pathCoords[1] = sourcePosition;
17 | const xInterpolate = interpolateBasis([targetPosition.x, pathCoords[1].x]);
18 | const yInterpolate = interpolateBasis([targetPosition.y, pathCoords[1].y]);
19 | const path = concreteLine(pathCoords as any);
20 | return { path, xInterpolate, yInterpolate };
21 | }
22 |
23 | type Props = {
24 | source: PortRecord;
25 | target: PortRecord;
26 | targetHandlePosition: Position;
27 | calculatePath: (
28 | sourcePosition: Position,
29 | targetPosition: Position,
30 | ) => { path: any; xInterpolate: number; yInterpolate: number };
31 | onSourceDrag?: (event: any) => void;
32 | onSourceDragEnd?: (event: any) => void;
33 | onTargetDrag?: (event: any) => void;
34 | onTargetDragEnd?: (event: any) => void;
35 | children?: any;
36 | linkSourceHandleComponent?: React.ReactElement;
37 | sourceHandlePosition?: Position;
38 | linkTargetHandleComponent?: React.ReactElement;
39 | };
40 |
41 | class AbstractLink extends React.PureComponent {
42 | static calculatePath = calculatePath;
43 |
44 | renderLinkSourceHandle() {
45 | if (this.props.linkSourceHandleComponent) {
46 | return (
47 |
53 | );
54 | }
55 | return null;
56 | }
57 |
58 | renderLinkTargetHandle() {
59 | if (this.props.linkTargetHandleComponent) {
60 | return (
61 |
67 | );
68 | }
69 | return null;
70 | }
71 |
72 | render() {
73 | const pathCalculationMethod = this.props.calculatePath || AbstractLink.calculatePath;
74 | const { path, xInterpolate, yInterpolate } = pathCalculationMethod(
75 | this.props.source.getPosition(),
76 | this.props.targetHandlePosition || this.props.target.getPosition(),
77 | );
78 | const newChildren = React.Children.map(this.props.children, child =>
79 | React.cloneElement(child, { d: path, xInterpolate, yInterpolate }),
80 | );
81 | return (
82 |
83 | {newChildren}
84 | {this.renderLinkSourceHandle()}
85 | {this.renderLinkTargetHandle()}
86 |
87 | );
88 | }
89 | }
90 |
91 | export default AbstractLink;
92 |
--------------------------------------------------------------------------------
/src/constants/flowdesigner.constants.ts:
--------------------------------------------------------------------------------
1 | export const FLOWDESIGNER_FLOW_LOAD = 'FLOWDESIGNER.FLOW_LOAD';
2 | export const FLOWDESIGNER_FLOW_RESET = 'FLOWDESIGNER.FLOW_RESET';
3 | export const FLOWDESIGNER_FLOW_ADD_ELEMENTS = 'FLOWDESIGNER.FLOW_ADD_ELEMENTS';
4 | export const FLOWDESIGNER_FLOW_SET_ZOOM = 'FLOWDESIGNER.FLOW_SET_ZOOM';
5 | export const FLOWDESIGNER_FLOW_ZOOM_IN = 'FLOWDESIGNER.FLOW_ZOOM_IN';
6 | export const FLOWDESIGNER_FLOW_ZOOM_OUT = 'FLOWDESIGNER.FLOW_ZOOM_OUT';
7 | export const FLOWDESIGNER_PAN_TO = 'FLOWDESIGNER_PAN_TO';
8 |
9 | export const FLOWDESIGNER_NODE_ADD = 'FLOWDESIGNER_NODE_ADD';
10 | export const FLOWDESIGNER_NODE_MOVE_START = 'FLOWDESIGNER_NODE_MOVE_START';
11 | export const FLOWDESIGNER_NODE_MOVE = 'FLOWDESIGNER_NODE_MOVE';
12 | export const FLOWDESIGNER_NODE_APPLY_MOVEMENT = 'FLOWDESIGNER_NODE_APPLY_MOVEMENT';
13 | export const FLOWDESIGNER_NODE_MOVE_END = 'FLOWDESIGNER_NODE_MOVE_END';
14 | export const FLOWDESIGNER_NODE_SET_SIZE = 'FLOWDESIGNER_NODE_SET_SIZE';
15 | export const FLOWDESIGNER_NODE_SET_TYPE = 'FLOWDESIGNER_NODE_SET_TYPE';
16 | export const FLOWDESIGNER_NODE_SET_GRAPHICAL_ATTRIBUTES =
17 | 'FLOWDESIGNER_NODE_SET_GRAPHICAL_ATTRIBUTES';
18 | export const FLOWDESIGNER_NODE_REMOVE_GRAPHICAL_ATTRIBUTES =
19 | 'FLOWDESIGNER_NODE_REMOVE_GRAPHICAL_ATTRIBUTES';
20 | export const FLOWDESIGNER_NODE_SET_DATA = 'FLOWDESIGNER_NODE_SET_DATA';
21 | export const FLOWDESIGNER_NODE_REMOVE_DATA = 'FLOWDESIGNER_NODE_REMOVE_DATA';
22 | export const FLOWDESIGNER_NODE_REMOVE = 'FLOWDESIGNER_NODE_REMOVE';
23 | export const FLOWDESIGNER_NODE_UPDATE = 'FLOWDESIGNER_NODE_UPDATE';
24 |
25 | export const FLOWDESIGNER_NODETYPE_SET = 'FLOWDESIGNER_NODETYPE_SET';
26 |
27 | export const FLOWDESIGNER_PORT_ADD = 'FLOWDESIGNER_PORT_ADD';
28 | export const FLOWDESIGNER_PORT_ADDS = 'FLOWDESIGNER_PORT_ADDS';
29 | export const FLOWDESIGNER_PORT_SET_GRAPHICAL_ATTRIBUTES =
30 | 'FLOWDESIGNER_PORT_SET_GRAPHICAL_ATTRIBUTES';
31 | export const FLOWDESIGNER_PORT_REMOVE_GRAPHICAL_ATTRIBUTES =
32 | 'FLOWDESIGNER_PORT_REMOVE_GRAPHICAL_ATTRIBUTES';
33 | export const FLOWDESIGNER_PORT_SET_DATA = 'FLOWDESIGNER_PORT_SET_DATA';
34 | export const FLOWDESIGNER_PORT_REMOVE_DATA = 'FLOWDESIGNER_PORT_REMOVE_DATA';
35 | export const FLOWDESIGNER_PORT_REMOVE = 'FLOWDESIGNER_PORT_REMOVE';
36 |
37 | export const FLOWDESIGNER_LINK_ADD = 'FLOWDESIGNER_LINK_ADD';
38 | export const FLOWDESIGNER_LINK_SET_TARGET = 'FLOWDESIGNER_LINK_SET_TARGET';
39 | export const FLOWDESIGNER_LINK_SET_SOURCE = 'FLOWDESIGNER_LINK_SET_SOURCE';
40 | export const FLOWDESIGNER_LINK_REMOVE = 'FLOWDESIGNER_LINK_REMOVE';
41 | export const FLOWDESIGNER_LINK_SET_GRAPHICAL_ATTRIBUTES =
42 | 'FLOWDESIGNER_LINK_SET_GRAPHICAL_ATTRIBUTES';
43 | export const FLOWDESIGNER_LINK_REMOVE_GRAPHICAL_ATTRIBUTES =
44 | 'FLOWDESIGNER_LINK_REMOVE_GRAPHICAL_ATTRIBUTES';
45 | export const FLOWDESIGNER_LINK_SET_DATA = 'FLOWDESIGNER_LINK_SET_DATA';
46 | export const FLOWDESIGNER_LINK_REMOVE_DATA = 'FLOWDESIGNER_LINK_REMOVE_DATA';
47 |
48 | export const PORT_SOURCE = 'OUTGOING';
49 | export const PORT_SINK = 'INCOMING';
50 |
51 | export const GRID_SIZE = 40;
52 |
--------------------------------------------------------------------------------
/src/actions/link.actions.ts:
--------------------------------------------------------------------------------
1 | import {
2 | FLOWDESIGNER_LINK_ADD,
3 | FLOWDESIGNER_LINK_SET_TARGET,
4 | FLOWDESIGNER_LINK_SET_SOURCE,
5 | FLOWDESIGNER_LINK_REMOVE,
6 | FLOWDESIGNER_LINK_SET_GRAPHICAL_ATTRIBUTES,
7 | FLOWDESIGNER_LINK_REMOVE_GRAPHICAL_ATTRIBUTES,
8 | FLOWDESIGNER_LINK_SET_DATA,
9 | FLOWDESIGNER_LINK_REMOVE_DATA,
10 | } from '../constants/flowdesigner.constants';
11 |
12 | /**
13 | * Ask for link creation
14 | * @param {string} linkId
15 | * @param {string} sourceId - the source port Identifier
16 | * @param {string} targetId - the target port Identifier
17 | * @param {Object} attr
18 | */
19 | export const addLink = (
20 | linkId: string,
21 | sourceId: string,
22 | targetId: string,
23 | { data = {}, graphicalAttributes = {} }: any = {},
24 | ) => ({
25 | type: FLOWDESIGNER_LINK_ADD,
26 | linkId,
27 | sourceId,
28 | targetId,
29 | data,
30 | graphicalAttributes,
31 | });
32 |
33 | /**
34 | * Ask for change of link target
35 | * @deprecated
36 | * @param {string} linkId
37 | * @param {string} targetId - the target port identifier
38 | */
39 | export const setLinkTarget = (linkId: string, targetId: string) => ({
40 | type: FLOWDESIGNER_LINK_SET_TARGET,
41 | linkId,
42 | targetId,
43 | });
44 |
45 | /**
46 | * Ask for change of link source
47 | * @deprecated
48 | * @param {string} linkId
49 | * @param {string} sourceId - the source port identifier
50 | */
51 | export const setLinkSource = (linkId: string, sourceId: string) => ({
52 | type: FLOWDESIGNER_LINK_SET_SOURCE,
53 | linkId,
54 | sourceId,
55 | });
56 |
57 | /**
58 | * Ask to set graphical attributes on link
59 | * @deprecated
60 | * @param {string} linkId
61 | * @param {Object} attr
62 | */
63 | export const setLinkGraphicalAttributes = (linkId: string, graphicalAttributes: any) => ({
64 | type: FLOWDESIGNER_LINK_SET_GRAPHICAL_ATTRIBUTES,
65 | linkId,
66 | graphicalAttributes,
67 | });
68 |
69 | /**
70 | * Ask to remove an graphical attribute on target link
71 | * @deprecated
72 | * @param {string} linkId
73 | * @param {string} attrKey - the key of the attribute to be removed
74 | */
75 | export const removeLinkGraphicalAttribute = (linkId: string, graphicalAttributesKey: any) => ({
76 | type: FLOWDESIGNER_LINK_REMOVE_GRAPHICAL_ATTRIBUTES,
77 | linkId,
78 | graphicalAttributesKey,
79 | });
80 |
81 | /**
82 | * Ask to set data on link
83 | * @deprecated
84 | * @param {string} linkId
85 | * @param {Object} attr
86 | */
87 | export const setLinkData = (linkId: string, data: any) => ({
88 | type: FLOWDESIGNER_LINK_SET_DATA,
89 | linkId,
90 | data,
91 | });
92 |
93 | /**
94 | * Ask to remove a data on target link
95 | * @deprecated
96 | * @param {string} linkId
97 | * @param {string} attrKey - the key of the attribute to be removed
98 | */
99 | export const removeLinkData = (linkId: string, dataKey: any) => ({
100 | type: FLOWDESIGNER_LINK_REMOVE_DATA,
101 | linkId,
102 | dataKey,
103 | });
104 |
105 | /**
106 | * Ask for link removal
107 | * @deprecated use deleteLink action
108 | * @param {string} linkId
109 | */
110 | export const removeLink = (linkId: string) => ({
111 | type: FLOWDESIGNER_LINK_REMOVE,
112 | linkId,
113 | });
114 |
--------------------------------------------------------------------------------
/src/actions/__snapshots__/node.actions.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Check that node action creators generate proper action objects and perform checking addNode generate action with 0 configuration 1`] = `
4 | Array [
5 | Object {
6 | "data": Object {},
7 | "graphicalAttributes": Object {
8 | "nodePosition": Object {
9 | "x": 75,
10 | "y": 75,
11 | },
12 | "nodeSize": Object {
13 | "height": 50,
14 | "width": 50,
15 | },
16 | "nodeType": "nodeType",
17 | },
18 | "nodeId": "id",
19 | "nodeType": "type",
20 | "type": "FLOWDESIGNER_NODE_ADD",
21 | },
22 | ]
23 | `;
24 |
25 | exports[`Check that node action creators generate proper action objects and perform checking moveNode generate a proper action object witch nodeId and nodePosition parameter 1`] = `
26 | Array [
27 | Object {
28 | "nodeId": "nodeId",
29 | "nodePosition": Object {
30 | "x": 10,
31 | "y": 20,
32 | },
33 | "type": "FLOWDESIGNER_NODE_MOVE",
34 | },
35 | ]
36 | `;
37 |
38 | exports[`Check that node action creators generate proper action objects and perform checking removeNode 1`] = `
39 | Array [
40 | Object {
41 | "nodeId": "id",
42 | "type": "FLOWDESIGNER_NODE_REMOVE",
43 | },
44 | ]
45 | `;
46 |
47 | exports[`Check that node action creators generate proper action objects and perform checking removeNodeData 1`] = `
48 | Array [
49 | Object {
50 | "dataKey": "type",
51 | "nodeId": "id",
52 | "type": "FLOWDESIGNER_NODE_REMOVE_DATA",
53 | },
54 | ]
55 | `;
56 |
57 | exports[`Check that node action creators generate proper action objects and perform checking removeNodeGraphicalAttribute 1`] = `
58 | Array [
59 | Object {
60 | "graphicalAttributesKey": "selected",
61 | "nodeId": "id",
62 | "type": "FLOWDESIGNER_NODE_REMOVE_GRAPHICAL_ATTRIBUTES",
63 | },
64 | ]
65 | `;
66 |
67 | exports[`Check that node action creators generate proper action objects and perform checking setNodeData 1`] = `
68 | Array [
69 | Object {
70 | "bySubmit": false,
71 | "data": Object {
72 | "type": "test",
73 | },
74 | "nodeId": "id",
75 | "type": "FLOWDESIGNER_NODE_SET_DATA",
76 | },
77 | ]
78 | `;
79 |
80 | exports[`Check that node action creators generate proper action objects and perform checking setNodeGraphicalAttributes 1`] = `
81 | Array [
82 | Object {
83 | "graphicalAttributes": Object {
84 | "selected": true,
85 | },
86 | "nodeId": "id",
87 | "type": "FLOWDESIGNER_NODE_SET_GRAPHICAL_ATTRIBUTES",
88 | },
89 | ]
90 | `;
91 |
92 | exports[`Check that node action creators generate proper action objects and perform checking setNodeSize 1`] = `
93 | Array [
94 | Object {
95 | "nodeId": "nodeId",
96 | "nodeSize": Object {
97 | "height": 100,
98 | "width": 100,
99 | },
100 | "type": "FLOWDESIGNER_NODE_SET_SIZE",
101 | },
102 | ]
103 | `;
104 |
105 | exports[`applyMovementTo generate proper action 1`] = `
106 | Object {
107 | "movement": Object {
108 | "x": 10,
109 | "y": 5,
110 | },
111 | "nodesId": Array [
112 | 1,
113 | 2,
114 | 3,
115 | ],
116 | "type": "FLOWDESIGNER_NODE_APPLY_MOVEMENT",
117 | }
118 | `;
119 |
--------------------------------------------------------------------------------
/src/api/size/size.ts:
--------------------------------------------------------------------------------
1 | import curry from 'lodash/curry';
2 | import flow from 'lodash/flow';
3 |
4 | import { throwInDev, throwTypeError } from '../throwInDev';
5 | import { SizeRecord } from '../../constants/flowdesigner.model';
6 | import { SizeRecord as SizeRecordType } from '../../customTypings/index.d';
7 |
8 | /**
9 | * @desc Represent a size comprised of width and height
10 | * avoid reading directly, use the Size module api
11 | * Do not mutate it manually, use the Size module api
12 | * @example Create a Size
13 | * const size = Size.create(100, 200);
14 | * @example Read from Size
15 | * const width = Size.getWidth(size);
16 | * @example transform a Size
17 | * const size = Size.setWidth(100, size);
18 | * @typedef {Immutable.Record} SizeRecord
19 | */
20 |
21 | /**
22 | * check if parameter is SizeRecord
23 | * @param {*} size
24 | * @return {bool}
25 | */
26 | export function isSize(size: SizeRecordType) {
27 | if (size && size instanceof SizeRecord) {
28 | return true;
29 | }
30 | return false;
31 | }
32 |
33 | /**
34 | * check if parameter is SizeRecord else throw in dev mode
35 | * @param {*} size
36 | * @return {bool}
37 | */
38 | export function isSizeElseThrow(size: SizeRecordType) {
39 | const test = isSize(size);
40 | if (!test) {
41 | throwTypeError('SizeRecord', size, 'Size');
42 | }
43 | return test;
44 | }
45 |
46 | /**
47 | * return width of SizeRecord
48 | * @param {SizeRecord} size
49 | * @return {number}
50 | */
51 | export function getWidth(size: SizeRecordType) {
52 | if (isSizeElseThrow(size)) {
53 | return size.get('width');
54 | }
55 | return null;
56 | }
57 |
58 | /**
59 | * set width of given SizeRecord
60 | * @function
61 | * @param {number} width
62 | * @param {SizeRecord} size
63 | * @return {SizeRecord}
64 | */
65 | export const setWidth = curry((width: number, size: SizeRecordType) => {
66 | if (isSizeElseThrow(size) && typeof width === 'number') {
67 | return size.set('width', width);
68 | }
69 | throwInDev(`width should be a number, was given ${width.toString()} of type ${typeof width}`);
70 | return size;
71 | });
72 |
73 | /**
74 | * return height of the SizeRecord
75 | * @param {SizeRecord} size
76 | * @return {number}
77 | */
78 | export function getHeight(size: SizeRecordType) {
79 | if (isSizeElseThrow(size)) {
80 | return size.get('height');
81 | }
82 | return null;
83 | }
84 |
85 | /**
86 | * set height of given SizeRecord
87 | * @function
88 | * @param {number} height
89 | * @param {SizeRecord} size
90 | * @returns {SizeRecord}
91 | */
92 | export const setHeight = curry((height: number, size: SizeRecordType) => {
93 | if (isSizeElseThrow(size) && typeof height === 'number') {
94 | return size.set('height', height);
95 | }
96 | throwInDev(
97 | `height should be a number, was given ${height.toString()} of type ${typeof height}`,
98 | );
99 | return size;
100 | });
101 |
102 | /**
103 | * given width and height create a SizeRecord
104 | * @function
105 | * @param {number} width
106 | * @param {number} height
107 | * @return {SizeRecord}
108 | */
109 | export const create = curry((width: number, height: number) =>
110 | flow([setWidth(width), setHeight(height)])(new SizeRecord()),
111 | );
112 |
--------------------------------------------------------------------------------
/src/actions/link.actions.test.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/no-extraneous-dependencies */
2 | import configureMockStore from 'redux-mock-store';
3 | import thunk from 'redux-thunk';
4 | import { Map } from 'immutable';
5 |
6 | import * as linkActions from './link.actions';
7 |
8 | const middlewares = [thunk];
9 | const mockStore = configureMockStore(middlewares);
10 |
11 | describe('Check that link action creators generate proper action objects and perform checking', () => {
12 | it('addLink', () => {
13 | const store = mockStore({
14 | flowDesigner: {
15 | links: Map(),
16 | ports: Map({
17 | id1: { id: 'portId', portType: 'type' },
18 | id2: { id: 'portId', portType: 'type' },
19 | }),
20 | },
21 | });
22 |
23 | store.dispatch(
24 | linkActions.addLink('linkId', 'sourceId', 'targetId', {
25 | graphicalAttributes: { selected: true },
26 | }),
27 | );
28 | expect(store.getActions()).toMatchSnapshot();
29 | });
30 |
31 | it('setLinkTarget', () => {
32 | const store = mockStore({
33 | flowDesigner: {
34 | links: Map({ linkId: { id: 'linkId' } }),
35 | ports: Map({ id1: { id: 'portId', portType: 'type' } }),
36 | },
37 | });
38 |
39 | store.dispatch(linkActions.setLinkTarget('linkId', 'portId'));
40 |
41 | expect(store.getActions()).toMatchSnapshot();
42 | });
43 |
44 | it('setLinkSource', () => {
45 | const store = mockStore({
46 | flowDesigner: {
47 | links: Map({ linkId: { id: 'linkId' } }),
48 | ports: Map({ id1: { id: 'portId', portType: 'type' } }),
49 | },
50 | });
51 |
52 | store.dispatch(linkActions.setLinkSource('linkId', 'portId'));
53 |
54 | expect(store.getActions()).toMatchSnapshot();
55 | });
56 |
57 | it('setLinkGraphicalAttributes', () => {
58 | const store = mockStore({
59 | flowDesigner: {
60 | links: Map({ id: { id: 'linkId', linkType: 'type' } }),
61 | },
62 | });
63 |
64 | store.dispatch(linkActions.setLinkGraphicalAttributes('id', { selected: true }));
65 |
66 | expect(store.getActions()).toMatchSnapshot();
67 | });
68 |
69 | it('removeLinkGrahicalAttribute', () => {
70 | const store = mockStore({
71 | flowDesigner: {
72 | links: Map({ id: { id: 'linkId', linkType: 'type' } }),
73 | },
74 | });
75 |
76 | store.dispatch(linkActions.removeLinkGraphicalAttribute('id', 'selected'));
77 |
78 | expect(store.getActions()).toMatchSnapshot();
79 | });
80 |
81 | it('setLinkData', () => {
82 | const store = mockStore({
83 | flowDesigner: {
84 | links: Map({ id: { id: 'linkId', linkType: 'type' } }),
85 | },
86 | });
87 |
88 | store.dispatch(linkActions.setLinkData('id', { type: 'test' }));
89 |
90 | expect(store.getActions()).toMatchSnapshot();
91 | });
92 |
93 | it('removeLinkData', () => {
94 | const store = mockStore({
95 | flowDesigner: {
96 | links: Map({ id: { id: 'linkId', linkType: 'type' } }),
97 | },
98 | });
99 |
100 | store.dispatch(linkActions.removeLinkData('id', 'type'));
101 |
102 | expect(store.getActions()).toMatchSnapshot();
103 | });
104 |
105 | it('removeLink', () => {
106 | const store = mockStore({
107 | flowDesigner: {
108 | links: Map({ id: { id: 'linkId' } }),
109 | },
110 | });
111 |
112 | store.dispatch(linkActions.removeLink('id'));
113 | expect(store.getActions()).toMatchSnapshot();
114 | });
115 | });
116 |
--------------------------------------------------------------------------------
/src/api/position/position.ts:
--------------------------------------------------------------------------------
1 | import curry from 'lodash/curry';
2 | import flow from 'lodash/flow';
3 | import isNumber from 'lodash/isNumber';
4 |
5 | import { throwInDev, throwTypeError } from '../throwInDev';
6 | import { PositionRecord } from '../../constants/flowdesigner.model';
7 | import { PositionRecord as PositionRecordType } from '../../customTypings/index.d';
8 |
9 | /**
10 | * @desc Represent a position comprised of X and Y coordinates
11 | * avoid reading directly, use the Size module api
12 | * Do not mutate it manually, use the Size module api
13 | * @example Create a Position
14 | * const position = Position.create(100, 200);
15 | * @example Read from Position
16 | * const x = Size.getXCoordinate(position);
17 | * @example transform a Position
18 | * const position = Size.setXCoordinate(100, position);
19 | * @typedef {Immutable.Record} PositionRecord
20 | */
21 |
22 | /**
23 | * given a parameter check if it is a PositionRecord
24 | * @param {*} position
25 | * @return {bool}
26 | */
27 | export function isPosition(position: PositionRecordType) {
28 | if (position && position instanceof PositionRecord) {
29 | return true;
30 | }
31 | return false;
32 | }
33 |
34 | /**
35 | * given a parameter check if it is a PositionRecord, if not throw in developpement
36 | * @param {*} position
37 | * @return {bool}
38 | */
39 | export function isPositionElseThrow(position: PositionRecordType) {
40 | const test = isPosition(position);
41 | if (!test) {
42 | throwTypeError('PositionRecord', position, 'Position');
43 | }
44 | return test;
45 | }
46 |
47 | /**
48 | * given a PositionRecord return X coordinate
49 | * @param {PositionRecord} position
50 | * @return {number}
51 | */
52 | export function getXCoordinate(position: PositionRecordType) {
53 | if (isPositionElseThrow(position)) {
54 | return position.get('x');
55 | }
56 | return null;
57 | }
58 |
59 | /**
60 | * given a number and a PositionRecord return updated PositionRecord
61 | * @function
62 | * @param {number} x
63 | * @param {PositionRecord} position
64 | * @return {PositionRecord}
65 | */
66 | export const setXCoordinate = curry((x: number, position: PositionRecordType) => {
67 | if (isPositionElseThrow(position) && isNumber(x)) {
68 | return position.set('x', x);
69 | }
70 | throwInDev(`x should be a number, was given ${x && x.toString()} of type ${typeof x}`);
71 | return position;
72 | });
73 |
74 | /**
75 | * given a PositionRecord return the Y coordinate
76 | * @param {PositionRecord} position
77 | * @return {number}
78 | */
79 | export function getYCoordinate(position: PositionRecordType) {
80 | if (isPositionElseThrow(position)) {
81 | return position.get('y');
82 | }
83 | return null;
84 | }
85 |
86 | /**
87 | * given a number and a PositionRecord return updated PositionRecord
88 | * @param {number} y
89 | * @param {PositionRecord} position
90 | * @return {PositionRecord}
91 | */
92 | export const setYCoordinate = curry((y: number, position: PositionRecordType) => {
93 | if (isPositionElseThrow(position) && isNumber(y)) {
94 | return position.set('y', y);
95 | }
96 | throwInDev(`y should be a number, was given ${y && y.toString()} of type ${typeof y}`);
97 | return position;
98 | });
99 |
100 | /**
101 | * given x and y coordinate return a PositionRecord
102 | * @param {number} x
103 | * @param {number} y
104 | * @return {PositionRecord}
105 | */
106 | export const create = curry((x: number, y: number) =>
107 | flow([setXCoordinate(x), setYCoordinate(y)])(new PositionRecord()),
108 | );
109 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # The code has been moved to https://github.com/Talend/ui
2 |
3 |
4 | [](https://www.codacy.com/app/Talend/react-flow-designer_2?utm_source=github.com&utm_medium=referral&utm_content=Talend/react-flow-designer&utm_campaign=badger)
5 | [](https://travis-ci.org/Talend/react-flow-designer.svg?branch=master)
6 |
7 | [](https://david-dm.org/acateland/react-flow-designer)
8 |
9 | [](https://coveralls.io/github/acateland/react-flow-designer)
10 |
11 | # Datastream Designer
12 |
13 | Use D3 for calculations.
14 | Redux is used as default state manager, still underlying pure component has been exposed to be used in a non-opiniated maner.
15 |
16 | ## Designed inside dataflow webapp but meant to be used as a module.
17 |
18 | ### How to use it
19 |
20 | #### Use the rendering component
21 |
22 | ```js
23 | import React from 'react';
24 | import { render } from 'react-dom';
25 | import { Provider } from 'react-redux';
26 | import configureStore from './store/configureStore';
27 |
28 | import { DatastreamDesigner } from './datastream_designer/';
29 |
30 | const store = configureStore();
31 |
32 | render(
33 |
34 |
35 | ,
36 | document.getElementById('app'),
37 | );
38 | ```
39 |
40 | #### integrate the reducer into your redux data store
41 |
42 | ```js
43 | import { combineReducers } from 'redux';
44 | import { routerReducer } from 'react-router-redux';
45 |
46 | import { datastreamDesignerReducer } from '../datastream_designer/';
47 |
48 | const rootReducer = combineReducers({
49 | routing: routerReducer,
50 | datastream: datastreamDesignerReducer,
51 | });
52 |
53 | export default rootReducer;
54 | ```
55 |
56 | the datastream_designer module expose its components, reducers, and action type constants.
57 |
58 | Action type constants are exposed for the sake of listening to them and add new feature to your application arround the datastream designer.
59 |
60 | Exemple a reducer listening for 'DATASTREAM_DESIGNER_NODE_SELECTED' could trigger a form so you can edit the node data.
61 |
62 | ## Redux API
63 |
64 | the idea is to reduce the surface api of the redux action, encouraging batching multiple transformation in a transaction
65 |
66 | ### Graph
67 |
68 | - Graph
69 | - transaction [List>]
70 | - Node
71 | - add NodeRecord
72 | - update NodeRecord
73 | - delete NodeRecord
74 | - moveStart nodeId Position
75 | - move nodeId Vector
76 | - moveEnd nodeId Position
77 | - Link
78 | - add LinkRecord
79 | - update LinkRecord
80 | - delete LinkRecord
81 | - Port
82 | - add PortRecord
83 | - update PortRecord
84 | - delete PortRecord
85 |
86 | each of those action are intended to be used with the apply function
87 |
88 | Each of those action are backed by the graph API wich check graph integrity, if one action fail to apply the whole transaction is void and the original graph is returned, one or many errors are logged.
89 |
90 | special action for movement are kept for optimisation purpose, nothing prevent the user to update position via the `update` action
91 |
92 | #### deprecate
93 |
94 | removeNode
95 | removeNodeData
96 | setNodeData
97 | removeNodeGraphicalAttribute
98 | setNodeGraphicalAttributes
99 | setNodeType
100 | setNodeSize
101 | moveNodeToEnd
102 | applyMovementTo
103 | moveNodeTo
104 | startMoveNodeTo
105 |
106 | ## Element API
107 |
108 | ### Node
109 |
110 | ### Link
111 |
112 | ### Port
113 |
114 | ### Graph
115 |
116 | ### Data
117 |
118 | ### Size
119 |
120 | ### Position
121 |
122 | ## Versioning and publication
123 |
124 | The package is **automatically** published on npm as soon as a **pull request is merged on the master with a different version**.
125 |
126 | DISCLAIMER: **We don't control the version**, you have to ensure you don't have an old version compared to the master, and you need to follow [semantic versioning](https://semver.org/) to upgrade the version when needed.
127 |
--------------------------------------------------------------------------------
/src/actions/node.actions.test.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/no-extraneous-dependencies */
2 | import configureMockStore from 'redux-mock-store';
3 | import thunk from 'redux-thunk';
4 | import { Map, OrderedMap } from 'immutable';
5 |
6 | import * as nodeActions from './node.actions';
7 | import { FLOWDESIGNER_NODE_SET_TYPE } from '../constants/flowdesigner.constants';
8 |
9 | const middlewares = [thunk];
10 | const mockStore = configureMockStore(middlewares);
11 |
12 | describe('Check that node action creators generate proper action objects and perform checking', () => {
13 | it('addNode generate action with 0 configuration', () => {
14 | const store = mockStore({
15 | flowDesigner: {
16 | nodes: Map({}),
17 | },
18 | });
19 |
20 | store.dispatch(
21 | nodeActions.addNode('id', 'type', {
22 | data: {},
23 | graphicalAttributes: {
24 | nodePosition: { x: 75, y: 75 },
25 | nodeSize: { width: 50, height: 50 },
26 | nodeType: 'nodeType',
27 | },
28 | }),
29 | );
30 |
31 | expect(store.getActions()).toMatchSnapshot();
32 | });
33 |
34 | it('moveNode generate a proper action object witch nodeId and nodePosition parameter', () => {
35 | const store = mockStore({
36 | flowDesigner: {
37 | nodes: Map({ nodeId: { id: 'nodeId', type: 'type' } }),
38 | nodeTypes: Map({
39 | type: Map({
40 | component: { calculatePortPosition: () => ({}) },
41 | }),
42 | }),
43 | // eslint-disable-next-line new-cap
44 | ports: OrderedMap(),
45 | },
46 | });
47 |
48 | store.dispatch(nodeActions.moveNodeTo('nodeId', { x: 10, y: 20 }));
49 |
50 | expect(store.getActions()).toMatchSnapshot();
51 | });
52 |
53 | it('setNodeSize', () => {
54 | const store = mockStore({
55 | flowDesigner: {
56 | nodes: Map({ nodeId: { id: 'nodeId', type: 'type' } }),
57 | },
58 | });
59 |
60 | store.dispatch(nodeActions.setNodeSize('nodeId', { width: 100, height: 100 }));
61 | expect(store.getActions()).toMatchSnapshot();
62 | });
63 |
64 | it('setNodeType', () => {
65 | const nodeId = 'nodeId';
66 | const nodeType = 'newNodeType';
67 | const store = mockStore({
68 | flowDesigner: {
69 | nodes: Map({ nodeId: { id: nodeId, type: 'type' } }),
70 | },
71 | });
72 |
73 | store.dispatch(nodeActions.setNodeType(nodeId, nodeType));
74 | expect(store.getActions()[0]).toEqual({
75 | type: FLOWDESIGNER_NODE_SET_TYPE,
76 | nodeId,
77 | nodeType,
78 | });
79 | });
80 |
81 | it('setNodeGraphicalAttributes', () => {
82 | const store = mockStore({
83 | flowDesigner: {
84 | nodes: Map({ id: { id: 'nodeId', type: 'type' } }),
85 | },
86 | });
87 |
88 | store.dispatch(nodeActions.setNodeGraphicalAttributes('id', { selected: true }));
89 |
90 | expect(store.getActions()).toMatchSnapshot();
91 | });
92 |
93 | it('removeNodeGraphicalAttribute', () => {
94 | const store = mockStore({
95 | flowDesigner: {
96 | nodes: Map({ id: { id: 'nodeId', type: 'type' } }),
97 | },
98 | });
99 |
100 | store.dispatch(nodeActions.removeNodeGraphicalAttribute('id', 'selected'));
101 |
102 | expect(store.getActions()).toMatchSnapshot();
103 | });
104 |
105 | it('setNodeData', () => {
106 | const store = mockStore({
107 | flowDesigner: {
108 | nodes: Map({ id: { id: 'nodeId', type: 'type' } }),
109 | },
110 | });
111 |
112 | store.dispatch(nodeActions.setNodeData('id', { type: 'test' }, false));
113 |
114 | expect(store.getActions()).toMatchSnapshot();
115 | });
116 |
117 | it('removeNodeData', () => {
118 | const store = mockStore({
119 | flowDesigner: {
120 | nodes: Map({
121 | id: {
122 | id: 'nodeId',
123 | type: 'type',
124 | data: Map({ testProperties: 'testProperties' }),
125 | },
126 | }),
127 | },
128 | });
129 |
130 | store.dispatch(nodeActions.removeNodeData('id', 'type'));
131 |
132 | expect(store.getActions()).toMatchSnapshot();
133 | });
134 |
135 | it('removeNode', () => {
136 | const store = mockStore({
137 | flowDesigner: {
138 | nodes: Map({ id: { id: 'nodeId', type: 'type' } }),
139 | },
140 | });
141 |
142 | store.dispatch(nodeActions.removeNode('id'));
143 |
144 | expect(store.getActions()).toMatchSnapshot();
145 | });
146 | });
147 |
148 | describe('applyMovementTo', () => {
149 | it('generate proper action', () => {
150 | expect(nodeActions.applyMovementTo([1, 2, 3], { x: 10, y: 5 })).toMatchSnapshot();
151 | });
152 | });
153 |
--------------------------------------------------------------------------------
/src/selectors/portSelectors.ts:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect';
2 | import memoize from 'lodash/memoize';
3 | import { Map } from 'immutable';
4 |
5 | import { Port } from '../api';
6 | import { PORT_SINK, PORT_SOURCE } from '../constants/flowdesigner.constants';
7 | import {
8 | PortRecordMap,
9 | PortRecord,
10 | NodeRecordMap,
11 | LinkRecordMap,
12 | State,
13 | LinkRecord,
14 | } from '../customTypings/index.d';
15 |
16 | const getNodes = (state: State): NodeRecordMap => state.get('nodes');
17 | const getPorts = (state: State): PortRecordMap => state.get('ports');
18 | const getLinks = (state: State): LinkRecordMap => state.get('links');
19 |
20 | /**
21 | * return a list of outgoing port for this node
22 | */
23 | export function outPort(state: State, nodeId: string) {
24 | return state.getIn(['out', nodeId]) || Map();
25 | }
26 |
27 | /**
28 | * return a list of ingoing port for this node
29 | */
30 | export function inPort(state: State, nodeId: string) {
31 | return state.getIn(['in', nodeId]) || Map();
32 | }
33 |
34 | /**
35 | * Create and return function who will return all ports for a specific node
36 | * @return {getPortsForNode}
37 | */
38 | export const getPortsForNode = createSelector(
39 | getPorts,
40 | (ports: PortRecordMap): PortRecord =>
41 | memoize((nodeId: string) =>
42 | ports.filter((port: PortRecord) => Port.getNodeId(port) === nodeId),
43 | ),
44 | );
45 |
46 | /**
47 | * Get all the data Emitter port attached to every nodes as a single map of port
48 | * map key is the port id
49 | * @return Map
50 | */
51 | export const getEmitterPorts = createSelector(
52 | getPorts,
53 | (ports: PortRecordMap): PortRecord =>
54 | ports.filter((port: any) => Port.getTopology(port) === PORT_SOURCE),
55 | );
56 |
57 | /**
58 | * Get all the data Sink port attached to every nodes as a single map of port
59 | * map key is the port id
60 | * @return Map
61 | */
62 | export const getSinkPorts = createSelector(
63 | getPorts,
64 | (ports: PortRecordMap): PortRecord =>
65 | ports.filter((port: any) => Port.getTopology(port) === PORT_SINK),
66 | );
67 |
68 | /**
69 | * Create and return function who will return all Emitter ports for a specific node
70 | */
71 | export const getEmitterPortsForNode = createSelector(
72 | getEmitterPorts,
73 | (ports: PortRecordMap): PortRecord => (nodeId: string) =>
74 | ports.filter((port: any) => Port.getNodeId(port) === nodeId),
75 | );
76 |
77 | /**
78 | * Create and return function who will return all Sink ports for a specific node
79 | */
80 | export const getSinkPortsForNode = createSelector(
81 | getSinkPorts,
82 | (ports: PortRecordMap): PortRecord => (nodeId: string) =>
83 | ports.filter((port: any) => Port.getNodeId(port) === nodeId),
84 | );
85 |
86 | /**
87 | * Get all the data Sink port attached to every nodes not attached at a single edge
88 | * as a single map of port
89 | * map key is the port id
90 | * @return Map
91 | */
92 | export const getFreeSinkPorts = createSelector(
93 | [getSinkPorts, getLinks],
94 | (sinkPorts: PortRecordMap, links: LinkRecordMap) => {
95 | return sinkPorts.filter(
96 | (sinkPort: PortRecord) =>
97 | !links.find((link: LinkRecord) => link.targetId === Port.getId(sinkPort)),
98 | ) as PortRecordMap;
99 | },
100 | );
101 |
102 | /**
103 | * Get all the data Emitter port attached to every nodes not attached at a single edge
104 | * as a single map of port
105 | * map key is the port id
106 | * @return Map
107 | */
108 | export const getFreeEmitterPorts = createSelector(
109 | [getEmitterPorts, getLinks],
110 | (emitterPorts: PortRecordMap, links: LinkRecordMap) =>
111 | emitterPorts.filter(
112 | (emitterPort: PortRecord) =>
113 | !links.find((link: LinkRecord) => link.sourceId === Port.getId(emitterPort)),
114 | ),
115 | );
116 |
117 | /**
118 | * Get all the data sink port attached to every node not attached at a single edge
119 | * as single map of port with an generated attached key
120 | * map key is the port id
121 | * @return Map
122 | */
123 | export const getActionKeyedPorts = createSelector(
124 | getFreeSinkPorts,
125 | (freeSinkPorts: PortRecordMap) =>
126 | freeSinkPorts.filter((sinkPort: { accessKey: any }) => sinkPort.accessKey),
127 | );
128 |
129 | export const getDetachedPorts = createSelector(
130 | [getPorts, getNodes],
131 | (ports: PortRecordMap, nodes: NodeRecordMap) =>
132 | ports.filter(
133 | (port: any) => !nodes.find((node: { id: any }) => node.id === Port.getNodeId(port)),
134 | ),
135 | );
136 |
--------------------------------------------------------------------------------
/src/constants/flowdesigner.model.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable new-cap */
2 | import { Record, Map, List } from 'immutable';
3 | import {
4 | Size,
5 | Position,
6 | PortDirection,
7 | PortRecord as PortRecordType,
8 | } from '../customTypings/index.d';
9 |
10 | export const NONE = 'NONE';
11 | export const SELECTED = 'SELECTED';
12 | export const DROP_TARGET = 'DROP_TARGET';
13 | export const FORBIDDEN_DROP_TARGET = 'FORBIDDEN_DROP_TARGET';
14 |
15 | export const PositionRecord = Record({
16 | x: undefined,
17 | y: undefined,
18 | });
19 |
20 | export const SizeRecord = Record({
21 | width: undefined,
22 | height: undefined,
23 | });
24 |
25 | /** TO BE REMOVED */
26 | export const NodeGraphicalAttributes = Record({
27 | position: new PositionRecord(),
28 | nodeSize: new SizeRecord(),
29 | nodeType: undefined,
30 | label: '',
31 | description: '',
32 | properties: Map(),
33 | });
34 |
35 | /** TO BE REMOVED */
36 | export const NodeData = Record({
37 | properties: Map(),
38 | label: '',
39 | description: '',
40 | datasetInfo: Map(),
41 | });
42 |
43 | /** TO BE REMOVED */
44 | export const LinkGraphicalAttributes = Record({
45 | linkType: undefined,
46 | properties: Map(),
47 | });
48 |
49 | /** TO BE REMOVED */
50 | export const LinkData = Record({
51 | properties: Map(),
52 | });
53 |
54 | /** TO BE REMOVED */
55 | export const PortGraphicalAttributes = Record({
56 | position: PositionRecord,
57 | portType: undefined,
58 | properties: Map(),
59 | });
60 |
61 | /** TO BE REMOVED */
62 | export const PortData = Record({
63 | properties: Map(),
64 | flowType: undefined,
65 | });
66 |
67 | const nodeRecordDefinition = {
68 | id: undefined,
69 | type: undefined,
70 | data: Map({
71 | properties: Map(),
72 | label: '',
73 | description: '',
74 | datasetInfo: Map(),
75 | }),
76 | graphicalAttributes: Map({
77 | position: new PositionRecord(),
78 | nodeSize: new SizeRecord(),
79 | nodeType: undefined,
80 | label: '',
81 | description: '',
82 | properties: Map(),
83 | }),
84 | };
85 |
86 | export class NodeRecord extends Record({
87 | ...nodeRecordDefinition,
88 | getPosition(): Position {
89 | return this.getIn(['graphicalAttributes', 'position']);
90 | },
91 |
92 | getSize(): Size {
93 | return this.getIn(['graphicalAttributes', 'nodeSize']);
94 | },
95 |
96 | getNodeType(): string {
97 | return this.getIn(['graphicalAttributes', 'nodeType']);
98 | },
99 | }) {}
100 |
101 | export class NestedNodeRecord extends Record({
102 | ...nodeRecordDefinition,
103 | components: List(),
104 | getComponents(): Map {
105 | return this.get('components');
106 | },
107 | setComponents(components: Map) {
108 | return this.set('components', components);
109 | },
110 | getPosition(): Position {
111 | return this.getIn(['graphicalAttributes', 'position']);
112 | },
113 |
114 | getSize(): Size {
115 | return this.getIn(['graphicalAttributes', 'nodeSize']);
116 | },
117 |
118 | getNodeType(): string {
119 | return this.getIn(['graphicalAttributes', 'nodeType']);
120 | },
121 | }) {}
122 |
123 | export const LinkRecord = Record({
124 | id: undefined,
125 | sourceId: undefined,
126 | targetId: undefined,
127 | data: Map({
128 | properties: Map(),
129 | }),
130 | graphicalAttributes: Map({
131 | linkType: undefined,
132 | properties: Map(),
133 | }),
134 |
135 | /** methods TO BE REMOVED */
136 | getLinkType(): string {
137 | return this.getIn(['graphicalAttributes', 'linkType']);
138 | },
139 | });
140 |
141 | export const PortRecord = Record({
142 | id: undefined,
143 | nodeId: undefined,
144 | data: Map({
145 | properties: Map(),
146 | flowType: undefined,
147 | }),
148 | graphicalAttributes: Map({
149 | position: PositionRecord,
150 | portType: undefined,
151 | properties: Map(),
152 | }),
153 |
154 | /** methods TO BE REMOVED */
155 | getPosition(): Position {
156 | return this.getIn(['graphicalAttributes', 'position']);
157 | },
158 | setPosition(position: Position): PortRecordType {
159 | return this.setIn(['graphicalAttributes', 'position'], position);
160 | },
161 | getPortType(): string {
162 | return this.getIn(['graphicalAttributes', 'portType']);
163 | },
164 | getPortDirection(): PortDirection {
165 | return this.getIn(['graphicalAttributes', 'properties', 'type']);
166 | },
167 | getPortFlowType(): string {
168 | return this.getIn(['data', 'flowType']);
169 | },
170 | getIndex(): number {
171 | return this.getIn(['graphicalAttributes', 'properties', 'index']);
172 | },
173 | setIndex(index: number): PortRecordType {
174 | return this.setIn(['graphicalAttributes', 'properties', 'index'], index);
175 | },
176 | });
177 |
--------------------------------------------------------------------------------
/src/reducers/link.reducer.test.ts:
--------------------------------------------------------------------------------
1 | import { Map, fromJS } from 'immutable';
2 |
3 | import { defaultState } from './flow.reducer';
4 | import linkReducer from './link.reducer';
5 | import { LinkRecord, PortRecord, NodeRecord } from '../constants/flowdesigner.model';
6 |
7 | describe('check linkreducer', () => {
8 | const initialState = defaultState
9 | .set(
10 | 'nodes',
11 | Map()
12 | .set(
13 | 'id1',
14 | new NodeRecord({
15 | id: 'id1',
16 | }),
17 | )
18 | .set(
19 | 'id2',
20 | new NodeRecord({
21 | id: 'id2',
22 | }),
23 | )
24 | .set(
25 | 'id3',
26 | new NodeRecord({
27 | id: 'id3',
28 | }),
29 | ),
30 | )
31 | .set(
32 | 'links',
33 | Map().set(
34 | 'id1',
35 | new LinkRecord({
36 | id: 'id1',
37 | sourceId: 'id1',
38 | targetId: 'id2',
39 | data: Map().set('attr', 'attr'),
40 | graphicalAttributes: Map().set('properties', fromJS({ attr: 'attr' })),
41 | }),
42 | ),
43 | )
44 | .set(
45 | 'ports',
46 | Map()
47 | .set(
48 | 'id1',
49 | new PortRecord({
50 | id: 'id1',
51 | nodeId: 'id1',
52 | }),
53 | )
54 | .set(
55 | 'id2',
56 | new PortRecord({
57 | id: 'id2',
58 | nodeId: 'id2',
59 | }),
60 | )
61 | .set(
62 | 'id3',
63 | new PortRecord({
64 | id: 'id3',
65 | nodeId: 'id2',
66 | }),
67 | )
68 | .set(
69 | 'id4',
70 | new PortRecord({
71 | id: 'id4',
72 | nodeId: 'id1',
73 | }),
74 | )
75 | .set(
76 | 'id5',
77 | new PortRecord({
78 | id: 'id5',
79 | nodeId: 'id3',
80 | }),
81 | )
82 | .set(
83 | 'id6',
84 | new PortRecord({
85 | id: 'id6',
86 | nodeID: 'id3',
87 | }),
88 | ),
89 | )
90 | .set(
91 | 'parents',
92 | Map().set('id1', Map()).set('id2', Map().set('id1', 'id1')).set('id3', Map()),
93 | )
94 | .set(
95 | 'childrens',
96 | Map().set('id1', Map().set('id2', 'id2')).set('id2', Map()).set('id3', Map()),
97 | );
98 |
99 | it('FLOWDESIGNER_LINK_ADD should add a new link to the state', () => {
100 | const newState = linkReducer(initialState, {
101 | type: 'FLOWDESIGNER_LINK_ADD',
102 | linkId: 'id2',
103 | sourceId: 'id1',
104 | targetId: 'id2',
105 | });
106 | expect(newState).toMatchSnapshot();
107 | expect(
108 | linkReducer(newState, {
109 | type: 'FLOWDESIGNER_LINK_ADD',
110 | linkId: 'id3',
111 | sourceId: 'id3',
112 | targetId: 'id5',
113 | }),
114 | ).toMatchSnapshot();
115 | });
116 |
117 | it('FLOWDESIGNER_LINK_REMOVE should remove link from state', () => {
118 | expect(
119 | linkReducer(initialState, { type: 'FLOWDESIGNER_LINK_REMOVE', linkId: 'id1' }),
120 | ).toMatchSnapshot();
121 | });
122 |
123 | it('FLOWDESIGNER_LINK_SET_TARGET switch target to correct port if it exist', () => {
124 | expect(
125 | linkReducer(initialState, {
126 | type: 'FLOWDESIGNER_LINK_SET_TARGET',
127 | linkId: 'id1',
128 | targetId: 'id5',
129 | }),
130 | ).toMatchSnapshot();
131 | });
132 |
133 | it('FLOWDESIGNER_LINK_SET_SOURCE switch source to correct port if it exist', () => {
134 | expect(
135 | linkReducer(initialState, {
136 | type: 'FLOWDESIGNER_LINK_SET_SOURCE',
137 | linkId: 'id1',
138 | sourceId: 'id4',
139 | }),
140 | ).toMatchSnapshot();
141 | });
142 |
143 | it('FLOWDESIGNER_LINK_SET_GRAPHICAL_ATTRIBUTES should merge attributes within link attr property', () => {
144 | expect(
145 | linkReducer(initialState, {
146 | type: 'FLOWDESIGNER_LINK_SET_GRAPHICAL_ATTRIBUTES',
147 | linkId: 'id1',
148 | graphicalAttributes: { selected: false },
149 | }),
150 | ).toMatchSnapshot();
151 | });
152 |
153 | it('FLOWDESIGNER_LINK_REMOVE_GRAPHICAL_ATTRIBUTES should remove a specific attributes from attr map', () => {
154 | expect(
155 | linkReducer(initialState, {
156 | type: 'FLOWDESIGNER_LINK_REMOVE_GRAPHICAL_ATTRIBUTES',
157 | linkId: 'id1',
158 | graphicalAttributesKey: 'attr',
159 | }),
160 | ).toMatchSnapshot();
161 | });
162 |
163 | it("FLOWDESIGNER_LINK_SET_DATA should add a data attribute type: 'test' from data map", () => {
164 | expect(
165 | linkReducer(initialState, {
166 | type: 'FLOWDESIGNER_LINK_SET_DATA',
167 | linkId: 'id1',
168 | data: { type: 'test' },
169 | }),
170 | ).toMatchSnapshot();
171 | });
172 |
173 | it("FLOWDESIGNER_LINK_REMOVE_DATA should remove 'attr'from data map", () => {
174 | expect(
175 | linkReducer(initialState, {
176 | type: 'FLOWDESIGNER_LINK_SET_DATA',
177 | linkId: 'id1',
178 | datakey: 'attr',
179 | }),
180 | ).toMatchSnapshot();
181 | });
182 | });
183 |
--------------------------------------------------------------------------------
/src/reducers/port.reducer.test.ts:
--------------------------------------------------------------------------------
1 | import { Map, OrderedMap } from 'immutable';
2 |
3 | import { defaultState } from './flow.reducer';
4 | import portReducer from './port.reducer';
5 | import { PortRecord, PositionRecord } from '../constants/flowdesigner.model';
6 | import { PORT_SINK, PORT_SOURCE } from '../constants/flowdesigner.constants';
7 |
8 | describe('Check port reducer', () => {
9 | const initialState = defaultState
10 | .set(
11 | 'ports',
12 | // eslint-disable-next-line new-cap
13 | OrderedMap()
14 | .set(
15 | 'id1',
16 | new PortRecord({
17 | id: 'id1',
18 | data: Map({ type: 'test' }),
19 | graphicalAttributes: Map({
20 | type: 'test',
21 | position: new PositionRecord({ x: 10, y: 10 }),
22 | }),
23 | }),
24 | )
25 | .set(
26 | 'id2',
27 | new PortRecord({
28 | id: 'id2',
29 | nodeId: 'test',
30 | graphicalAttributes: Map({
31 | position: new PositionRecord({ x: 10, y: 10 }),
32 | }),
33 | }),
34 | )
35 | .set(
36 | 'id3',
37 | new PortRecord({
38 | id: 'id3',
39 | graphicalAttributes: Map({
40 | position: new PositionRecord({ x: 10, y: 10 }),
41 | }),
42 | }),
43 | ),
44 | )
45 | .set('nodes', Map().set('nodeId', Map()))
46 | .set('links', Map());
47 |
48 | it('FLOWDESIGNER_PORT_ADD properly add the port to the port Map', () => {
49 | expect(
50 | portReducer(initialState, {
51 | type: 'FLOWDESIGNER_PORT_ADD',
52 | nodeId: 'nodeId',
53 | id: 'portId',
54 | data: { flowType: 'string', properties: {} },
55 | graphicalAttributes: {
56 | portType: 'portType',
57 | properties: { type: PORT_SOURCE, index: 1 },
58 | },
59 | }),
60 | ).toMatchSnapshot();
61 | });
62 |
63 | it('FLOWDESIGNER_PORT_ADDS to add multiple ports (portId1, portId2) to port collection', () => {
64 | expect(
65 | portReducer(initialState, {
66 | type: 'FLOWDESIGNER_PORT_ADDS',
67 | nodeId: 'nodeId',
68 | ports: [
69 | {
70 | id: 'portId1',
71 | nodeId: 'nodeId',
72 | data: { flowType: 'string', properties: {} },
73 | graphicalAttributes: {
74 | portType: 'portType',
75 | properties: { type: PORT_SOURCE },
76 | },
77 | },
78 | {
79 | id: 'portId2',
80 | nodeId: 'nodeId',
81 | data: { flowType: 'string', properties: {} },
82 | graphicalAttributes: {
83 | portType: 'portType',
84 | properties: { type: PORT_SINK },
85 | },
86 | },
87 | {
88 | id: 'portId3',
89 | nodeId: 'nodeId',
90 | data: { flowType: 'string', properties: {} },
91 | graphicalAttributes: {
92 | portType: 'portType',
93 | properties: { type: PORT_SINK },
94 | },
95 | },
96 | ],
97 | }),
98 | ).toMatchSnapshot();
99 | });
100 |
101 | it('FLOWDESIGNER_PORT_SET_GRAPHICAL_ATTRIBUTES to merge { selected: true } on port id1 graphicalAttribute map', () => {
102 | expect(
103 | portReducer(initialState, {
104 | type: 'FLOWDESIGNER_PORT_SET_GRAPHICAL_ATTRIBUTES',
105 | portId: 'id1',
106 | graphicalAttributes: { selected: true },
107 | }),
108 | ).toMatchSnapshot();
109 | });
110 |
111 | it('FLOWDESIGNER_PORT_REMOVE_GRAPHICAL_ATTRIBUTES to remove attr on port id1 graphicalAttribute map', () => {
112 | expect(
113 | portReducer(initialState, {
114 | type: 'FLOWDESIGNER_PORT_REMOVE_GRAPHICAL_ATTRIBUTES',
115 | portId: 'id1',
116 | graphicalAttributesKey: 'attr',
117 | }),
118 | ).toMatchSnapshot();
119 | });
120 |
121 | it("FLOWDESIGNER_PORT_SET_DATA to merge { type: 'string' } on port id1 data map", () => {
122 | expect(
123 | portReducer(initialState, {
124 | type: 'FLOWDESIGNER_PORT_SET_DATA',
125 | portId: 'id1',
126 | data: { type: 'string' },
127 | }),
128 | ).toMatchSnapshot();
129 | });
130 |
131 | it('FLOWDESIGNER_PORT_REMOVE_DATA remove type on port id1 on port data map', () => {
132 | expect(
133 | portReducer(initialState, {
134 | type: 'FLOWDESIGNER_PORT_REMOVE_DATA',
135 | portId: 'id1',
136 | dataKey: 'type',
137 | }),
138 | ).toMatchSnapshot();
139 | });
140 |
141 | it('FLOWDESIGNER_PORT_REMOVE should only remove port id1 from ports collection', () => {
142 | expect(
143 | portReducer(initialState, {
144 | type: 'FLOWDESIGNER_PORT_REMOVE',
145 | portId: 'id1',
146 | }),
147 | ).toMatchSnapshot();
148 | });
149 |
150 | it('FLOWDESIGNER_PORT_REMOVE should only remove port id2 from ports collection if its parent node does not exist', () => {
151 | expect(
152 | portReducer(initialState, {
153 | type: 'FLOWDESIGNER_PORT_REMOVE',
154 | portId: 'id2',
155 | }),
156 | ).toMatchSnapshot();
157 | });
158 | });
159 |
--------------------------------------------------------------------------------
/src/api/size/size.test.ts:
--------------------------------------------------------------------------------
1 | import { Map } from 'immutable';
2 |
3 | import { SizeRecord } from '../../constants/flowdesigner.model';
4 |
5 | import * as Size from './size';
6 |
7 | const isNotSizeException = `SizeRecord should be a SizeRecord, was given
8 | """
9 | object
10 | """
11 | Map {}
12 | """
13 | you should use Size module functions to create and transform Size`;
14 | const isNotProperSizeException = `SizeRecord should be a SizeRecord, was given
15 | """
16 | object
17 | """
18 | Map { "width": 10, "height": 10 }
19 | """
20 | you should use Size module functions to create and transform Size`;
21 | const isImproperWidth = 'width should be a number, was given 10 of type string';
22 | const isImproperHeight = 'height should be a number, was given 50 of type string';
23 |
24 | describe('isSizeElseThrow', () => {
25 | it('return true if parameter size is a SizeRecord', () => {
26 | // given
27 | const testSize = new SizeRecord();
28 | // when
29 | const test = Size.isSizeElseThrow(testSize);
30 | // expect
31 | expect(test).toEqual(true);
32 | });
33 |
34 | it('throw an error if parameter is not a SizeRecord', () => {
35 | // given
36 | const testSize = Map();
37 | // when
38 | // expect
39 | expect(() => Size.isSizeElseThrow(testSize)).toThrow(isNotSizeException);
40 | });
41 | });
42 |
43 | describe('Size', () => {
44 | const width = 10;
45 | const height = 50;
46 | const testSize = Size.create(width, height);
47 |
48 | const improperWidth = '10';
49 | const improperHeight = '50';
50 | const improperTestSize = Map({ width: 10, height: 10 });
51 | describe('create', () => {
52 | it('given proper width and height return a Size', () => {
53 | // given
54 | // when
55 | const test = Size.create(width, height);
56 | // expect
57 | expect(Size.isSize(test)).toEqual(true);
58 | });
59 | it('throw if given an improper width', () => {
60 | // given
61 | // when
62 | // expect
63 | expect(() => Size.create(improperWidth as any, height)).toThrow(isImproperWidth);
64 | });
65 | it('throw if given an improper Height', () => {
66 | // given
67 | // when
68 | // expect
69 | expect(() => Size.create(width, improperHeight as any)).toThrow(isImproperHeight);
70 | });
71 | });
72 | describe('isSize', () => {
73 | it('return true if parameter size is a SizeRecord', () => {
74 | // given
75 | // when
76 | const test = Size.isSize(testSize);
77 | // expect
78 | expect(test).toEqual(true);
79 | });
80 |
81 | it('thow an error if parameter is not a NodeRecord', () => {
82 | // given
83 | // when
84 | const test = Size.isSize(improperTestSize);
85 | // expect
86 | expect(test).toEqual(false);
87 | });
88 | });
89 | describe('getWidth', () => {
90 | it('given a proper Size return width', () => {
91 | // given
92 | // when
93 | const test = Size.getWidth(testSize);
94 | // expect
95 | expect(test).toEqual(width);
96 | });
97 | it('throw given an improper Size', () => {
98 | expect(() => Size.getWidth(improperTestSize)).toThrow(isNotProperSizeException);
99 | });
100 | });
101 | describe('setWidth', () => {
102 | it('given a proper Size and a width return a Size with updated width', () => {
103 | // given
104 | const newWidth = 500;
105 | // when
106 | const test = Size.setWidth(newWidth, testSize);
107 | // expect
108 | expect(Size.getWidth(test)).toEqual(newWidth);
109 | });
110 | it('throw given an improper width', () => {
111 | // given
112 | // when
113 | // expect
114 | expect(() => Size.setWidth(improperWidth as any, testSize)).toThrow(
115 | 'width should be a number, was given 10 of type string',
116 | );
117 | });
118 | it('throw given an improper Position', () => {
119 | // given
120 | // when
121 | // expect
122 | expect(() => Size.setWidth(width, improperTestSize)).toThrow(isNotProperSizeException);
123 | });
124 | });
125 | describe('getHeight', () => {
126 | it('given a proper size return height', () => {
127 | // given
128 | // when
129 | const test = Size.getHeight(testSize);
130 | // expect
131 | expect(test).toEqual(height);
132 | });
133 | it('throw given an improper size', () => {
134 | expect(() => Size.getHeight(improperTestSize)).toThrow(isNotProperSizeException);
135 | });
136 | });
137 | describe('setHeight', () => {
138 | it('given a proper Size and width return a Position with updated width', () => {
139 | // given
140 | const newHeight = 500;
141 | // when
142 | const test = Size.setHeight(newHeight, testSize);
143 | // expect
144 | expect(Size.getHeight(test)).toEqual(newHeight);
145 | });
146 | it('throw given an improper height', () => {
147 | // given
148 | // when
149 | // expect
150 | expect(() => Size.setHeight(improperHeight as any, testSize)).toThrow(
151 | 'height should be a number, was given 50 of type string',
152 | );
153 | });
154 | it('throw given an improper Size', () => {
155 | // given
156 | // when
157 | // expect
158 | expect(() => Size.setHeight(height, improperTestSize)).toThrow(
159 | isNotProperSizeException,
160 | );
161 | });
162 | });
163 | });
164 |
--------------------------------------------------------------------------------
/src/customTypings/index.d.ts:
--------------------------------------------------------------------------------
1 | import { Record, Map } from 'immutable';
2 | import { PORT_SINK, PORT_SOURCE } from '../constants/flowdesigner.constants';
3 |
4 | /** $BASIC */
5 |
6 | export type Id = string;
7 |
8 | export interface Position {
9 | x: number;
10 | y: number;
11 | }
12 |
13 | export interface Size {
14 | width: number;
15 | height: number;
16 | }
17 |
18 | export interface Action {
19 | type: string;
20 | }
21 |
22 | export type PortDirection = typeof PORT_SINK | typeof PORT_SOURCE;
23 |
24 | export interface PortGraphicalAttributes {
25 | portType?: string;
26 | position?: Position;
27 | properties: {
28 | type: PortDirection;
29 | index?: number;
30 | } & any;
31 | }
32 |
33 | export interface PortData {
34 | flowType: string;
35 | properties?: {};
36 | }
37 |
38 | export interface Port {
39 | id: Id;
40 | nodeId: string;
41 | data?: PortData;
42 | graphicalAttributes?: PortGraphicalAttributes;
43 | }
44 |
45 | export interface NodeGraphicalAttributes {
46 | position: Position;
47 | nodeSize: Size;
48 | nodeType: string;
49 | label: string;
50 | description: string;
51 | properties?: {};
52 | }
53 |
54 | export interface NodeData {
55 | datasetId: Id;
56 | properties?: {};
57 | label: string;
58 | description: string;
59 | datasetInfo?: {};
60 | }
61 |
62 | export interface Node {
63 | id: Id;
64 | type: string;
65 | data: NodeData;
66 | graphicalAttributes: NodeGraphicalAttributes;
67 | components?: List;
68 | }
69 |
70 | export interface LinkGraphicalAttributes {
71 | linkType: string;
72 | properties?: {};
73 | }
74 |
75 | export interface LinkData {
76 | properties?: {};
77 | }
78 |
79 | export interface Link {
80 | id: Id;
81 | source: Id;
82 | target: Id;
83 | data: LinkData;
84 | graphicalAttributes: LinkGraphicalAttributes;
85 | }
86 |
87 | /** $RECORDS */
88 | export type PositionRecord = Record & Position;
89 |
90 | export type SizeRecord = Record & Size;
91 |
92 | export type PortRecord = Record & {
93 | getPosition: () => Position;
94 | getPortType: () => string;
95 | getPortDirection: () => PortDirection;
96 | getPortFlowType: () => string;
97 | getIndex: () => number;
98 | setIndex: (index: number) => PortRecord;
99 | } & Port;
100 |
101 | // TODO add record
102 | export type NodeRecord = Record & {
103 | getPosition: () => Position;
104 | getSize: () => Size;
105 | getNodeType: () => string;
106 | } & Node;
107 |
108 | export type NestedNodeRecord = Record & {
109 | getPosition: () => Position;
110 | getSize: () => Size;
111 | getNodeType: () => string;
112 | } & Node;
113 |
114 | export type LinkRecord = Record & {
115 | getLinkType: () => string;
116 | } & Link;
117 |
118 | /** $STATE */
119 |
120 | export type PortRecordMap = Map;
121 | export type NodeRecordMap = Map;
122 | export type LinkRecordMap = Map;
123 |
124 | type getStateNodes = (selector: ['nodes', Id]) => NodeRecord;
125 | type getStatePorts = (selector: ['ports', Id]) => PortRecord;
126 | type getStateLinks = (selector: ['links', Id]) => LinkRecord;
127 | type getStateIn = (selector: ['in', Id]) => Id;
128 | type getStateOut = (selector: ['out', Id]) => Id;
129 |
130 | export type State = {
131 | in: Map>;
132 | parents: Map>;
133 | transform: Transform;
134 | transformToApply?: Transform;
135 | out: Map>;
136 | nodes: Map>;
137 | ports: Map>;
138 | children: Map>;
139 | nodeTypes: Map>;
140 | links: Map>;
141 | } & Map & { getIn: getStateNodes | getStatePorts | getStateLinks | getStateIn | getStateOut };
142 |
143 | /** $ACTIONS */
144 | export interface PortActionAdd {
145 | type: 'FLOWDESIGNER_PORT_ADD';
146 | nodeId: Id;
147 | id: Id;
148 | data?: PortData;
149 | graphicalAttributes?: PortGraphicalAttributes;
150 | }
151 |
152 | export type PortAction =
153 | | PortActionAdd
154 | | {
155 | type: 'FLOWDESIGNER_PORT_ADDS';
156 | nodeId: Id;
157 | ports: Array;
158 | }
159 | | {
160 | type: 'FLOWDESIGNER_PORT_SET_GRAPHICAL_ATTRIBUTES';
161 | portId: Id;
162 | graphicalAttributes: {};
163 | }
164 | | {
165 | type: 'FLOWDESIGNER_PORT_REMOVE_GRAPHICAL_ATTRIBUTES';
166 | portId: Id;
167 | graphicalAttributesKey: string;
168 | }
169 | | {
170 | type: 'FLOWDESIGNER_PORT_SET_DATA';
171 | portId: Id;
172 | data: Object;
173 | }
174 | | {
175 | type: 'FLOWDESIGNER_PORT_REMOVE_DATA';
176 | portId: Id;
177 | dataKey: string;
178 | }
179 | | {
180 | type: 'FLOWDESIGNER_PORT_REMOVE';
181 | portId: Id;
182 | };
183 |
184 | export interface NodeType {
185 | id: string;
186 | position: Position;
187 | }
188 |
189 | export interface PortType {
190 | id: string;
191 | nodeId: string;
192 | position: Position;
193 | }
194 |
195 | export interface LinkType {
196 | id: string;
197 | sourceId: string;
198 | targetId: string;
199 | }
200 |
201 | export interface Transform {
202 | k: number;
203 | x: number;
204 | y: number;
205 | }
206 |
--------------------------------------------------------------------------------
/src/actions/node.actions.ts:
--------------------------------------------------------------------------------
1 | import {
2 | FLOWDESIGNER_NODE_MOVE_START,
3 | FLOWDESIGNER_NODE_APPLY_MOVEMENT,
4 | FLOWDESIGNER_NODE_MOVE,
5 | FLOWDESIGNER_NODE_MOVE_END,
6 | FLOWDESIGNER_NODE_ADD,
7 | FLOWDESIGNER_NODE_SET_TYPE,
8 | FLOWDESIGNER_NODE_SET_GRAPHICAL_ATTRIBUTES,
9 | FLOWDESIGNER_NODE_REMOVE_GRAPHICAL_ATTRIBUTES,
10 | FLOWDESIGNER_NODE_SET_DATA,
11 | FLOWDESIGNER_NODE_REMOVE_DATA,
12 | FLOWDESIGNER_NODE_SET_SIZE,
13 | FLOWDESIGNER_NODE_REMOVE,
14 | FLOWDESIGNER_NODE_UPDATE,
15 | } from '../constants/flowdesigner.constants';
16 | import { Position } from '../customTypings/index.d';
17 |
18 | /**
19 | * Ask for node creation and injection into current dataflow
20 | * @param {string} nodeId
21 | * @param {string} nodeType
22 | * @param {Object} attr
23 | * @return {Object}
24 | */
25 | export const addNode = (
26 | nodeId: string,
27 | nodeType?: string,
28 | { data = {}, graphicalAttributes = {} }: any = {},
29 | ) => ({
30 | type: FLOWDESIGNER_NODE_ADD,
31 | nodeId,
32 | nodeType,
33 | data,
34 | graphicalAttributes,
35 | });
36 |
37 | /**
38 | * @deprecated use moveStart action
39 | */
40 | export function startMoveNodeTo(nodeId: string, nodePosition: string) {
41 | return {
42 | type: FLOWDESIGNER_NODE_MOVE_START,
43 | nodeId,
44 | nodePosition,
45 | };
46 | }
47 |
48 | /**
49 | * Ask for moving node
50 | * @deprecated use move action
51 | * @param {string} nodeId - identifier of the targeted node
52 | * @param {{x: number, y: number}} nodePosition - the new absolute position of the node
53 | * @return {Object}
54 | */
55 | export function moveNodeTo(nodeId: string, nodePosition: Position) {
56 | return {
57 | type: FLOWDESIGNER_NODE_MOVE,
58 | nodeId,
59 | nodePosition,
60 | };
61 | }
62 |
63 | /**
64 | * Ask to apply the same movement to multiples nodesId
65 | * @deprecated
66 | * @param nodesId {array} list of nodeId
67 | * @param movement {Object} relative movement to apply on all nodes
68 | *
69 | * @return {Object}
70 | */
71 | export const applyMovementTo = (nodesId: number[], movement: Position) => ({
72 | type: FLOWDESIGNER_NODE_APPLY_MOVEMENT,
73 | nodesId,
74 | movement,
75 | });
76 |
77 | /**
78 | * When node movement is done
79 | * @deprecated use moveEnd action
80 | * @param {string} nodeId - identifier of the targeted node
81 | * @param {{x: number, y: number}} nodePosition - the new absolute position of the node
82 | * @return {Object}
83 | */
84 | export function moveNodeToEnd(nodeId: string, nodePosition: { x: number; y: number }) {
85 | return {
86 | type: FLOWDESIGNER_NODE_MOVE_END,
87 | nodeId,
88 | nodePosition,
89 | };
90 | }
91 |
92 | /**
93 | * set node size
94 | * @deprecated
95 | * @param {string} nodeId
96 | * @param {{height: number, width: number}} nodeSize
97 | * @return {Object}
98 | */
99 | export const setNodeSize = (nodeId: string, nodeSize: { width: number; height: number }) => ({
100 | type: FLOWDESIGNER_NODE_SET_SIZE,
101 | nodeId,
102 | nodeSize,
103 | });
104 |
105 | /**
106 | * Ask for node creation and injection into current dataflow
107 | * @deprecated
108 | * @param {string} nodeId
109 | * @param {string} nodeType
110 | * @return {Object}
111 | */
112 | export function setNodeType(nodeId: string, nodeType: string) {
113 | return {
114 | type: FLOWDESIGNER_NODE_SET_TYPE,
115 | nodeId,
116 | nodeType,
117 | };
118 | }
119 |
120 | /**
121 | * Give the ability to a graphical attribute onto the node
122 | * @deprecated
123 | * @param {string} nodeId
124 | * @param {Object} graphicalAttributes
125 | */
126 | export const setNodeGraphicalAttributes = (
127 | nodeId: string,
128 | graphicalAttributes: { selected: boolean },
129 | ) => ({
130 | type: FLOWDESIGNER_NODE_SET_GRAPHICAL_ATTRIBUTES,
131 | nodeId,
132 | graphicalAttributes,
133 | });
134 |
135 | /**
136 | * Ask to remove a graphical attribute on target node
137 | * @deprecated
138 | * @param {string} nodeId
139 | * @param {string} graphicalAttributesKey - the key of the attribute to be removed
140 | */
141 | export const removeNodeGraphicalAttribute = (nodeId: string, graphicalAttributesKey: string) => ({
142 | type: FLOWDESIGNER_NODE_REMOVE_GRAPHICAL_ATTRIBUTES,
143 | nodeId,
144 | graphicalAttributesKey,
145 | });
146 |
147 | /**
148 | * Give the ability to set data onto a node
149 | * @deprecated
150 | * @param {string} nodeId
151 | * @param {Object} data
152 | * @param {boolean} bySubmit Flag to indicates that the action was triggered by a manual user action
153 | */
154 | export const setNodeData = (nodeId: string, data: { type: string }, bySubmit: boolean) => ({
155 | type: FLOWDESIGNER_NODE_SET_DATA,
156 | nodeId,
157 | data,
158 | bySubmit,
159 | });
160 |
161 | /**
162 | * Ask to remove a graphical attribute on target node
163 | * @deprecated
164 | * @param {string} nodeId
165 | * @param {string} dataKey - the key of the data to be removed
166 | */
167 | export const removeNodeData = (nodeId: string, dataKey: string) => ({
168 | type: FLOWDESIGNER_NODE_REMOVE_DATA,
169 | nodeId,
170 | dataKey,
171 | });
172 |
173 | /**
174 | * Ask for removal of target node and each ports/links attached to it
175 | * @deprecated use deleteNode action
176 | * @param {string} nodeId
177 | */
178 | export const removeNode = (nodeId: string) => ({
179 | type: FLOWDESIGNER_NODE_REMOVE,
180 | nodeId,
181 | });
182 |
183 | export const update = (nodeId: string, node: any) => ({
184 | type: FLOWDESIGNER_NODE_UPDATE,
185 | node,
186 | nodeId,
187 | });
188 |
--------------------------------------------------------------------------------
/src/api/position/position.test.ts:
--------------------------------------------------------------------------------
1 | import { Map } from 'immutable';
2 |
3 | import { PositionRecord } from '../../constants/flowdesigner.model';
4 |
5 | import * as Position from './position';
6 |
7 | const isNotPositionException = `PositionRecord should be a PositionRecord, was given
8 | """
9 | object
10 | """
11 | Map {}
12 | """
13 | you should use Position module functions to create and transform Position`;
14 | const improperPositionException = `PositionRecord should be a PositionRecord, was given
15 | """
16 | object
17 | """
18 | Map { "x": 10, "y": 10 }
19 | """
20 | you should use Position module functions to create and transform Position`;
21 | const isImproperXCoordinate = 'x should be a number, was given 10 of type string';
22 | const isImproperYCoordinate = 'y should be a number, was given 50 of type string';
23 |
24 | describe('isPositionElseThrow', () => {
25 | it('return true if parameter position is a PositionRecord', () => {
26 | // given
27 | const testPosition = new PositionRecord();
28 | // when
29 | const test = Position.isPositionElseThrow(testPosition);
30 | // expect
31 | expect(test).toEqual(true);
32 | });
33 |
34 | it('thow an error if parameter is not a PositionRecord', () => {
35 | // given
36 | const testPosition = Map();
37 | // when
38 | // expect
39 | expect(() => Position.isPositionElseThrow(testPosition)).toThrow(isNotPositionException);
40 | });
41 | });
42 |
43 | describe('Position', () => {
44 | const x = 10;
45 | const y = 50;
46 | const testPosition = Position.create(x, y);
47 |
48 | const improperX = '10';
49 | const improperY = '50';
50 | const improperTestPosition = Map({ x: 10, y: 10 });
51 | describe('create', () => {
52 | it('given proper x and y coordinate return a Position', () => {
53 | // given
54 | // when
55 | const test = Position.create(x, y);
56 | // expect
57 | expect(Position.isPosition(test)).toEqual(true);
58 | });
59 | it('throw if given an improper id', () => {
60 | // given
61 | // when
62 | // expect
63 | expect(() => Position.create(improperX as any, y)).toThrow(isImproperXCoordinate);
64 | });
65 | it('throw if given an improper Position', () => {
66 | // given
67 | // when
68 | // expect
69 | expect(() => Position.create(x, improperY as any)).toThrow(isImproperYCoordinate);
70 | });
71 | });
72 | describe('isPosition', () => {
73 | it('return true if parameter position is a PositionRecord', () => {
74 | // given
75 | // when
76 | const test = Position.isPosition(testPosition);
77 | // expect
78 | expect(test).toEqual(true);
79 | });
80 |
81 | it('thow an error if parameter is not a NodeRecord', () => {
82 | // given
83 | // when
84 | const test = Position.isPosition(improperTestPosition);
85 | // expect
86 | expect(test).toEqual(false);
87 | });
88 | });
89 | describe('getXCoordinate', () => {
90 | it('given a proper position return x', () => {
91 | // given
92 | // when
93 | const test = Position.getXCoordinate(testPosition);
94 | // expect
95 | expect(test).toEqual(x);
96 | });
97 | it('throw given an improper position', () => {
98 | expect(() => Position.getXCoordinate(improperTestPosition)).toThrow(
99 | improperPositionException,
100 | );
101 | });
102 | });
103 | describe('setXCoordinate', () => {
104 | it('given a proper Position and X coordinate return a Position with updated coordinate', () => {
105 | // given
106 | const newX = 500;
107 | // when
108 | const test = Position.setXCoordinate(newX, testPosition);
109 | // expect
110 | expect(Position.getXCoordinate(test)).toEqual(newX);
111 | });
112 | it('throw given an improper X coordinate', () => {
113 | // given
114 | // when
115 | // expect
116 | expect(() => Position.setXCoordinate(improperX as any, testPosition)).toThrow(
117 | 'x should be a number, was given 10 of type string',
118 | );
119 | });
120 | it('throw given an improper Position', () => {
121 | // given
122 | // when
123 | // expect
124 | expect(() => Position.setXCoordinate(x, improperTestPosition)).toThrow(
125 | improperPositionException,
126 | );
127 | });
128 | });
129 | describe('getYCoordinate', () => {
130 | it('given a proper Position return y', () => {
131 | // given
132 | // when
133 | const test = Position.getYCoordinate(testPosition);
134 | // expect
135 | expect(test).toEqual(y);
136 | });
137 | it('throw given an improper position', () => {
138 | expect(() => Position.getYCoordinate(improperTestPosition)).toThrow(
139 | improperPositionException,
140 | );
141 | });
142 | });
143 | describe('setYCoordinate', () => {
144 | it('given a proper Position and Y coordinate return a Position with updated coordinate', () => {
145 | // given
146 | const newY = 500;
147 | // when
148 | const test = Position.setYCoordinate(newY, testPosition);
149 | // expect
150 | expect(Position.getYCoordinate(test)).toEqual(newY);
151 | });
152 | it('throw given an improperY coordinate', () => {
153 | // given
154 | // when
155 | // expect
156 | expect(() => Position.setYCoordinate(improperY as any, testPosition)).toThrow(
157 | 'y should be a number, was given 50 of type string',
158 | );
159 | });
160 | it('throw given an improper Position', () => {
161 | // given
162 | // when
163 | // expect
164 | expect(() => Position.setYCoordinate(y, improperTestPosition)).toThrow(
165 | improperPositionException,
166 | );
167 | });
168 | });
169 | });
170 |
--------------------------------------------------------------------------------
/src/reducers/flow.reducer.test.ts:
--------------------------------------------------------------------------------
1 | import { Map } from 'immutable';
2 |
3 | import { reducer, calculatePortsPosition, defaultState } from './flow.reducer';
4 | import * as nodeActions from '../actions/node.actions';
5 | import * as portActions from '../actions/port.actions';
6 | import { NodeRecord, PortRecord } from '../constants/flowdesigner.model';
7 | import { PORT_SOURCE } from '../constants/flowdesigner.constants';
8 |
9 | describe('FLOWDESIGNER_FLOW_ADD_ELEMENTS', () => {
10 | it('should batch one element creation', () => {
11 | expect(
12 | reducer(defaultState, {
13 | type: 'FLOWDESIGNER.FLOW_ADD_ELEMENTS',
14 | listOfActionCreation: [
15 | nodeActions.addNode('nodeId', undefined, {
16 | data: {},
17 | graphicalAttributes: {
18 | nodeSize: { height: 10, width: 10 },
19 | position: { x: 10, y: 10 },
20 | },
21 | }),
22 | ],
23 | }),
24 | ).toMatchSnapshot();
25 | });
26 |
27 | it('should batch many elements creation', () => {
28 | expect(
29 | reducer(defaultState, {
30 | type: 'FLOWDESIGNER.FLOW_ADD_ELEMENTS',
31 | listOfActionCreation: [
32 | nodeActions.addNode('nodeId', undefined, {
33 | data: {},
34 | graphicalAttributes: {
35 | nodeSize: { height: 10, width: 10 },
36 | position: { x: 10, y: 10 },
37 | },
38 | }),
39 | nodeActions.addNode('node2', undefined, {
40 | data: {},
41 | graphicalAttributes: {
42 | nodeSize: { height: 10, width: 10 },
43 | position: { x: 10, y: 10 },
44 | },
45 | }),
46 | portActions.addPort('nodeId', 'portId', {
47 | data: {
48 | flowType: 'batch',
49 | },
50 | graphicalAttributes: {
51 | properties: {
52 | type: PORT_SOURCE,
53 | },
54 | },
55 | }),
56 | ],
57 | }),
58 | ).toMatchSnapshot();
59 | });
60 |
61 | it('should throw in reducer', () => {
62 | const shouldThrow = () =>
63 | reducer(defaultState, {
64 | type: 'FLOWDESIGNER.FLOW_ADD_ELEMENTS',
65 | listOfActionCreation: [
66 | nodeActions.addNode('nodeId', undefined, {
67 | data: {},
68 | graphicalAttributes: {
69 | nodeSize: { height: 10, width: 10 },
70 | position: { x: 10, y: 10 },
71 | },
72 | }),
73 | nodeActions.addNode('node2', undefined, {
74 | data: {},
75 | graphicalAttributes: {
76 | nodeSize: { height: 10, width: 10 },
77 | position: { x: 10, y: 10 },
78 | },
79 | }),
80 | portActions.addPort('node3', 'portId', {
81 | data: undefined,
82 | graphicalAttributes: undefined,
83 | }),
84 | ],
85 | });
86 | expect(shouldThrow).toThrow();
87 | });
88 | });
89 |
90 | describe('FLOWDESIGNER_FLOW_LOAD should reset old flow state and load news not touching flow config', () => {
91 | it('should load elements', () => {
92 | expect(
93 | reducer(defaultState, {
94 | type: 'FLOWDESIGNER.FLOW_LOAD',
95 | listOfActionCreation: [
96 | nodeActions.addNode('nodeId', undefined, {
97 | data: {},
98 | graphicalAttributes: {
99 | nodeSize: { height: 10, width: 10 },
100 | position: { x: 10, y: 10 },
101 | },
102 | }),
103 | nodeActions.addNode('node2', undefined, {
104 | data: {},
105 | graphicalAttributes: {
106 | nodeSize: { height: 10, width: 10 },
107 | position: { x: 10, y: 10 },
108 | },
109 | }),
110 | portActions.addPort('nodeId', 'portId', {
111 | graphicalAttributes: {
112 | properties: {
113 | type: PORT_SOURCE,
114 | },
115 | },
116 | }),
117 | ],
118 | }),
119 | ).toMatchSnapshot();
120 | });
121 | });
122 |
123 | describe('FLOWDESIGNER_PAN_TO set a calculated transformation into transformToApply', () => {
124 | it('', () => {
125 | expect(
126 | reducer(defaultState, {
127 | type: 'FLOWDESIGNER_PAN_TO',
128 | x: 400,
129 | y: 400,
130 | }),
131 | ).toMatchSnapshot();
132 | });
133 | });
134 |
135 | describe('calculatePortsPosition behavior', () => {
136 | const state = defaultState
137 | .set(
138 | 'nodes',
139 | Map().set(
140 | '42',
141 | new NodeRecord({
142 | id: '42',
143 | graphicalAttributes: Map({
144 | nodeType: '42',
145 | }),
146 | }),
147 | ),
148 | )
149 | .set(
150 | 'ports',
151 | Map().set(
152 | '42',
153 | new PortRecord({
154 | id: '42',
155 | nodeId: '42',
156 | }),
157 | ),
158 | )
159 | .set('nodeTypes', Map().set('42', Map().set('component', {})));
160 |
161 | it('should trigger only if NODE/PORT/FLOW action are dispatched', () => {
162 | const calculatePortPosition = jest.fn();
163 | const givenState = state.setIn(['nodeTypes', '42', 'component'], { calculatePortPosition });
164 | calculatePortsPosition(givenState, {
165 | type: 'FLOWDESIGNER_NODE_MOVE',
166 | });
167 | calculatePortsPosition(givenState, {
168 | type: 'FLOWDESIGNER_PORT_ADD',
169 | });
170 | calculatePortsPosition(givenState, {
171 | type: 'FLOWDESIGNER_FLOW_RESET',
172 | });
173 | expect(calculatePortPosition.mock.calls.length).toEqual(3);
174 | });
175 |
176 | it('should not trigger on FLOWDESIGNER_NODE_REMOVE and FLOWDESIGNER_PORT_REMOVE', () => {
177 | const calculatePortPosition = jest.fn();
178 | const givenState = state.setIn(['nodeTypes', '42', 'component'], { calculatePortPosition });
179 | calculatePortsPosition(givenState, {
180 | type: 'FLOWDESIGNER_NODE_REMOVE',
181 | });
182 | calculatePortsPosition(givenState, {
183 | type: 'FLOWDESIGNER_PORT_REMOVE',
184 | });
185 | expect(calculatePortPosition.mock.calls.length).toEqual(0);
186 | });
187 | });
188 |
--------------------------------------------------------------------------------
/jenkins/veracode-scan.groovy:
--------------------------------------------------------------------------------
1 | #!/usr/bin/groovy
2 | import groovy.transform.Field
3 |
4 | /**
5 | * Security scan pipeline for the react-flow-designer project
6 | */
7 |
8 | /**
9 | * Global variables
10 | */
11 | @Field def dockerArtifactoryDomain = "artifactory.datapwn.com"
12 | @Field def dockerRegistryDomain = "registry.datapwn.com"
13 |
14 | def slackChannel = 'pipeline-designer-notifications'
15 | def decodedJobName = env.JOB_NAME.replaceAll("%2F", "/")
16 | def projectName = 'react-flow-designer'
17 | def pipelineName = 'security-scan'
18 |
19 |
20 | /**
21 | * Pod configuration
22 | */
23 | def podLabel = "${projectName}-${pipelineName}-${UUID.randomUUID().toString()}".take(53)
24 | def podConfiguration = """
25 | apiVersion: v1
26 | kind: Pod
27 | spec:
28 | imagePullSecrets:
29 | - talend-registry
30 | containers:
31 | - name: nodejs
32 | image: artifactory.datapwn.com/tlnd-docker-prod/talend/common/tsbi/node-builder:2.5.4-20210112094055
33 | resources:
34 | requests:
35 | memory: "2G"
36 | cpu: "2"
37 | limits:
38 | memory: "8G"
39 | cpu: "4"
40 | command:
41 | - cat
42 | tty: true
43 | volumeMounts:
44 | - name: npm-cache
45 | mountPath: /root/.npm
46 | - name: docker
47 | mountPath: /var/run/docker.sock
48 |
49 | volumes:
50 | - name: docker
51 | hostPath:
52 | path: /var/run/docker.sock
53 | - name: npm-cache
54 | persistentVolumeClaim:
55 | claimName: efs-jenkins-dp-pipeline-designer-npm
56 | """
57 |
58 | /**
59 | * Credentials
60 | */
61 |
62 | def artifactoryCredentials = usernamePassword(
63 | credentialsId: 'artifactory-datapwn-credentials',
64 | passwordVariable: 'dockerArtifactoryPassword',
65 | usernameVariable: 'dockerArtifactoryUsername'
66 | )
67 |
68 | def nexusCredentials = usernamePassword(
69 | credentialsId: 'nexus-credentials',
70 | passwordVariable: 'nexusPassword',
71 | usernameVariable: 'nexusUsername'
72 | )
73 |
74 | def npmCredentials = string(
75 | credentialsId: 'npm-credentials',
76 | variable: 'npmAuthToken',
77 | )
78 |
79 | def dockerCredentials = usernamePassword(
80 | credentialsId: 'docker-registry-credentials',
81 | passwordVariable: 'dockerRegistryPassword',
82 | usernameVariable: 'dockerRegistryUsername'
83 | )
84 |
85 | /**
86 | * Options
87 | */
88 | def discarderConfiguration = logRotator(numToKeepStr: '30', artifactNumToKeepStr: '10')
89 | def timeoutConfiguration = [time: 45, unit: 'MINUTES']
90 |
91 | pipeline {
92 | agent {
93 | kubernetes {
94 | label podLabel
95 | yaml podConfiguration
96 | }
97 | }
98 |
99 | triggers {
100 | cron('0 11 * * 0')
101 | }
102 |
103 | options {
104 | buildDiscarder(discarderConfiguration)
105 | timeout(timeoutConfiguration)
106 | skipStagesAfterUnstable()
107 | }
108 |
109 | environment {
110 | branch = "${env.BRANCH_NAME}"
111 | commit = "${env.GIT_COMMIT}"
112 | timestamp = sh(returnStdout: true, script: "date +%Y%m%d_%H%M%S").trim()
113 | }
114 |
115 | stages {
116 | stage('Prepare Node JS Container') {
117 | steps {
118 | container('nodejs') {
119 | withCredentials([npmCredentials, dockerCredentials, artifactoryCredentials]) {
120 | script {
121 | sh """#! /bin/bash
122 | curl --silent --location https://dl.yarnpkg.com/rpm/yarn.repo | tee /etc/yum.repos.d/yarn.repo
123 | rpm --import https://dl.yarnpkg.com/rpm/pubkey.gpg
124 | yum --assumeyes install yarn
125 | echo -e "email=devops@talend.com\n//registry.npmjs.org/:_authToken=${npmAuthToken}" > /root/.npmrc
126 | docker login ${dockerRegistryDomain} -u ${dockerRegistryUsername} --password-stdin <<< ${dockerRegistryPassword}
127 | docker login ${dockerArtifactoryDomain} -u ${dockerArtifactoryUsername} --password-stdin <<< ${dockerArtifactoryPassword}
128 | """
129 | }
130 | }
131 | }
132 | }
133 | }
134 |
135 | stage('Source clear scan') {
136 | steps {
137 | container('nodejs') {
138 | withCredentials([string(credentialsId: 'veracode-token', variable: 'SRCCLR_API_TOKEN')]) {
139 | sh '''#!/bin/bash
140 | curl -sSL https://download.sourceclear.com/ci.sh | SRCCLR_API_TOKEN=${SRCCLR_API_TOKEN} DEBUG=0 sh -s -- scan --recursive --loud;
141 | '''
142 | }
143 | }
144 | }
145 | }
146 | }
147 |
148 | post {
149 | success {
150 | slackSend (color: 'good', channel: "${slackChannel}", message: "${decodedJobName} - #${env.BUILD_NUMBER} Success after ${currentBuild.durationString} (<${env.BUILD_URL}|Open>)")
151 | }
152 |
153 | unstable {
154 | slackSend (color: 'warning', channel: "${slackChannel}", message: "${decodedJobName} - #${env.BUILD_NUMBER} Unstable after ${currentBuild.durationString} (<${env.BUILD_URL}|Open>)")
155 | }
156 |
157 | failure {
158 | slackSend (color: 'danger', channel: "${slackChannel}", message: "${decodedJobName} - #${env.BUILD_NUMBER} Failure after ${currentBuild.durationString} (<${env.BUILD_URL}|Open>)")
159 | }
160 |
161 | aborted {
162 | slackSend (color: 'warning', channel: "${slackChannel}", message: "${decodedJobName} - #${env.BUILD_NUMBER} Aborted after ${currentBuild.durationString} (<${env.BUILD_URL}|Open>)")
163 | }
164 | }
165 | }
166 |
--------------------------------------------------------------------------------
/src/components/FlowDesigner.container.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { connect } from 'react-redux';
3 | import invariant from 'invariant';
4 | import get from 'lodash/get';
5 | import { Map } from 'immutable';
6 |
7 | import { setZoom } from '../actions/flow.actions';
8 | import Grid from './grid/Grid.component';
9 | import ZoomHandler from './ZoomHandler.component';
10 | import NodesRenderer from './node/NodesRenderer.component';
11 | import LinksRenderer from './link/LinksRenderer.component';
12 | import PortsRenderer from './port/PortsRenderer.component';
13 |
14 | import { startMoveNodeTo, moveNodeTo, moveNodeToEnd } from '../actions/node.actions';
15 | import { setNodeTypes } from '../actions/nodeType.actions';
16 | import {
17 | Transform,
18 | NodeRecordMap,
19 | PortRecordMap,
20 | LinkRecordMap,
21 | Position,
22 | Id,
23 | } from '../customTypings/index.d';
24 |
25 | type Props = {
26 | children?: any;
27 | setNodeTypes: (nodeTypeMap: Map) => void;
28 | startMoveNodeTo: (nodeId: Id, nodePosition: string) => void;
29 | moveNodeTo: (nodeId: Id, nodePosition: Position) => void;
30 | moveNodeToEnd: (nodeId: Id, nodePosition: Position) => void;
31 | nodes: NodeRecordMap;
32 | ports: PortRecordMap;
33 | links: LinkRecordMap;
34 | reduxMountPoint: string;
35 | onClick?: () => {};
36 | transform?: Transform;
37 | transformToApply?: Transform;
38 | setZoom?: (transform: Transform) => void;
39 | snapToGrid?: boolean;
40 | };
41 |
42 | type State = {
43 | nodeTypeMap: Map;
44 | linkTypeMap: Map;
45 | portTypeMap: Map;
46 | };
47 |
48 | export class FlowDesigner extends React.Component {
49 | node: any;
50 |
51 | static defaultProps = {
52 | snapToGrid: false,
53 | };
54 |
55 | constructor(props: Props) {
56 | super(props);
57 | this.state = {
58 | nodeTypeMap: Map(),
59 | linkTypeMap: Map(),
60 | portTypeMap: Map(),
61 | };
62 | }
63 |
64 | UNSAFE_componentWillMount() {
65 | const { children } = this.props;
66 | let nodeTypeMap = Map();
67 | let linkTypeMap = Map();
68 | let portTypeMap = Map();
69 | if (children) {
70 | (children as any).forEach(
71 | (element: {
72 | type: { displayName: string };
73 | props: { component: any; type: string };
74 | }) => {
75 | switch (element.type.displayName) {
76 | case 'NodeType':
77 | nodeTypeMap = {
78 | ...nodeTypeMap,
79 | [element.props.type]: {
80 | component: element.props.component,
81 | },
82 | };
83 | break;
84 | case 'LinkType':
85 | linkTypeMap = {
86 | ...linkTypeMap,
87 | [element.props.type]: {
88 | component: element.props.component,
89 | },
90 | };
91 | break;
92 | case 'PortType':
93 | portTypeMap = {
94 | ...portTypeMap,
95 | [element.props.type]: {
96 | component: element.props.component,
97 | },
98 | };
99 | break;
100 | default:
101 | invariant(
102 | false,
103 | `<${element.type.displayName} /> is an unknown component configuration`,
104 | );
105 | }
106 | },
107 | );
108 | } else {
109 | invariant(false, ' should have configuration component as child');
110 | }
111 |
112 | this.props.setNodeTypes(nodeTypeMap);
113 | this.setState({ nodeTypeMap, linkTypeMap, portTypeMap });
114 | }
115 |
116 | render() {
117 | return (
118 |
164 | );
165 | }
166 | }
167 |
168 | const mapStateToProps = (state: State, ownProps: Props) => ({
169 | nodes: get(state, ownProps.reduxMountPoint).get('nodes'),
170 | links: get(state, ownProps.reduxMountPoint).get('links'),
171 | ports: get(state, ownProps.reduxMountPoint).get('ports'),
172 | transform: get(state, ownProps.reduxMountPoint).get('transform'),
173 | transformToApply: get(state, ownProps.reduxMountPoint).get('transformToApply'),
174 | });
175 |
176 | const mapDispatchToProps = (dispatch: any) => ({
177 | setNodeTypes: (nodeTypeMap: Map) => dispatch(setNodeTypes(nodeTypeMap)),
178 | startMoveNodeTo: (nodeId: Id, nodePosition: string) =>
179 | dispatch(startMoveNodeTo(nodeId, nodePosition)),
180 | moveNodeTo: (nodeId: Id, nodePosition: Position) => dispatch(moveNodeTo(nodeId, nodePosition)),
181 | moveNodeToEnd: (nodeId: Id, nodePosition: Position) =>
182 | dispatch(moveNodeToEnd(nodeId, nodePosition)),
183 | setZoom: (transform: Transform) => dispatch(setZoom(transform)),
184 | });
185 |
186 | const connector: any = connect(mapStateToProps, mapDispatchToProps);
187 |
188 | export default connector(FlowDesigner);
189 |
--------------------------------------------------------------------------------
/src/api/data/data.test.ts:
--------------------------------------------------------------------------------
1 | import Immutable from 'immutable';
2 |
3 | import * as Data from './data';
4 |
5 | export const isNotMapException = `Immutable.Map should be a Immutable.Map, was given
6 | """
7 | object
8 | """
9 | [object Map]
10 | """
11 | `;
12 | export const isNotKeyException = 'key should be a string, was given 8 of type number';
13 |
14 | describe('isMapElseThrow', () => {
15 | it('return true if parameter is an Map', () => {
16 | // given
17 | const testMap = Immutable.Map();
18 | // when
19 | const test = Data.isMapElseThrow(testMap);
20 | // expect
21 | expect(test).toEqual(true);
22 | });
23 |
24 | it('throw an error if parameter is not an Map', () => {
25 | // given
26 | const testMap = new Map();
27 | // when
28 | // expect
29 | expect(() => Data.isMapElseThrow(testMap as any)).toThrow(isNotMapException);
30 | });
31 | });
32 |
33 | describe('isKeyElseThrow', () => {
34 | it('return true if parameter key is a String', () => {
35 | // given
36 | const testString = 'a String';
37 | // when
38 | const test = Data.isKeyElseThrow(testString);
39 | // expect
40 | expect(test).toEqual(true);
41 | });
42 |
43 | it('throw an error if parameter is not a String', () => {
44 | // given
45 | const testString = 8;
46 | // when
47 | // expect
48 | expect(() => Data.isKeyElseThrow(testString)).toThrow(isNotKeyException);
49 | });
50 | });
51 |
52 | describe('Data', () => {
53 | describe('set', () => {
54 | it('given a proper key and map update said map', () => {
55 | // given
56 | const key = 'key';
57 | const value = 'value';
58 | const map = Immutable.Map({
59 | withValue: 'value',
60 | });
61 | // when
62 | const test = Data.set(key, value, map);
63 | // expect
64 | expect(test.get(key)).toEqual(value);
65 | });
66 |
67 | it('given an improper key throw', () => {
68 | // given
69 | const key = 8;
70 | const value = 'value';
71 | const map = Immutable.Map({
72 | withValue: 'value',
73 | });
74 | // when
75 | // expect
76 | expect(() => Data.set(key, value, map)).toThrow(isNotKeyException);
77 | });
78 |
79 | it('given an improper map throw', () => {
80 | // given
81 | const key = 'key';
82 | const value = 'value';
83 | const map = new Map();
84 | // when
85 | // expect
86 | expect(() => Data.set(key, value, map as any)).toThrow(isNotMapException);
87 | });
88 | });
89 |
90 | describe('get', () => {
91 | it('given a key and map containing said key return value', () => {
92 | // given
93 | const key = 'key';
94 | const value = 'value';
95 | const map = Immutable.Map({
96 | key: value,
97 | });
98 | // when
99 | const test = Data.get(key, map);
100 | // expect
101 | expect(test).toEqual(value);
102 | });
103 |
104 | it('given a key and map not containing said key return undefined', () => {
105 | // given
106 | const key = 'anotherKey';
107 | const value = 'value';
108 | const map = Immutable.Map({
109 | key: value,
110 | });
111 | // when
112 | const test = Data.get(key, map);
113 | // expect
114 | expect(test).toEqual(undefined);
115 | });
116 |
117 | it('given an improper key throw', () => {
118 | // given
119 | const key = 8;
120 | const map = Immutable.Map({
121 | withValue: 'value',
122 | });
123 | // when
124 | // expect
125 | expect(() => Data.get(key, map)).toThrow(isNotKeyException);
126 | });
127 |
128 | it('given an improper map throw', () => {
129 | // given
130 | const key = 'key';
131 | const map = new Map();
132 | // when
133 | // expect
134 | expect(() => Data.get(key, map as any)).toThrow(isNotMapException);
135 | });
136 | });
137 |
138 | describe('has', () => {
139 | it('given a key and map containing said key return true', () => {
140 | // given
141 | const key = 'key';
142 | const value = 'value';
143 | const map = Immutable.Map({
144 | key: value,
145 | });
146 | // when
147 | const test = Data.has(key, map);
148 | // expect
149 | expect(test).toEqual(true);
150 | });
151 |
152 | it('given a key and map not containing said key return false', () => {
153 | // given
154 | const key = 'anotherKey';
155 | const value = 'value';
156 | const map = Immutable.Map({
157 | key: value,
158 | });
159 | // when
160 | const test = Data.has(key, map);
161 | // expect
162 | expect(test).toEqual(false);
163 | });
164 |
165 | it('given an improper key throw', () => {
166 | // given
167 | const key = 8;
168 | const map = Immutable.Map({
169 | withValue: 'value',
170 | });
171 | // when
172 | // expect
173 | expect(() => Data.has(key, map)).toThrow(isNotKeyException);
174 | });
175 |
176 | it('given an improper map throw', () => {
177 | // given
178 | const key = 'key';
179 | const map = new Map();
180 | // when
181 | // expect
182 | expect(() => Data.has(key, map as any)).toThrow(isNotMapException);
183 | });
184 | });
185 |
186 | describe('delete', () => {
187 | it('given a key and map containing said key return map without this value', () => {
188 | // given
189 | const key = 'key';
190 | const value = 'value';
191 | const map = Immutable.Map({
192 | key: value,
193 | });
194 | // when
195 | const test = Data.deleteKey(key, map);
196 | // expect
197 | expect(test).toEqual(Immutable.Map());
198 | });
199 |
200 | it('given a key and map not containing said key return same map', () => {
201 | // given
202 | const key = 'anotherKey';
203 | const value = 'value';
204 | const map = Immutable.Map({
205 | key: value,
206 | });
207 | // when
208 | const test = Data.deleteKey(key, map);
209 | // expect
210 | expect(test).toEqual(map);
211 | });
212 |
213 | it('given an improper key throw', () => {
214 | // given
215 | const key = 8;
216 | const map = Immutable.Map({
217 | withValue: 'value',
218 | });
219 | // when
220 | // expect
221 | expect(() => Data.deleteKey(key, map)).toThrow(isNotKeyException);
222 | });
223 |
224 | it('given an improper map throw', () => {
225 | // given
226 | const key = 'key';
227 | const map = new Map();
228 | // when
229 | // expect
230 | expect(() => Data.deleteKey(key, map as any)).toThrow(isNotMapException);
231 | });
232 | });
233 | });
234 |
--------------------------------------------------------------------------------
/src/reducers/node.reducer.test.ts:
--------------------------------------------------------------------------------
1 | import { Map } from 'immutable';
2 |
3 | import { defaultState } from './flow.reducer';
4 | import nodeReducer from './node.reducer';
5 | import {
6 | NodeRecord,
7 | PositionRecord,
8 | NodeGraphicalAttributes,
9 | } from '../constants/flowdesigner.model';
10 | import {
11 | FLOWDESIGNER_NODE_SET_TYPE,
12 | FLOWDESIGNER_NODE_MOVE_START,
13 | FLOWDESIGNER_NODE_MOVE,
14 | FLOWDESIGNER_NODE_MOVE_END,
15 | } from '../constants/flowdesigner.constants';
16 |
17 | describe('Check node reducer', () => {
18 | const initialState = defaultState
19 | .setIn(
20 | ['nodes', 'id1'],
21 | new NodeRecord({
22 | id: 'id1',
23 | type: 'type1',
24 | data: Map({ type: 'test' }),
25 | graphicalAttributes: new NodeGraphicalAttributes({
26 | type: 'type1',
27 | selected: true,
28 | position: new PositionRecord({ x: 10, y: 10 }),
29 | }),
30 | }),
31 | )
32 | .setIn(
33 | ['nodes', 'id2'],
34 | new NodeRecord({
35 | id: 'id2',
36 | type: 'type2',
37 | data: Map({ type: 'test' }),
38 | graphicalAttributes: new NodeGraphicalAttributes({
39 | type: 'type2',
40 | selected: false,
41 | position: new PositionRecord({ x: 10, y: 10 }),
42 | }),
43 | }),
44 | );
45 |
46 | it('FLOWDESIGNER_NODE_ADD properly add a new node to the node collection', () => {
47 | expect(
48 | nodeReducer(defaultState, {
49 | type: 'FLOWDESIGNER_NODE_ADD',
50 | nodeId: 'id',
51 | graphicalAttributes: {
52 | position: { x: 10, y: 10 },
53 | },
54 | }),
55 | ).toMatchSnapshot();
56 | });
57 |
58 | it('FLOWDESIGNER_NODE_ADD add a new node to the node collection with the right type', () => {
59 | expect(
60 | nodeReducer(defaultState, {
61 | type: 'FLOWDESIGNER_NODE_ADD',
62 | nodeId: 'id',
63 | graphicalAttributes: {
64 | name: 'test',
65 | nodePosition: { x: 10, y: 10 },
66 | type: 'MY_NODE_TYPE',
67 | },
68 | }),
69 | ).toMatchSnapshot();
70 | });
71 |
72 | it('Create a startPosition when receiving a FLOWDESIGNER_NODE_MOVE_START command', () => {
73 | expect(
74 | nodeReducer(initialState, {
75 | type: FLOWDESIGNER_NODE_MOVE_START,
76 | nodeId: 'id1',
77 | nodePosition: { x: 50, y: 50 },
78 | })
79 | .getIn(['nodes', 'id1', 'graphicalAttributes', 'properties', 'startPosition'])
80 | .toJS(),
81 | ).toEqual({ x: 50, y: 50 });
82 | });
83 |
84 | it('FLOWDESIGNER_NODE_MOVE update node position', () => {
85 | expect(
86 | nodeReducer(initialState, {
87 | type: FLOWDESIGNER_NODE_MOVE,
88 | nodeId: 'id2',
89 | nodePosition: { x: 50, y: 50 },
90 | }),
91 | ).toMatchSnapshot();
92 | });
93 |
94 | it('empty the startPostion when receiving a FLOW_DESIGNER_MOVE_END command', () => {
95 | expect(
96 | nodeReducer(initialState, {
97 | type: FLOWDESIGNER_NODE_MOVE_END,
98 | nodeId: 'id1',
99 | nodePosition: { x: 50, y: 50 },
100 | }).getIn(['nodes', 'id1', 'graphicalAttributes', 'properties', 'startPosition']),
101 | ).toEqual(undefined);
102 | });
103 |
104 | it('FLOWDESIGNER_NODE_SET_SIZE update node size property', () => {
105 | expect(
106 | nodeReducer(initialState, {
107 | type: 'FLOWDESIGNER_NODE_SET_SIZE',
108 | nodeId: 'id1',
109 | nodeSize: { height: 200, width: 200 },
110 | }),
111 | ).toMatchSnapshot();
112 | });
113 |
114 | it('FLOWDESIGNER_NODE_SET_TYPE update node type', () => {
115 | const nodeId = 'id1';
116 | const nodeType = 'nodetype';
117 |
118 | expect(
119 | nodeReducer(initialState, {
120 | type: FLOWDESIGNER_NODE_SET_TYPE,
121 | nodeId,
122 | nodeType,
123 | }),
124 | ).toMatchSnapshot();
125 | });
126 |
127 | it('FLOWDESIGNER_NODE_SET_GRAPHICAL_ATTRIBUTES should add { selected: false } attribute to node graphicalAttributes map', () => {
128 | expect(
129 | nodeReducer(initialState, {
130 | type: 'FLOWDESIGNER_NODE_SET_GRAPHICAL_ATTRIBUTES',
131 | nodeId: 'id1',
132 | graphicalAttributes: { selected: false },
133 | }),
134 | ).toMatchSnapshot();
135 | });
136 |
137 | it('FLOWDESIGNER_NODE_REMOVE_GRAPHICAL_ATTRIBUTES should remove {selected} attribute to node graphicalAttributes map', () => {
138 | expect(
139 | nodeReducer(initialState, {
140 | type: 'FLOWDESIGNER_NODE_REMOVE_GRAPHICAL_ATTRIBUTES',
141 | nodeId: 'id1',
142 | graphicalAttributesKey: 'selected',
143 | }),
144 | ).toMatchSnapshot();
145 | });
146 |
147 | it("FLOWDESIGNER_NODE_SET_DATA should add { type: 'string' } attribute to node data map", () => {
148 | expect(
149 | nodeReducer(initialState, {
150 | type: 'FLOWDESIGNER_NODE_SET_DATA',
151 | nodeId: 'id1',
152 | data: { type: 'string' },
153 | }),
154 | ).toMatchSnapshot();
155 | });
156 |
157 | it('FLOWDESIGNER_NODE_REMOVE_DATA should remove {type} attribute to node data map', () => {
158 | expect(
159 | nodeReducer(initialState, {
160 | type: 'FLOWDESIGNER_NODE_REMOVE_DATA',
161 | nodeId: 'id1',
162 | data: 'type',
163 | }),
164 | ).toMatchSnapshot();
165 | });
166 |
167 | it('FLOWDESIGNER_NODE_REMOVE should remove node from node collection', () => {
168 | expect(
169 | nodeReducer(initialState, {
170 | type: 'FLOWDESIGNER_NODE_REMOVE',
171 | nodeId: 'id1',
172 | }),
173 | ).toMatchSnapshot();
174 | });
175 | });
176 |
177 | describe('FLOWDESIGNER_NODE_APPLY_MOVEMENT', () => {
178 | const initialState = defaultState
179 | .setIn(
180 | ['nodes', 'id1'],
181 | new NodeRecord({
182 | id: 'id1',
183 | nodeType: 'type1',
184 | graphicalAttributes: new NodeGraphicalAttributes({
185 | position: new PositionRecord({ x: 10, y: 10 }),
186 | }),
187 | }),
188 | )
189 | .setIn(
190 | ['nodes', 'id2'],
191 | new NodeRecord({
192 | id: 'id2',
193 | nodeType: 'type2',
194 | graphicalAttributes: new NodeGraphicalAttributes({
195 | position: new PositionRecord({ x: 10, y: 10 }),
196 | }),
197 | }),
198 | )
199 | .setIn(
200 | ['nodes', 'id3'],
201 | new NodeRecord({
202 | id: 'id3',
203 | nodeType: 'type2',
204 | graphicalAttributes: new NodeGraphicalAttributes({
205 | position: new PositionRecord({ x: 10, y: 10 }),
206 | }),
207 | }),
208 | );
209 | it('should apply the same relative movement to each node listed', () => {
210 | expect(
211 | nodeReducer(initialState, {
212 | type: 'FLOWDESIGNER_NODE_APPLY_MOVEMENT',
213 | nodesId: ['id1', 'id2'],
214 | movement: { x: 10, y: 5 },
215 | }),
216 | ).toMatchSnapshot();
217 | });
218 | });
219 |
--------------------------------------------------------------------------------
/src/components/node/AbstractNode.component.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { scaleLinear, drag, select } from 'd3';
3 | import { Map } from 'immutable';
4 |
5 | import invariant from 'invariant';
6 |
7 | import { Node, Port, Position, Size } from '../../api';
8 | import { GRID_SIZE, PORT_SINK, PORT_SOURCE } from '../../constants/flowdesigner.constants';
9 | import {
10 | PortRecordMap,
11 | Position as PositionType,
12 | Size as SizeType,
13 | NodeRecord,
14 | Id,
15 | } from '../../customTypings/index.d';
16 |
17 | export const ABSTRACT_NODE_INVARIANT = ` should not be used without giving it a children
18 | ex: `;
19 |
20 | /**
21 | * calculate the position of each ports for a given node information
22 | * @param ports
23 | * @param nodePosition
24 | * @param nodeSize
25 | */
26 | function calculatePortPosition(
27 | ports: PortRecordMap,
28 | nodePosition: PositionType,
29 | nodeSize: SizeType,
30 | ) {
31 | let portsWithPosition = Map();
32 | const emitterPorts = ports.filter(port => Port.getTopology(port) === PORT_SOURCE);
33 | const sinkPorts = ports.filter(port => Port.getTopology(port) === PORT_SINK);
34 | const range = [
35 | Position.getYCoordinate(nodePosition),
36 | Position.getYCoordinate(nodePosition) + Size.getHeight(nodeSize),
37 | ];
38 | const scaleYEmitter = scaleLinear()
39 | .domain([0, emitterPorts.size + 1])
40 | .range(range);
41 | const scaleYSink = scaleLinear()
42 | .domain([0, sinkPorts.size + 1])
43 | .range(range);
44 | let emitterNumber = 0;
45 | let sinkNumber = 0;
46 | emitterPorts
47 | .sort((a, b) => {
48 | if (Port.getIndex(a) < Port.getIndex(b)) {
49 | return -1;
50 | }
51 | if (Port.getIndex(a) > Port.getIndex(b)) {
52 | return 1;
53 | }
54 | return 0;
55 | })
56 | .forEach(port => {
57 | emitterNumber += 1;
58 |
59 | const position = Position.create(
60 | Position.getXCoordinate(nodePosition) + Size.getWidth(nodeSize),
61 | scaleYEmitter(emitterNumber),
62 | );
63 | portsWithPosition = portsWithPosition.set(
64 | Port.getId(port),
65 | Port.setPosition(port, position),
66 | );
67 | });
68 | sinkPorts
69 | .sort((a, b) => {
70 | if (Port.getIndex(a) < Port.getIndex(b)) {
71 | return -1;
72 | }
73 | if (Port.getIndex(a) > Port.getIndex(b)) {
74 | return 1;
75 | }
76 | return 0;
77 | })
78 | .forEach(port => {
79 | sinkNumber += 1;
80 | const position = Position.create(
81 | Position.getXCoordinate(nodePosition),
82 | scaleYSink(sinkNumber),
83 | );
84 | portsWithPosition = portsWithPosition.set(
85 | Port.getId(port),
86 | Port.setPosition(port, position),
87 | );
88 | });
89 | return portsWithPosition;
90 | }
91 |
92 | type Props = {
93 | node: NodeRecord;
94 | startMoveNodeTo: (nodeId: Id, nodePosition: PositionType) => void;
95 | moveNodeTo: (nodeId: Id, nodePosition: PositionType) => void;
96 | moveNodeToEnd: (nodeId: Id, nodePosition: PositionType) => void;
97 | snapToGrid?: boolean;
98 | onDragStart?: (event: any) => void;
99 | onDrag?: (event: any) => void;
100 | onDragEnd?: (event: any) => void;
101 | onClick?: React.MouseEventHandler;
102 | children?: any;
103 | };
104 |
105 | class AbstractNode extends React.Component {
106 | static calculatePortPosition = calculatePortPosition;
107 |
108 | d3Node: any;
109 |
110 | nodeElement: any;
111 |
112 | squaredDeltaDrag: number = 0;
113 |
114 | constructor(props: Props) {
115 | super(props);
116 | this.onClick = this.onClick.bind(this);
117 | this.onDragStart = this.onDragStart.bind(this);
118 | this.onDrag = this.onDrag.bind(this);
119 | this.onDragEnd = this.onDragEnd.bind(this);
120 | this.renderContent = this.renderContent.bind(this);
121 | this.getEventPosition = this.getEventPosition.bind(this);
122 | }
123 |
124 | componentDidMount() {
125 | this.d3Node = select(this.nodeElement);
126 | this.d3Node.data([this.props.node.getPosition()]);
127 | this.d3Node.call(
128 | drag().on('start', this.onDragStart).on('drag', this.onDrag).on('end', this.onDragEnd),
129 | );
130 | }
131 |
132 | UNSAFE_componentWillReceiveProps(nextProps: Props) {
133 | const nextPosition = Node.getPosition(nextProps.node);
134 | if (nextPosition !== Node.getPosition(this.props.node)) {
135 | this.d3Node.data([nextPosition]);
136 | }
137 | }
138 |
139 | shouldComponentUpdate(nextProps: Props) {
140 | return nextProps !== this.props;
141 | }
142 |
143 | componentWillUnmount() {
144 | this.d3Node.remove();
145 | }
146 |
147 | onClick(clickEvent: React.MouseEvent) {
148 | if (this.props.onClick) {
149 | this.props.onClick(clickEvent);
150 | }
151 | }
152 |
153 | onDragStart(event: any) {
154 | this.squaredDeltaDrag = 0;
155 | const position = {
156 | x: event.x,
157 | y: event.y,
158 | };
159 | this.props.startMoveNodeTo(this.props.node.id, position);
160 | if (this.props.onDragStart) {
161 | this.props.onDragStart(event);
162 | }
163 | }
164 |
165 | onDrag(event: any) {
166 | this.squaredDeltaDrag += event.dx * event.dx + event.dy * event.dy;
167 | const position = {
168 | x: event.x,
169 | y: event.y,
170 | movementX: event.sourceEvent.movementX,
171 | movementY: event.sourceEvent.movementY,
172 | };
173 | this.props.moveNodeTo(this.props.node.id, position);
174 | if (this.props.onDrag) {
175 | this.props.onDrag(position);
176 | }
177 | }
178 |
179 | onDragEnd(event: any) {
180 | // Ok this is pretty specific
181 | // for a chrome windows bug
182 | // where d3 inhibit onCLick propagation
183 | // if there is any delta between down and up of the mouse
184 | // here we add a tolerance, so the underlying click doesn't
185 | // get smooshed if the user do not initiate drag
186 | if (this.squaredDeltaDrag < 1) {
187 | select(window).on('click.drag', null);
188 | }
189 | const position = this.getEventPosition(event);
190 | this.props.moveNodeToEnd(this.props.node.id, position);
191 | this.d3Node.data([position]);
192 | if (this.props.onDragEnd) {
193 | this.props.onDragEnd(position);
194 | }
195 | }
196 |
197 | getEventPosition(event: any) {
198 | if (this.props.snapToGrid) {
199 | return {
200 | x: event.x - (event.x % GRID_SIZE),
201 | y: event.y - (event.y % GRID_SIZE),
202 | };
203 | }
204 | return { x: event.x, y: event.y };
205 | }
206 |
207 | renderContent() {
208 | if (this.props.children) {
209 | return this.props.children;
210 | }
211 | invariant(false, ABSTRACT_NODE_INVARIANT);
212 | return null;
213 | }
214 |
215 | render() {
216 | const { node } = this.props;
217 | const { x, y } = Node.getPosition(node);
218 | const transform = `translate(${x}, ${y})`;
219 | return (
220 |
221 | {
224 | this.nodeElement = c;
225 | }}
226 | onClick={this.onClick}
227 | >
228 | {this.renderContent()}
229 |
230 |
231 | );
232 | }
233 | }
234 |
235 | export default AbstractNode;
236 |
--------------------------------------------------------------------------------
/src/reducers/flow.reducer.ts:
--------------------------------------------------------------------------------
1 | import { Map } from 'immutable';
2 | import { zoomIdentity } from 'd3';
3 | import {
4 | FLOWDESIGNER_FLOW_ADD_ELEMENTS,
5 | FLOWDESIGNER_FLOW_RESET,
6 | FLOWDESIGNER_FLOW_LOAD,
7 | FLOWDESIGNER_FLOW_SET_ZOOM,
8 | FLOWDESIGNER_FLOW_ZOOM_IN,
9 | FLOWDESIGNER_FLOW_ZOOM_OUT,
10 | FLOWDESIGNER_PAN_TO,
11 | FLOWDESIGNER_NODETYPE_SET,
12 | } from '../constants/flowdesigner.constants';
13 | import nodesReducer from './node.reducer';
14 | import linksReducer from './link.reducer';
15 | import portsReducer from './port.reducer';
16 | import nodeTypeReducer from './nodeType.reducer';
17 | import { State, NodeRecord, Id, LinkRecord, PortRecord } from '../customTypings/index.d';
18 |
19 | export const defaultState: Partial = Map({
20 | nodes: Map(),
21 | links: Map(),
22 | ports: Map(),
23 | out: Map>(),
24 | in: Map>(),
25 | childrens: Map>(),
26 | parents: Map>(),
27 | nodeTypes: Map(),
28 | transform: { k: 1, x: 0, y: 0 },
29 | transformToApply: undefined,
30 | });
31 |
32 | function combinedReducer(state = defaultState, action: any) {
33 | return [nodesReducer, linksReducer, portsReducer, nodeTypeReducer].reduce(
34 | (cumulatedState, subReducer) => subReducer(cumulatedState, action),
35 | state,
36 | );
37 | }
38 |
39 | enum ZoomDirection {
40 | IN = 'IN',
41 | OUT = 'OUT',
42 | }
43 |
44 | const DEFAULT_ZOOM_SCALE_STEP: number = 0.1;
45 |
46 | /**
47 | * Return the new zoom value based on parameters.
48 | * @param currentZoom Current zoom value
49 | * @param zoomDirection Indicate if we want to zoom in or out
50 | * @param step The zoom change to apply
51 | * @returns The new zoom value, rounded to be a multiple of step value
52 | * @example
53 | * A zoom at 100% has a currentZoom value of 1
54 | * If you want to zoom by step of 25%, step value is 0.25
55 | * Example 1:
56 | * You are at 125%, you zoom in. We want to zoom to 150%.
57 | * We have (1.25 + 0.25) / 0.25 = 6, no rounding, so then 6 * 0.25 = 1.5
58 | * So the new zoom value is 1.5, which is 150%, that what we want.
59 | * Example 2:
60 | * You had zoomed with the mouse wheel (do not use steps) at 136%, and you zoom in.
61 | * We want to go to the next step, so we want to zoom to 150%.
62 | * We have (1.36 + 0.25) = 1.61, it should means a zoom at 161%
63 | * So the goal is to round by step, to do that we have to check how many step are in the new zoom value: 1.61 / 0.25 = 6.44
64 | * Because we are zooming in, we want to round down the value, in order to stop at the first step encountered: Math.floor(6.44) = 6
65 | * Now we can multiply by the step to retrieve to correct value: 6 * 0.25 = 1.5
66 | * Bingo, we have the right value and zoom to 150%
67 | */
68 | function calculateZoomScale(
69 | currentZoom: number,
70 | zoomDirection: ZoomDirection,
71 | step: number,
72 | ): number {
73 | let zoomValue;
74 | if (zoomDirection === ZoomDirection.IN) {
75 | zoomValue = Math.floor((currentZoom + step) / step) * step;
76 | } else {
77 | zoomValue = Math.ceil((currentZoom - step) / step) * step;
78 | }
79 |
80 | // If the new zoom should be 0, we set a zoom at 1%.
81 | if (zoomValue < step) zoomValue = step;
82 |
83 | return zoomValue;
84 | }
85 |
86 | export function reducer(state: State, action: any) {
87 | switch (action.type) {
88 | case FLOWDESIGNER_FLOW_ADD_ELEMENTS:
89 | return action.listOfActionCreation.reduce(
90 | (cumulativeState: State, actionCreation: any) =>
91 | combinedReducer(cumulativeState, actionCreation),
92 | state,
93 | );
94 | case FLOWDESIGNER_FLOW_RESET:
95 | return defaultState.set('nodeTypes', state.get('nodeTypes'));
96 | case FLOWDESIGNER_FLOW_LOAD:
97 | return action.listOfActionCreation.reduce(
98 | (cumulativeState: State, actionCreation: any) =>
99 | combinedReducer(cumulativeState, actionCreation),
100 | defaultState.set('nodeTypes', state.get('nodeTypes')),
101 | );
102 | case FLOWDESIGNER_FLOW_SET_ZOOM:
103 | return state.set('transform', action.transform);
104 | case FLOWDESIGNER_FLOW_ZOOM_IN:
105 | return state.set(
106 | 'transformToApply',
107 | zoomIdentity
108 | .translate(state.get('transform').x, state.get('transform').y)
109 | .scale(
110 | calculateZoomScale(
111 | state.get('transform').k,
112 | ZoomDirection.IN,
113 | action.scale || DEFAULT_ZOOM_SCALE_STEP,
114 | ),
115 | ),
116 | );
117 | case FLOWDESIGNER_FLOW_ZOOM_OUT:
118 | return state.set(
119 | 'transformToApply',
120 | zoomIdentity
121 | .translate(state.get('transform').x, state.get('transform').y)
122 | .scale(
123 | calculateZoomScale(
124 | state.get('transform').k,
125 | ZoomDirection.OUT,
126 | action.scale || DEFAULT_ZOOM_SCALE_STEP,
127 | ),
128 | ),
129 | );
130 | case FLOWDESIGNER_PAN_TO:
131 | return state.update('transformToApply', () =>
132 | zoomIdentity
133 | .translate(state.get('transform').x, state.get('transform').y)
134 | .scale(state.get('transform').k)
135 | .scale(1 / state.get('transform').k)
136 | .translate(
137 | -(state.get('transform').x + action.x),
138 | -(state.get('transform').y + action.y),
139 | ),
140 | );
141 | default:
142 | return combinedReducer(state, action);
143 | }
144 | }
145 |
146 | /**
147 | * Calculate port position with the methods provided by port parent node
148 | * calcul is done only if node moved or list of attached port have its size changed
149 | * also update position if registered nodetype change
150 | * because the node hold the function used to calculate position of their attached port
151 | * Beware could be slow if the calculus methode provided is slow
152 | * @params {object} state react-flow-designer state
153 | * @params {object} action
154 | *
155 | * @return {object} new state
156 | */
157 | export function calculatePortsPosition(state: State, action: any) {
158 | let nodes = [];
159 | // TODO: NOT a big fan of this way to optimize port recalculations, don't feel future proof
160 | if (
161 | (/FLOWDESIGNER_NODE_/.exec(action.type) && action.type !== 'FLOWDESIGNER_NODE_REMOVE') ||
162 | (/FLOWDESIGNER_PORT_/.exec(action.type) && action.type !== 'FLOWDESIGNER_PORT_REMOVE') ||
163 | /FLOWDESIGNER.FLOW_/.exec(action.type) ||
164 | action.type === FLOWDESIGNER_NODETYPE_SET
165 | ) {
166 | if (action.nodeId) {
167 | nodes.push(state.getIn(['nodes', action.nodeId]));
168 | } else if (action.portId) {
169 | nodes.push(state.getIn(['nodes'], state.getIn(['ports', action.portId]).nodeId));
170 | } else {
171 | nodes = state.get('nodes');
172 | }
173 | return nodes.reduce((cumulativeState: State, node: NodeRecord) => {
174 | const nodeType = node.getNodeType();
175 | const ports = state.get('ports').filter((port: PortRecord) => port.nodeId === node.id);
176 | const component = state.getIn(['nodeTypes', nodeType, 'component']);
177 | if (component) {
178 | const calculatePortPosition = component.calculatePortPosition;
179 | if (calculatePortPosition) {
180 | return cumulativeState.mergeIn(
181 | ['ports'],
182 | calculatePortPosition(ports, node.getPosition(), node.getSize()),
183 | );
184 | }
185 | }
186 | return state;
187 | }, state);
188 | }
189 | return state;
190 | }
191 |
192 | function flowDesignerReducer(state: State, action: any) {
193 | let newState = reducer(state, action);
194 | newState = calculatePortsPosition(newState, action);
195 | return newState;
196 | }
197 |
198 | export default flowDesignerReducer;
199 |
--------------------------------------------------------------------------------
/src/selectors/nodeSelectors.test.ts:
--------------------------------------------------------------------------------
1 | import { List, Map, OrderedMap } from 'immutable';
2 | import * as Selectors from './nodeSelectors';
3 | import {
4 | NodeRecord,
5 | NestedNodeRecord,
6 | PortRecord,
7 | LinkRecord,
8 | } from '../constants/flowdesigner.model';
9 | import {
10 | State,
11 | NodeRecord as NodeRecordType,
12 | NestedNodeRecord as NestedNodeRecordType,
13 | PortRecord as PortRecordType,
14 | LinkRecord as LinkRecordType,
15 | Id,
16 | } from '../customTypings/index.d';
17 |
18 | describe('Testing node selectors', () => {
19 | const node1 = new NodeRecord({
20 | id: 'id1',
21 | });
22 |
23 | const node2 = new NodeRecord({
24 | id: 'id2',
25 | });
26 |
27 | const node3 = new NodeRecord({
28 | id: 'id3',
29 | });
30 |
31 | const node4 = new NodeRecord({
32 | id: 'id4',
33 | });
34 |
35 | const node5 = new NodeRecord({
36 | id: 'id5',
37 | });
38 |
39 | const node6 = new NodeRecord({
40 | id: 'id6',
41 | });
42 |
43 | const node7 = new NodeRecord({
44 | id: 'id7',
45 | });
46 |
47 | const node8 = new NodeRecord({
48 | id: 'id8',
49 | });
50 |
51 | const port1 = new PortRecord({
52 | id: 'port1',
53 | nodeId: 'id1',
54 | });
55 |
56 | const port2 = new PortRecord({
57 | id: 'port2',
58 | nodeId: 'id2',
59 | });
60 |
61 | const port3 = new PortRecord({
62 | id: 'port3',
63 | nodeId: 'id2',
64 | });
65 |
66 | const port4 = new PortRecord({
67 | id: 'id4',
68 | nodeId: 'id2',
69 | });
70 |
71 | const port5 = new PortRecord({
72 | id: 'id5',
73 | nodeId: 'id3',
74 | });
75 |
76 | const port6 = new PortRecord({
77 | id: 'id6',
78 | nodeId: 'id3',
79 | });
80 |
81 | const port7 = new PortRecord({
82 | id: 'id7',
83 | nodeId: 'id4',
84 | });
85 |
86 | const port8 = new PortRecord({
87 | id: 'id8',
88 | nodeId: 'id4',
89 | });
90 |
91 | const port9 = new PortRecord({
92 | id: 'id9',
93 | nodeId: 'id5',
94 | });
95 |
96 | const port10 = new PortRecord({
97 | id: 'id10',
98 | nodeId: 'id5',
99 | });
100 |
101 | const port11 = new PortRecord({
102 | id: 'id11',
103 | nodeId: 'id6',
104 | });
105 |
106 | const port12 = new PortRecord({
107 | id: 'id12',
108 | nodeId: 'id6',
109 | });
110 |
111 | const port13 = new PortRecord({
112 | id: 'id13',
113 | nodeId: 'id7',
114 | });
115 |
116 | const port14 = new PortRecord({
117 | id: 'id14',
118 | nodeId: 'id7',
119 | });
120 |
121 | const port15 = new PortRecord({
122 | id: 'id15',
123 | nodeId: 'id7',
124 | });
125 |
126 | const port16 = new PortRecord({
127 | id: 'id16',
128 | nodeId: 'id8',
129 | });
130 |
131 | const link1 = new LinkRecord({
132 | id: 'id1',
133 | sourceId: 'id1',
134 | targetId: 'id2',
135 | graphicalAttributes: Map().set('attr', 'attr'),
136 | });
137 |
138 | const link2 = new LinkRecord({
139 | id: 'id2',
140 | sourceId: 'id3',
141 | targetId: 'id5',
142 | graphicalAttributes: Map().set('attr', 'attr'),
143 | });
144 | const link3 = new LinkRecord({
145 | id: 'id3',
146 | sourceId: 'id6',
147 | targetId: 'id9',
148 | graphicalAttributes: Map().set('attr', 'attr'),
149 | });
150 | const link4 = new LinkRecord({
151 | id: 'id4',
152 | sourceId: 'id10',
153 | targetId: 'id13',
154 | graphicalAttributes: Map().set('attr', 'attr'),
155 | });
156 | const link5 = new LinkRecord({
157 | id: 'id5',
158 | sourceId: 'id15',
159 | targetId: 'id16',
160 | graphicalAttributes: Map().set('attr', 'attr'),
161 | });
162 | const link6 = new LinkRecord({
163 | id: 'id6',
164 | sourceId: 'id4',
165 | targetId: 'id7',
166 | graphicalAttributes: Map().set('attr', 'attr'),
167 | });
168 | const link7 = new LinkRecord({
169 | id: 'id7',
170 | sourceId: 'id8',
171 | targetId: 'id11',
172 | graphicalAttributes: Map().set('attr', 'attr'),
173 | });
174 | const link8 = new LinkRecord({
175 | id: 'id8',
176 | sourceId: 'id12',
177 | targetId: 'id14',
178 | graphicalAttributes: Map().set('attr', 'attr'),
179 | });
180 |
181 | const givenState: State = Map({
182 | nodes: Map()
183 | .set('id1', node1)
184 | .set('id2', node2)
185 | .set('id3', node3)
186 | .set('id4', node4)
187 | .set('id5', node5)
188 | .set('id6', node6)
189 | .set('id7', node7)
190 | .set('id8', node8),
191 | // eslint-disable-next-line new-cap
192 | ports: OrderedMap()
193 | .set('id1', port1)
194 | .set('id2', port2)
195 | .set('id3', port3)
196 | .set('id4', port4)
197 | .set('id5', port5)
198 | .set('id6', port6)
199 | .set('id7', port7)
200 | .set('id8', port8)
201 | .set('id9', port9)
202 | .set('id10', port10)
203 | .set('id11', port11)
204 | .set('id12', port12)
205 | .set('id13', port13)
206 | .set('id14', port14)
207 | .set('id15', port15)
208 | .set('id16', port16),
209 | links: Map()
210 | .set('id1', link1)
211 | .set('id2', link2)
212 | .set('id3', link3)
213 | .set('id4', link4)
214 | .set('id5', link5)
215 | .set('id6', link6)
216 | .set('id7', link7)
217 | .set('id8', link8),
218 | parents: Map>()
219 | .set('id1', Map({}))
220 | .set('id2', Map({ id1: 'id1' }))
221 | .set('id3', Map({ id2: 'id2' }))
222 | .set('id4', Map({ id2: 'id2' }))
223 | .set('id5', Map({ id3: 'id3' }))
224 | .set('id6', Map({ id4: 'id4' }))
225 | .set('id7', Map({ id5: 'id5', id6: 'id6' }))
226 | .set('id8', Map({ id7: 'id7' })),
227 | childrens: Map>()
228 | .set('id1', Map({ id2: 'id2' }))
229 | .set('id2', Map({ id3: 'id3', id4: 'id4' }))
230 | .set('id3', Map({ id5: 'id5' }))
231 | .set('id4', Map({ id6: 'id6' }))
232 | .set('id5', Map({ id7: 'id7' }))
233 | .set('id6', Map({ id7: 'id7' }))
234 | .set('id7', Map({ id8: 'id8' }))
235 | .set('id8', Map({})),
236 | });
237 |
238 | it('node1 should not have any predecessors', () => {
239 | expect(Selectors.getPredecessors(givenState, 'id1')).toMatchSnapshot();
240 | });
241 |
242 | it('node1 should have 7 successors', () => {
243 | expect(Selectors.getSuccessors(givenState, 'id1')).toMatchSnapshot();
244 | });
245 |
246 | it('node8 should have 7 predecessors', () => {
247 | expect(Selectors.getPredecessors(givenState, 'id8')).toMatchSnapshot();
248 | });
249 |
250 | it('node8 should not have any successors', () => {
251 | expect(Selectors.getSuccessors(givenState, 'id8')).toMatchSnapshot();
252 | });
253 |
254 | it('node4 should have node1, node2 as predecessors', () => {
255 | expect(Selectors.getPredecessors(givenState, 'id4')).toMatchSnapshot();
256 | });
257 |
258 | it('node4 should have node6, node7, node8 as successors', () => {
259 | expect(Selectors.getSuccessors(givenState, 'id4')).toMatchSnapshot();
260 | });
261 | });
262 |
263 | describe('Testing node selectors on nested nodes', () => {
264 | const nodeA1 = new NestedNodeRecord({
265 | id: 'nodeIdA1',
266 | components: List(),
267 | });
268 | const nodeA = new NestedNodeRecord({
269 | id: 'nodeIdA',
270 | components: List([nodeA1]),
271 | });
272 |
273 | const nodeB = new NestedNodeRecord({
274 | id: 'nodeIdB',
275 | });
276 |
277 | const givenState: State = Map({
278 | nodes: Map().set('nodeIdA', nodeA).set('nodeIdB', nodeB),
279 | // eslint-disable-next-line new-cap
280 | ports: OrderedMap(),
281 | links: Map(),
282 | parents: Map>(),
283 | childrens: Map>(),
284 | });
285 |
286 | it('nodeA should not have 1 embeded child and node B 0 children', () => {
287 | expect(givenState.get('nodes').get('nodeIdA').get('components').size).toBe(1);
288 | expect(givenState.get('nodes').get('nodeIdB').get('components').size).toBe(0);
289 | });
290 | });
291 |
--------------------------------------------------------------------------------
/src/reducers/port.reducer.ts:
--------------------------------------------------------------------------------
1 | import invariant from 'invariant';
2 | import { Map, fromJS } from 'immutable';
3 |
4 | import { PortRecord, PositionRecord } from '../constants/flowdesigner.model';
5 | import { removeLink } from '../actions/link.actions';
6 | import linkReducer from './link.reducer';
7 | import { portOutLink, portInLink } from '../selectors/linkSelectors';
8 |
9 | import {
10 | FLOWDESIGNER_PORT_ADD,
11 | FLOWDESIGNER_PORT_ADDS,
12 | FLOWDESIGNER_PORT_SET_GRAPHICAL_ATTRIBUTES,
13 | FLOWDESIGNER_PORT_REMOVE_GRAPHICAL_ATTRIBUTES,
14 | FLOWDESIGNER_PORT_SET_DATA,
15 | FLOWDESIGNER_PORT_REMOVE_DATA,
16 | FLOWDESIGNER_PORT_REMOVE,
17 | PORT_SINK,
18 | PORT_SOURCE,
19 | } from '../constants/flowdesigner.constants';
20 | import {
21 | PortRecordMap,
22 | Id,
23 | LinkRecord as LinkRecordType,
24 | PortRecord as PortRecordType,
25 | PortDirection,
26 | State,
27 | PortAction,
28 | } from '../customTypings/index.d';
29 |
30 | /**
31 | * get ports attached to a node
32 | */
33 | function filterPortsByNode(ports: PortRecordMap, nodeId: Id): PortRecordMap {
34 | return ports.filter((port: PortRecordType) => port.nodeId === nodeId) as PortRecordMap;
35 | }
36 |
37 | /**
38 | * get ports of direction EMITTER or SINK
39 | */
40 | function filterPortsByDirection(ports: PortRecordMap, direction: PortDirection): PortRecordMap {
41 | return ports.filter(
42 | (port: PortRecordType) => port.getPortDirection() === direction,
43 | ) as PortRecordMap;
44 | }
45 |
46 | /**
47 | * for a new port calculate its index by retrieving all its siblings
48 | */
49 | function calculateNewPortIndex(ports: PortRecordMap, port: PortRecordType): number {
50 | return filterPortsByDirection(
51 | filterPortsByNode(ports, port.nodeId),
52 | port.graphicalAttributes.properties.type,
53 | ).size;
54 | }
55 |
56 | function indexPortMap(ports: PortRecordMap): PortRecordMap {
57 | let i = 0;
58 | return ports
59 | .sort((a, b) => {
60 | if (a.getIndex() < b.getIndex()) {
61 | return -1;
62 | }
63 | if (a.getIndex() > b.getIndex()) {
64 | return 1;
65 | }
66 | return 0;
67 | })
68 | .map(port => {
69 | i += 1;
70 | return port.setIndex(i - 1);
71 | }) as PortRecordMap;
72 | }
73 |
74 | /**
75 | * @todo migration to new API
76 | * @param {*} state
77 | * @param {*} port
78 | */
79 | function setPort(state: State, port: PortRecordType) {
80 | const index: number =
81 | port.graphicalAttributes.properties.index ||
82 | calculateNewPortIndex(state.get('ports'), port);
83 | const newState = state.setIn(
84 | ['ports', port.id],
85 | new PortRecord({
86 | id: port.id,
87 | nodeId: port.nodeId,
88 | data: Map(port.data).set(
89 | 'properties',
90 | fromJS(port.data && port.data.properties) || Map(),
91 | ),
92 | graphicalAttributes: Map(port.graphicalAttributes)
93 | .set('position', new PositionRecord(port.graphicalAttributes.position))
94 | .set(
95 | 'properties',
96 | fromJS(
97 | port.graphicalAttributes && {
98 | index,
99 | ...port.graphicalAttributes.properties,
100 | },
101 | ) || Map(),
102 | ),
103 | }),
104 | );
105 | const type = port.graphicalAttributes.properties.type;
106 | if (type === PORT_SOURCE) {
107 | return newState.setIn(['out', port.nodeId, port.id], Map());
108 | } else if (type === PORT_SINK) {
109 | return newState.setIn(['in', port.nodeId, port.id], Map());
110 | }
111 | invariant(
112 | false,
113 | `Can't set a new port ${port.id} if it
114 | data.graphicalAttributes.properties.type !== EMITTER || SINK,
115 | given ${port.graphicalAttributes.properties.type}`,
116 | );
117 | return state;
118 | }
119 |
120 | export default function portReducer(state: State, action: PortAction): State {
121 | switch (action.type) {
122 | case FLOWDESIGNER_PORT_ADD:
123 | if (!state.getIn(['nodes', action.nodeId])) {
124 | invariant(
125 | false,
126 | `Can't set a new port ${action.id} on non existing node ${action.nodeId}`,
127 | );
128 | }
129 |
130 | return setPort(state, {
131 | id: action.id,
132 | nodeId: action.nodeId,
133 | data: action.data,
134 | graphicalAttributes: action.graphicalAttributes,
135 | });
136 | case FLOWDESIGNER_PORT_ADDS: {
137 | const localAction = action;
138 | if (!state.getIn(['nodes', action.nodeId])) {
139 | invariant(false, `Can't set a new ports on non existing node ${action.nodeId}`);
140 | }
141 | return action.ports.reduce(
142 | (cumulatedState, port) =>
143 | setPort(cumulatedState, {
144 | id: port.id,
145 | nodeId: localAction.nodeId,
146 | data: port.data,
147 | graphicalAttributes: port.graphicalAttributes,
148 | }),
149 | state,
150 | );
151 | }
152 | case FLOWDESIGNER_PORT_SET_GRAPHICAL_ATTRIBUTES:
153 | if (!state.getIn(['ports', action.portId])) {
154 | invariant(
155 | false,
156 | `Can't set an graphical attribute on non existing port ${action.portId}`,
157 | );
158 | }
159 |
160 | try {
161 | return state.mergeIn(
162 | ['ports', action.portId, 'graphicalAttributes'],
163 | fromJS(action.graphicalAttributes),
164 | );
165 | } catch (error) {
166 | console.error(error);
167 | return state.mergeIn(
168 | ['ports', action.portId, 'graphicalAttributes', 'properties'],
169 | fromJS(action.graphicalAttributes),
170 | );
171 | }
172 |
173 | case FLOWDESIGNER_PORT_REMOVE_GRAPHICAL_ATTRIBUTES:
174 | if (!state.getIn(['ports', action.portId])) {
175 | invariant(
176 | false,
177 | `Can't remove a graphical attribute on non existing port ${action.portId}`,
178 | );
179 | }
180 |
181 | return state.deleteIn([
182 | 'ports',
183 | action.portId,
184 | 'graphicalAttributes',
185 | 'properties',
186 | action.graphicalAttributesKey,
187 | ]);
188 | case FLOWDESIGNER_PORT_SET_DATA:
189 | if (!state.getIn(['ports', action.portId])) {
190 | invariant(false, `Can't set a data on non existing port ${action.portId}`);
191 | }
192 |
193 | try {
194 | return state.mergeIn(['ports', action.portId, 'data'], fromJS(action.data));
195 | } catch (error) {
196 | console.error(error);
197 | return state.mergeIn(
198 | ['ports', action.portId, 'data', 'properties'],
199 | fromJS(action.data),
200 | );
201 | }
202 |
203 | case FLOWDESIGNER_PORT_REMOVE_DATA:
204 | if (!state.getIn(['ports', action.portId])) {
205 | invariant(false, `Can't remove a data on non existing port ${action.portId}`);
206 | }
207 |
208 | return state.deleteIn(['ports', action.portId, 'data', 'properties', action.dataKey]);
209 | case FLOWDESIGNER_PORT_REMOVE: {
210 | if (!state.getIn(['ports', action.portId])) {
211 | invariant(false, `Can not remove port ${action.portId} since it doesn't exist`);
212 | }
213 | const port: PortRecordType | null | undefined = state.getIn(['ports', action.portId]);
214 | if (port) {
215 | const newState = portInLink(state, action.portId)
216 | .reduce(
217 | (cumulativeState: State, link: LinkRecordType) =>
218 | linkReducer(cumulativeState, removeLink(link.id)),
219 | portOutLink(state, action.portId).reduce(
220 | (cumulativeState: State, link: LinkRecordType) =>
221 | linkReducer(cumulativeState, removeLink(link.id)),
222 | state,
223 | ),
224 | )
225 | .deleteIn(['ports', action.portId])
226 | .deleteIn([
227 | 'out',
228 | state.getIn(['ports', action.portId, 'nodeId']),
229 | action.portId,
230 | ])
231 | .deleteIn([
232 | 'in',
233 | state.getIn(['ports', action.portId, 'nodeId']),
234 | action.portId,
235 | ]);
236 | return newState.mergeDeep({
237 | ports: indexPortMap(
238 | filterPortsByDirection(
239 | filterPortsByNode(newState.get('ports'), port.nodeId),
240 | port.getPortDirection(),
241 | ),
242 | ),
243 | });
244 | }
245 | return state;
246 | }
247 | default:
248 | return state;
249 | }
250 | }
251 |
--------------------------------------------------------------------------------
/src/reducers/node.reducer.ts:
--------------------------------------------------------------------------------
1 | import Immutable, { Map, fromJS } from 'immutable';
2 | import invariant from 'invariant';
3 | import { removePort } from '../actions/port.actions';
4 | import portReducer from './port.reducer';
5 | import { outPort, inPort } from '../selectors/portSelectors';
6 |
7 | import {
8 | FLOWDESIGNER_NODE_ADD,
9 | FLOWDESIGNER_NODE_MOVE_START,
10 | FLOWDESIGNER_NODE_MOVE,
11 | FLOWDESIGNER_NODE_APPLY_MOVEMENT,
12 | FLOWDESIGNER_NODE_MOVE_END,
13 | FLOWDESIGNER_NODE_SET_TYPE,
14 | FLOWDESIGNER_NODE_SET_GRAPHICAL_ATTRIBUTES,
15 | FLOWDESIGNER_NODE_REMOVE_GRAPHICAL_ATTRIBUTES,
16 | FLOWDESIGNER_NODE_SET_DATA,
17 | FLOWDESIGNER_NODE_REMOVE_DATA,
18 | FLOWDESIGNER_NODE_SET_SIZE,
19 | FLOWDESIGNER_NODE_REMOVE,
20 | FLOWDESIGNER_NODE_UPDATE,
21 | } from '../constants/flowdesigner.constants';
22 | import { Node } from '../api';
23 | import {
24 | NodeRecord,
25 | PositionRecord,
26 | SizeRecord,
27 | NodeGraphicalAttributes,
28 | } from '../constants/flowdesigner.model';
29 | import { PortRecord, Id, State, NodeRecordMap } from '../customTypings/index.d';
30 |
31 | const defaultState = Map();
32 | const nodeReducer = (state: State = defaultState, action: any) => {
33 | switch (action.type) {
34 | case FLOWDESIGNER_NODE_ADD:
35 | if (state.getIn(['nodes', action.nodeId])) {
36 | invariant(
37 | false,
38 | `Can not create node ${action.nodeId} since it does already exist`,
39 | );
40 | }
41 |
42 | return state
43 | .setIn(
44 | ['nodes', action.nodeId],
45 | new NodeRecord({
46 | id: action.nodeId,
47 | type: action.nodeType,
48 | data: Immutable.Map(action.data).set(
49 | 'properties',
50 | fromJS(action.data && action.data.properties) || Map(),
51 | ),
52 | graphicalAttributes: new NodeGraphicalAttributes(
53 | fromJS(action.graphicalAttributes),
54 | )
55 | .set('nodeSize', new SizeRecord(action.graphicalAttributes.nodeSize))
56 | .set(
57 | 'position',
58 | new PositionRecord(action.graphicalAttributes.position),
59 | )
60 | .set(
61 | 'properties',
62 | fromJS(action.graphicalAttributes.properties) || Map(),
63 | ),
64 | }),
65 | )
66 | .setIn(['out', action.nodeId], Map())
67 | .setIn(['in', action.nodeId], Map())
68 | .setIn(['childrens', action.nodeId], Map())
69 | .setIn(['parents', action.nodeId], Map());
70 | case FLOWDESIGNER_NODE_UPDATE:
71 | if (action.nodeId === Node.getId(action.node)) {
72 | return state.setIn(['nodes', Node.getId(action.node)], action.node);
73 | } // special case here, the id got changed and it have lots of implication
74 |
75 | return state
76 | .setIn(['nodes', Node.getId(action.node)], action.node)
77 | .deleteIn(['nodes', action.nodeId])
78 | .setIn(['out', Node.getId(action.node)], Map())
79 | .setIn(['in', Node.getId(action.node)], Map())
80 | .setIn(['childrens', Node.getId(action.node)], Map())
81 | .setIn(['parents', Node.getId(action.node)], Map());
82 | case FLOWDESIGNER_NODE_MOVE_START:
83 | if (!state.getIn('nodes', action.nodeId)) {
84 | invariant(false, `Can't move node ${action.nodeId} since it doesn't exist`);
85 | }
86 |
87 | return state.setIn(
88 | ['nodes', action.nodeId, 'graphicalAttributes', 'properties', 'startPosition'],
89 | new PositionRecord(action.nodePosition),
90 | );
91 | case FLOWDESIGNER_NODE_MOVE:
92 | if (!state.getIn('nodes', action.nodeId)) {
93 | invariant(false, `Can't move node ${action.nodeId} since it doesn't exist`);
94 | }
95 |
96 | return state.setIn(
97 | ['nodes', action.nodeId, 'graphicalAttributes', 'position'],
98 | new PositionRecord(action.nodePosition),
99 | );
100 | case FLOWDESIGNER_NODE_MOVE_END:
101 | if (!state.getIn('nodes', action.nodeId)) {
102 | invariant(false, `Can't move node ${action.nodeId} since it doesn't exist`);
103 | }
104 |
105 | return state
106 | .setIn(
107 | ['nodes', action.nodeId, 'graphicalAttributes', 'position'],
108 | new PositionRecord(action.nodePosition),
109 | )
110 | .deleteIn([
111 | 'nodes',
112 | action.nodeId,
113 | 'graphicalAttributes',
114 | 'properties',
115 | 'startPosition',
116 | ]);
117 | case FLOWDESIGNER_NODE_APPLY_MOVEMENT:
118 | return state.update('nodes', (nodes: NodeRecordMap) =>
119 | nodes.map(node => {
120 | if (action.nodesId.find((id: Id) => id === node.id)) {
121 | return node
122 | .setIn(
123 | ['graphicalAttributes', 'position', 'x'],
124 | node.getPosition().x + action.movement.x,
125 | )
126 | .setIn(
127 | ['graphicalAttributes', 'position', 'y'],
128 | node.getPosition().y + action.movement.y,
129 | );
130 | }
131 | return node;
132 | }),
133 | );
134 | case FLOWDESIGNER_NODE_SET_SIZE:
135 | if (!state.getIn(['nodes', action.nodeId])) {
136 | invariant(false, `Can't set size on node ${action.nodeId} since it doesn't exist`);
137 | }
138 |
139 | return state.setIn(
140 | ['nodes', action.nodeId, 'graphicalAttributes', 'nodeSize'],
141 | new SizeRecord(action.nodeSize),
142 | );
143 | case FLOWDESIGNER_NODE_SET_TYPE:
144 | if (!state.getIn(['nodes', action.nodeId])) {
145 | invariant(
146 | false,
147 | `Can't set node.type on node ${action.nodeid} since it doesn't exist`,
148 | );
149 | }
150 |
151 | return state.setIn(['nodes', action.nodeId, 'type'], action.nodeType);
152 | case FLOWDESIGNER_NODE_SET_GRAPHICAL_ATTRIBUTES:
153 | if (!state.getIn(['nodes', action.nodeId])) {
154 | invariant(
155 | false,
156 | `Can't set a graphical attribute on non existing node ${action.nodeId}`,
157 | );
158 | }
159 |
160 | try {
161 | return state.mergeIn(
162 | ['nodes', action.nodeId, 'graphicalAttributes'],
163 | fromJS(action.graphicalAttributes),
164 | );
165 | } catch (error) {
166 | console.error(error);
167 | return state.mergeIn(
168 | ['nodes', action.nodeId, 'graphicalAttributes', 'properties'],
169 | fromJS(action.graphicalAttributes),
170 | );
171 | }
172 |
173 | case FLOWDESIGNER_NODE_REMOVE_GRAPHICAL_ATTRIBUTES:
174 | if (!state.getIn(['nodes', action.nodeId])) {
175 | invariant(
176 | false,
177 | `Can't remove a graphical attribute on non existing node ${action.nodeId}`,
178 | );
179 | }
180 |
181 | return state.deleteIn([
182 | 'nodes',
183 | action.nodeId,
184 | 'graphicalAttributes',
185 | 'properties',
186 | action.graphicalAttributesKey,
187 | ]);
188 | case FLOWDESIGNER_NODE_SET_DATA:
189 | if (!state.getIn(['nodes', action.nodeId])) {
190 | invariant(false, `Can't set a data on non existing node ${action.nodeId}`);
191 | }
192 |
193 | try {
194 | return state.mergeIn(['nodes', action.nodeId, 'data'], fromJS(action.data));
195 | } catch (error) {
196 | console.error(error);
197 | return state.mergeIn(
198 | ['nodes', action.nodeId, 'data', 'properties'],
199 | fromJS(action.data),
200 | );
201 | }
202 |
203 | case FLOWDESIGNER_NODE_REMOVE_DATA:
204 | if (!state.getIn(['nodes', action.nodeId])) {
205 | invariant(false, `Can't remove a data on non existing node ${action.nodeId}`);
206 | }
207 |
208 | return state.deleteIn(['nodes', action.nodeId, 'data', 'properties', action.dataKey]);
209 | case FLOWDESIGNER_NODE_REMOVE:
210 | if (!state.getIn(['nodes', action.nodeId])) {
211 | invariant(false, `Can not remove node ${action.nodeId} since it doesn't exist`);
212 | }
213 |
214 | return inPort(state, action.nodeId)
215 | .reduce(
216 | (cumulativeState: State, port: PortRecord, key: Id) =>
217 | portReducer(cumulativeState, removePort(key)),
218 | outPort(state, action.nodeId).reduce(
219 | (cumulativeState: State, port: PortRecord, key: Id) =>
220 | portReducer(cumulativeState, removePort(key)),
221 | state,
222 | ),
223 | )
224 | .deleteIn(['nodes', action.nodeId])
225 | .deleteIn(['out', action.nodeId])
226 | .deleteIn(['in', action.nodeId])
227 | .deleteIn(['childrens', action.nodeId])
228 | .deleteIn(['parents', action.nodeId]);
229 | default:
230 | return state;
231 | }
232 | };
233 |
234 | export default nodeReducer;
235 |
--------------------------------------------------------------------------------
/src/api/link/link.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This module is public and deal with Graph's object Links
3 | */
4 |
5 | import curry from 'lodash/curry';
6 | import flow from 'lodash/flow';
7 | import indexOf from 'lodash/indexOf';
8 | import isString from 'lodash/isString';
9 | import upperFirst from 'lodash/upperFirst';
10 |
11 | import { throwInDev, throwTypeError } from '../throwInDev';
12 | import { LinkRecord } from '../../constants/flowdesigner.model';
13 | import * as Data from '../data/data';
14 | import { LinkRecord as LinkRecordType, Id } from '../../customTypings/index.d';
15 |
16 | const linkTypeSelector = ['graphicalAttributes', 'linkType'];
17 |
18 | /** in future properties should be removed from the react-flow-designer lib */
19 | const FORBIDEN_GRAPHICAL_ATTRIBUTES = ['properties', 'linkType'];
20 |
21 | /**
22 | * @desc represent a link between Port of the flow diagram
23 | * @typedef {Immutable.Record} LinkRecord
24 | */
25 |
26 | /**
27 | * Test if the first parameter is a LinkRecord instance
28 | * @param {LinkRecord} link
29 | * @return {bool}
30 | * @throws
31 | */
32 | export function isLink(link: LinkRecordType) {
33 | if (link && link instanceof LinkRecord) {
34 | return true;
35 | }
36 | return false;
37 | }
38 |
39 | /**
40 | * Test if the first parameter is a LinkRecord, throw if not
41 | * @param {*} link
42 | * @return {bool}
43 | * @throws
44 | */
45 | export function isLinkElseThrow(link: LinkRecordType) {
46 | const test = isLink(link);
47 | if (!test) {
48 | throwTypeError('Linkrecord', link, 'Link');
49 | }
50 | return test;
51 | }
52 |
53 | /**
54 |
55 | * @param {LinkRecord} link
56 | * @return {string}
57 | */
58 | export function getId(link: LinkRecordType) {
59 | if (isLinkElseThrow(link)) {
60 | return link.get('id');
61 | }
62 | return null;
63 | }
64 |
65 | /**
66 | * @function
67 | * @param {string} id
68 | * @param {LinkRecord} link
69 | * @return {LinkRecord}
70 | */
71 | export const setId = curry((id: Id, link: LinkRecordType) => {
72 | if (isString(id) && isLinkElseThrow(link)) {
73 | return link.set('id', id);
74 | }
75 | throwInDev(`id should be a string, was given ${id && id.toString()}`);
76 | return link;
77 | });
78 |
79 | /**
80 | * @param {LinkRecord} link
81 | * @return {string}
82 | */
83 | export function getSourceId(link: LinkRecordType) {
84 | if (isLinkElseThrow(link)) {
85 | return link.get('sourceId');
86 | }
87 | return null;
88 | }
89 |
90 | /**
91 | * @function
92 | * @param {string} sourceId
93 | * @param {LinkRecord} link
94 | * @return {LinkRecord}
95 | */
96 | export const setSourceId = curry((sourceId: Id, link: LinkRecordType) => {
97 | if (isString(sourceId) && isLinkElseThrow(link)) {
98 | return link.set('sourceId', sourceId);
99 | }
100 | throwInDev(`id should be a string, was given ${sourceId && sourceId.toString()}`);
101 | return link;
102 | });
103 |
104 | /**
105 | * @param {LinkRecord} link
106 | * @return {string}
107 | */
108 | export function getTargetId(link: LinkRecordType) {
109 | if (isLinkElseThrow(link)) {
110 | return link.get('targetId');
111 | }
112 | return null;
113 | }
114 |
115 | /**
116 | * @function
117 | * @param {string} targetId
118 | * @param {LinkRecord} link
119 | * @return {LinkRecord}
120 | */
121 | export const setTargetId = curry((targetId: Id, link: LinkRecordType) => {
122 | if (isString(targetId) && isLinkElseThrow(link)) {
123 | return link.set('targetId', targetId);
124 | }
125 | throwInDev(`id should be a string, was given ${targetId && targetId.toString()}`);
126 | return link;
127 | });
128 |
129 | /**
130 | * @param {LinkRecord} link
131 | * @return {string}
132 | */
133 | export function getComponentType(link: LinkRecordType) {
134 | if (isLinkElseThrow(link)) {
135 | return link.getIn(linkTypeSelector);
136 | }
137 | return null;
138 | }
139 |
140 | /**
141 | * @function
142 | * @param {string} linkType
143 | * @param {LinkRecord} link
144 | * @return {LinkRecord}
145 | */
146 | export const setComponentType = curry((linkType: string, link: LinkRecordType) => {
147 | if (isString(linkType) && isLinkElseThrow(link)) {
148 | return link.setIn(linkTypeSelector, linkType);
149 | }
150 | throwInDev(`linkType should be a string, was given ${linkType && linkType.toString()}`);
151 | return link;
152 | });
153 |
154 | /**
155 | * @function
156 | * @param {string} key
157 | * @param {any} value
158 | * @param {LinkRecord} link
159 | * @return {LinkRecord}
160 | */
161 | export const setData = curry((key: string, value: any, link: LinkRecordType) => {
162 | if (isLinkElseThrow(link)) {
163 | return link.set('data', Data.set(key, value, link.get('data')));
164 | }
165 | return link;
166 | });
167 |
168 | /**
169 | * @function
170 | * @param {string} key
171 | * @param {LinkRecord} link
172 | * @return {any | null}
173 | */
174 | export const getData = curry((key: string, link: LinkRecordType) => {
175 | if (isLinkElseThrow(link)) {
176 | return Data.get(key, link.get('data'));
177 | }
178 | return null;
179 | });
180 |
181 | /**
182 | * @function
183 | * @param {string} key
184 | * @param {LinkRecord} link
185 | * @return {bool}
186 | */
187 | export const hasData = curry((key: string, link: LinkRecordType) => {
188 | if (isLinkElseThrow(link)) {
189 | return Data.has(key, link.get('data'));
190 | }
191 | return false;
192 | });
193 |
194 | /**
195 | * @function
196 | * @param {string} key
197 | * @param {LinkRecord} link
198 | * @return {NodeRecord}
199 | */
200 | export const deleteData = curry((key: string, link: LinkRecordType) => {
201 | if (isLinkElseThrow(link)) {
202 | return link.set('data', Data.deleteKey(key, link.get('data')));
203 | }
204 | return link;
205 | });
206 |
207 | /**
208 | * given a key check if that key is white listed
209 | * @param {string} key
210 | * @return {bool}
211 | */
212 | function isWhiteListAttribute(key: string) {
213 | if (indexOf(FORBIDEN_GRAPHICAL_ATTRIBUTES, key) === -1) {
214 | return true;
215 | }
216 | throwInDev(
217 | `${key} is a protected value of the Link, please use get${upperFirst(key)} set${upperFirst(
218 | key,
219 | )} from this module to make change on those values`,
220 | );
221 | return false;
222 | }
223 |
224 | /**
225 | * @function
226 | * @param {string} key
227 | * @param {any} value
228 | * @param {LinkRecord} link
229 | * @return {LinkRecord}
230 | */
231 | export const setGraphicalAttribute = curry((key: string, value: any, link: LinkRecordType) => {
232 | if (isLinkElseThrow(link) && isWhiteListAttribute(key)) {
233 | return link.set(
234 | 'graphicalAttributes',
235 | Data.set(key, value, link.get('graphicalAttributes')),
236 | );
237 | }
238 | return link;
239 | });
240 |
241 | /**
242 | * @function
243 | * @param {string} key
244 | * @param {LinkRecord} link
245 | * @return {any | null}
246 | */
247 | export const getGraphicalAttribute = curry((key: string, link: LinkRecordType) => {
248 | if (isLinkElseThrow(link) && isWhiteListAttribute(key)) {
249 | return Data.get(key, link.get('graphicalAttributes'));
250 | }
251 | return null;
252 | });
253 |
254 | /**
255 | * @function
256 | * @param {string} key
257 | * @param {LinkRecord} link
258 | * @return {bool}
259 | */
260 | export const hasGraphicalAttribute = curry((key: string, link: LinkRecordType) => {
261 | if (isLinkElseThrow(link) && isWhiteListAttribute(key)) {
262 | return Data.has(key, link.get('graphicalAttributes'));
263 | }
264 | return false;
265 | });
266 |
267 | /**
268 | * @function
269 | * @param {string} key
270 | * @param {LinkRecord} node
271 | * @return {LinkRecord}
272 | */
273 | export const deleteGraphicalAttribute = curry((key: string, link: LinkRecordType) => {
274 | if (isLinkElseThrow(link) && isWhiteListAttribute(key)) {
275 | return link.set(
276 | 'graphicalAttributes',
277 | Data.deleteKey(key, link.get('graphicalAttributes')),
278 | );
279 | }
280 | return link;
281 | });
282 |
283 | /**
284 | * minimal link creation factory, additionnals information can be set trought
285 | * the above set* functions
286 | * @function
287 | * @param {string} id
288 | * @param {string} sourceId
289 | * @param {string} targetId
290 | * @param {string} componenttype
291 | * @return {LinkRecord}
292 | */
293 | export const create = curry((id: Id, sourceId: Id, targetId: Id, componentType: string) =>
294 | flow([
295 | setId(id),
296 | setSourceId(sourceId),
297 | setTargetId(targetId),
298 | setComponentType(componentType),
299 | ])(new LinkRecord()),
300 | );
301 |
--------------------------------------------------------------------------------