{
27 | renderNextOptions(value: any) {
28 | const { nextChoices } = this.props;
29 |
30 | // This function is defined and used locally to avoid tslint's jsx-no-lambda error.
31 | // It requires the local value variable, so it cannot be defined in the class.
32 | const handleChange = (event: any) => {
33 | event.target.value = value;
34 | };
35 |
36 | return (
37 |
42 | );
43 | }
44 |
45 | renderAndObjectArray(andObjectArray: any[]) {
46 | return andObjectArray.map((andObject, index) => {
47 | return (
48 |
49 | {Object.keys(andObject).map(key => {
50 | return (
51 |
52 | {this.renderKey(key, andObject[key])}
53 |
54 | );
55 | })}
56 |
57 | );
58 | });
59 | }
60 |
61 | renderChoicesOptions(value: any) {
62 | return value.map((choice, index) => {
63 | return (
64 |
65 | {Object.keys(choice).map(choiceOption => {
66 | if (choiceOption === 'Next') {
67 | // "Next" option
68 | return (
69 |
70 | {' '}
71 | {this.renderNextOptions(choice[choiceOption])}
72 |
73 | );
74 | } else if (Array.isArray(choice[choiceOption])) {
75 | // "And" array
76 | return (
77 |
78 | {' '}
79 | {this.renderAndObjectArray(choice[choiceOption])}
80 |
81 | );
82 | }
83 |
84 | // text option
85 | return (
86 |
87 | {choice[choiceOption]}
88 |
89 | );
90 | })}
91 |
92 | );
93 | });
94 | }
95 |
96 | renderKey(key: string, value: any) {
97 | if (key === 'Next') {
98 | return this.renderNextOptions(value);
99 | } else if (key === 'Choices') {
100 | return this.renderChoicesOptions(value);
101 | } else if (
102 | typeof value === 'string' ||
103 | typeof value === 'number' ||
104 | typeof value === 'boolean'
105 | ) {
106 | return value;
107 | } else if (typeof value === 'object' && !Array.isArray(value)) {
108 | return Object.keys(value).map(valueKey => {
109 | return (
110 |
111 | {' '}
112 | {this.renderKey(valueKey, value[valueKey])}
113 |
114 | );
115 | });
116 | }
117 |
118 | return {JSON.stringify(value, null, 2)};
119 | }
120 |
121 | render() {
122 | const { bwdlNode, bwdlNodeKey } = this.props;
123 |
124 | return (
125 |
126 |
{bwdlNodeKey}
127 | {Object.keys(bwdlNode).map(key => {
128 | return (
129 |
130 | {this.renderKey(key, bwdlNode[key])}
131 |
132 | );
133 | })}
134 |
135 | );
136 | }
137 | }
138 |
139 | export default BwdlNodeForm;
140 |
--------------------------------------------------------------------------------
/src/examples/bwdl-editable/bwdl-config.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | /*
3 | Copyright(c) 2018 Uber Technologies, Inc.
4 |
5 | Licensed under the Apache License, Version 2.0 (the "License");
6 | you may not use this file except in compliance with the License.
7 | You may obtain a copy of the License at
8 |
9 | http://www.apache.org/licenses/LICENSE-2.0
10 |
11 | Unless required by applicable law or agreed to in writing, software
12 | distributed under the License is distributed on an "AS IS" BASIS,
13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | See the License for the specific language governing permissions and
15 | limitations under the License.
16 | */
17 |
18 | /*
19 | Example config for GraphView component
20 | */
21 | import * as React from 'react';
22 |
23 | export const NODE_KEY = 'title'; // Key used to identify nodes
24 |
25 | // These keys are arbitrary (but must match the config)
26 | // However, GraphView renders text differently for empty types
27 | // so this has to be passed in if that behavior is desired.
28 | export const EMPTY_TYPE = 'empty'; // Empty node type
29 | export const CHOICE_TYPE = 'Choice';
30 | export const TASK_TYPE = 'Task';
31 | export const PASS_TYPE = 'Pass';
32 | export const WAIT_TYPE = 'Wait';
33 | export const TERMINATOR_TYPE = 'Terminator';
34 | export const SPECIAL_CHILD_SUBTYPE = 'specialChild';
35 | export const EMPTY_EDGE_TYPE = 'emptyEdge';
36 | export const SPECIAL_EDGE_TYPE = 'specialEdge';
37 |
38 | export const nodeTypes = [
39 | EMPTY_TYPE,
40 | CHOICE_TYPE,
41 | TASK_TYPE,
42 | PASS_TYPE,
43 | WAIT_TYPE,
44 | TERMINATOR_TYPE,
45 | ];
46 | export const edgeTypes = [EMPTY_EDGE_TYPE, SPECIAL_EDGE_TYPE];
47 |
48 | const EmptyShape = (
49 |
50 |
51 |
52 | );
53 |
54 | const ChoiceShape = (
55 |
56 |
57 |
58 | );
59 |
60 | const TaskShape = (
61 |
62 |
63 |
64 | );
65 |
66 | const PassShape = (
67 |
68 |
69 |
70 | );
71 |
72 | const WaitShape = (
73 |
74 |
75 |
76 | );
77 |
78 | const TerminatorShape = (
79 |
80 |
87 |
88 | );
89 |
90 | const SpecialChildShape = (
91 |
92 |
93 |
94 | );
95 |
96 | const EmptyEdgeShape = (
97 |
98 |
99 |
100 | );
101 |
102 | const SpecialEdgeShape = (
103 |
104 |
112 |
113 | );
114 |
115 | export default {
116 | EdgeTypes: {
117 | emptyEdge: {
118 | shape: EmptyEdgeShape,
119 | shapeId: '#emptyEdge',
120 | },
121 | specialEdge: {
122 | shape: SpecialEdgeShape,
123 | shapeId: '#specialEdge',
124 | },
125 | },
126 | NodeSubtypes: {
127 | specialChild: {
128 | shape: SpecialChildShape,
129 | shapeId: '#specialChild',
130 | },
131 | },
132 | NodeTypes: {
133 | Choice: {
134 | shape: ChoiceShape,
135 | shapeId: '#choice',
136 | typeText: 'Choice',
137 | },
138 | emptyNode: {
139 | shape: EmptyShape,
140 | shapeId: '#empty',
141 | typeText: 'None',
142 | },
143 | Pass: {
144 | shape: PassShape,
145 | shapeId: '#pass',
146 | typeText: 'Pass',
147 | },
148 | Task: {
149 | shape: TaskShape,
150 | shapeId: '#task',
151 | typeText: 'Task',
152 | },
153 | Terminator: {
154 | shape: TerminatorShape,
155 | shapeId: '#terminator',
156 | typeText: 'Terminator',
157 | },
158 | Wait: {
159 | shape: WaitShape,
160 | shapeId: '#wait',
161 | typeText: 'Wait',
162 | },
163 | },
164 | };
165 |
--------------------------------------------------------------------------------
/src/examples/bwdl/bwdl-config.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | /*
3 | Copyright(c) 2018 Uber Technologies, Inc.
4 |
5 | Licensed under the Apache License, Version 2.0 (the "License");
6 | you may not use this file except in compliance with the License.
7 | You may obtain a copy of the License at
8 |
9 | http://www.apache.org/licenses/LICENSE-2.0
10 |
11 | Unless required by applicable law or agreed to in writing, software
12 | distributed under the License is distributed on an "AS IS" BASIS,
13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | See the License for the specific language governing permissions and
15 | limitations under the License.
16 | */
17 |
18 | /*
19 | Example config for GraphView component
20 | */
21 | import * as React from 'react';
22 |
23 | export const NODE_KEY = 'title'; // Key used to identify nodes
24 |
25 | // These keys are arbitrary (but must match the config)
26 | // However, GraphView renders text differently for empty types
27 | // so this has to be passed in if that behavior is desired.
28 | export const EMPTY_TYPE = 'empty'; // Empty node type
29 | export const CHOICE_TYPE = 'Choice';
30 | export const TASK_TYPE = 'Task';
31 | export const PASS_TYPE = 'Pass';
32 | export const WAIT_TYPE = 'Wait';
33 | export const TERMINATOR_TYPE = 'Terminator';
34 | export const SPECIAL_CHILD_SUBTYPE = 'specialChild';
35 | export const EMPTY_EDGE_TYPE = 'emptyEdge';
36 | export const SPECIAL_EDGE_TYPE = 'specialEdge';
37 |
38 | export const nodeTypes = [
39 | EMPTY_TYPE,
40 | CHOICE_TYPE,
41 | TASK_TYPE,
42 | PASS_TYPE,
43 | WAIT_TYPE,
44 | TERMINATOR_TYPE,
45 | ];
46 | export const edgeTypes = [EMPTY_EDGE_TYPE, SPECIAL_EDGE_TYPE];
47 |
48 | const EmptyShape = (
49 |
50 |
51 |
52 | );
53 |
54 | const ChoiceShape = (
55 |
56 |
57 |
58 | );
59 |
60 | const TaskShape = (
61 |
62 |
69 |
70 | );
71 |
72 | const PassShape = (
73 |
74 |
75 |
76 | );
77 |
78 | const WaitShape = (
79 |
80 |
81 |
82 | );
83 |
84 | const TerminatorShape = (
85 |
86 |
93 |
94 | );
95 |
96 | const SpecialChildShape = (
97 |
98 |
99 |
100 | );
101 |
102 | const EmptyEdgeShape = (
103 |
104 |
105 |
106 | );
107 |
108 | const SpecialEdgeShape = (
109 |
110 |
118 |
119 | );
120 |
121 | export default {
122 | EdgeTypes: {
123 | emptyEdge: {
124 | shape: EmptyEdgeShape,
125 | shapeId: '#emptyEdge',
126 | },
127 | specialEdge: {
128 | shape: SpecialEdgeShape,
129 | shapeId: '#specialEdge',
130 | },
131 | },
132 | NodeSubtypes: {
133 | specialChild: {
134 | shape: SpecialChildShape,
135 | shapeId: '#specialChild',
136 | },
137 | },
138 | NodeTypes: {
139 | Choice: {
140 | shape: ChoiceShape,
141 | shapeId: '#choice',
142 | typeText: 'Choice',
143 | },
144 | emptyNode: {
145 | shape: EmptyShape,
146 | shapeId: '#empty',
147 | typeText: 'None',
148 | },
149 | Pass: {
150 | shape: PassShape,
151 | shapeId: '#pass',
152 | typeText: 'Pass',
153 | },
154 | Task: {
155 | shape: TaskShape,
156 | shapeId: '#task',
157 | typeText: 'Task',
158 | },
159 | Terminator: {
160 | shape: TerminatorShape,
161 | shapeId: '#terminator',
162 | typeText: 'Terminator',
163 | },
164 | Wait: {
165 | shape: WaitShape,
166 | shapeId: '#wait',
167 | typeText: 'Wait',
168 | },
169 | },
170 | };
171 |
--------------------------------------------------------------------------------
/src/examples/graph-config.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | /*
3 | Copyright(c) 2018 Uber Technologies, Inc.
4 |
5 | Licensed under the Apache License, Version 2.0 (the "License");
6 | you may not use this file except in compliance with the License.
7 | You may obtain a copy of the License at
8 |
9 | http://www.apache.org/licenses/LICENSE-2.0
10 |
11 | Unless required by applicable law or agreed to in writing, software
12 | distributed under the License is distributed on an "AS IS" BASIS,
13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | See the License for the specific language governing permissions and
15 | limitations under the License.
16 | */
17 |
18 | /*
19 | Example config for GraphView component
20 | */
21 | import * as React from 'react';
22 |
23 | export const NODE_KEY = 'id'; // Key used to identify nodes
24 |
25 | // These keys are arbitrary (but must match the config)
26 | // However, GraphView renders text differently for empty types
27 | // so this has to be passed in if that behavior is desired.
28 | export const EMPTY_TYPE = 'customEmpty'; // Empty node type
29 | export const POLY_TYPE = 'poly';
30 | export const SPECIAL_TYPE = 'special';
31 | export const SKINNY_TYPE = 'skinny';
32 | export const SPECIAL_CHILD_SUBTYPE = 'specialChild';
33 | export const EMPTY_EDGE_TYPE = 'emptyEdge';
34 | export const SPECIAL_EDGE_TYPE = 'specialEdge';
35 | export const COMPLEX_CIRCLE_TYPE = 'complexCircle';
36 |
37 | export const nodeTypes = [EMPTY_TYPE, POLY_TYPE, SPECIAL_TYPE, SKINNY_TYPE];
38 | export const edgeTypes = [EMPTY_EDGE_TYPE, SPECIAL_EDGE_TYPE];
39 |
40 | const EmptyNodeShape = (
41 |
42 |
43 |
44 | );
45 |
46 | const CustomEmptyShape = (
47 |
48 |
49 |
50 | );
51 |
52 | const SpecialShape = (
53 |
54 |
55 |
56 | );
57 |
58 | const PolyShape = (
59 |
60 |
61 |
62 | );
63 |
64 | const ComplexCircleShape = (
65 |
66 |
67 |
68 |
72 |
73 | );
74 |
75 | const SkinnyShape = (
76 |
77 |
78 |
79 | );
80 |
81 | const SpecialChildShape = (
82 |
83 |
90 |
91 | );
92 |
93 | const EmptyEdgeShape = (
94 |
95 |
96 |
97 | );
98 |
99 | const SpecialEdgeShape = (
100 |
101 |
109 |
110 | );
111 |
112 | export default {
113 | EdgeTypes: {
114 | emptyEdge: {
115 | shape: EmptyEdgeShape,
116 | shapeId: '#emptyEdge',
117 | },
118 | specialEdge: {
119 | shape: SpecialEdgeShape,
120 | shapeId: '#specialEdge',
121 | },
122 | },
123 | NodeSubtypes: {
124 | specialChild: {
125 | shape: SpecialChildShape,
126 | shapeId: '#specialChild',
127 | },
128 | },
129 | NodeTypes: {
130 | emptyNode: {
131 | shape: EmptyNodeShape,
132 | shapeId: '#emptyNode',
133 | typeText: 'None',
134 | },
135 | empty: {
136 | shape: CustomEmptyShape,
137 | shapeId: '#empty',
138 | typeText: 'None',
139 | },
140 | special: {
141 | shape: SpecialShape,
142 | shapeId: '#special',
143 | typeText: 'Special',
144 | },
145 | skinny: {
146 | shape: SkinnyShape,
147 | shapeId: '#skinny',
148 | typeText: 'Skinny',
149 | },
150 | poly: {
151 | shape: PolyShape,
152 | shapeId: '#poly',
153 | typeText: 'Poly',
154 | },
155 | complexCircle: {
156 | shape: ComplexCircleShape,
157 | shapeId: '#complexCircle',
158 | typeText: '#complexCircle',
159 | },
160 | },
161 | };
162 |
--------------------------------------------------------------------------------
/src/utilities/graph-util.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | /*
3 | Copyright(c) 2018 Uber Technologies, Inc.
4 |
5 | Licensed under the Apache License, Version 2.0 (the "License");
6 | you may not use this file except in compliance with the License.
7 | You may obtain a copy of the License at
8 |
9 | http://www.apache.org/licenses/LICENSE-2.0
10 |
11 | Unless required by applicable law or agreed to in writing, software
12 | distributed under the License is distributed on an "AS IS" BASIS,
13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | See the License for the specific language governing permissions and
15 | limitations under the License.
16 | */
17 |
18 | import { type IEdge } from '../components/edge';
19 | import { type INode } from '../components/node';
20 | import fastDeepEqual from 'fast-deep-equal';
21 |
22 | export type INodeMapNode = {
23 | node: INode,
24 | originalArrIndex: number,
25 | incomingEdges: IEdge[],
26 | outgoingEdges: IEdge[],
27 | parents: INode[],
28 | children: INode[],
29 | };
30 |
31 | class GraphUtils {
32 | static getNodesMap(nodes: any, key: string) {
33 | const map = {};
34 | const arr = Object.keys(nodes).map(key => nodes[key]);
35 | let item = null;
36 |
37 | for (let i = 0; i < arr.length; i++) {
38 | item = arr[i];
39 | map[`key-${item[key]}`] = {
40 | children: [],
41 | incomingEdges: [],
42 | node: item,
43 | originalArrIndex: i,
44 | outgoingEdges: [],
45 | parents: [],
46 | };
47 | }
48 |
49 | return map;
50 | }
51 |
52 | static getEdgesMap(arr: IEdge[]) {
53 | const map = {};
54 | let item = null;
55 |
56 | for (let i = 0; i < arr.length; i++) {
57 | item = arr[i];
58 |
59 | if (!item.target) {
60 | continue;
61 | }
62 |
63 | map[`${item.source || ''}_${item.target}`] = {
64 | edge: item,
65 | originalArrIndex: i,
66 | };
67 | }
68 |
69 | return map;
70 | }
71 |
72 | static linkNodesAndEdges(nodesMap: any, edges: IEdge[]) {
73 | let nodeMapSourceNode = null;
74 | let nodeMapTargetNode = null;
75 | let edge = null;
76 |
77 | for (let i = 0; i < edges.length; i++) {
78 | edge = edges[i];
79 |
80 | if (!edge.target) {
81 | continue;
82 | }
83 |
84 | nodeMapSourceNode = nodesMap[`key-${edge.source || ''}`];
85 | nodeMapTargetNode = nodesMap[`key-${edge.target}`];
86 |
87 | // avoid an orphaned edge
88 | if (nodeMapSourceNode && nodeMapTargetNode) {
89 | nodeMapSourceNode.outgoingEdges.push(edge);
90 | nodeMapTargetNode.incomingEdges.push(edge);
91 | nodeMapSourceNode.children.push(nodeMapTargetNode);
92 | nodeMapTargetNode.parents.push(nodeMapSourceNode);
93 | }
94 | }
95 | }
96 |
97 | static removeElementFromDom(id: string) {
98 | const container = document.getElementById(id);
99 |
100 | if (container && container.parentNode) {
101 | container.parentNode.removeChild(container);
102 |
103 | return true;
104 | }
105 |
106 | return false;
107 | }
108 |
109 | static findParent(element: any, selector: string) {
110 | if (element && element.matches && element.matches(selector)) {
111 | return element;
112 | } else if (element && element.parentNode) {
113 | return GraphUtils.findParent(element.parentNode, selector);
114 | }
115 |
116 | return null;
117 | }
118 |
119 | static classNames(...args: any[]) {
120 | let className = '';
121 |
122 | for (const arg of args) {
123 | if (typeof arg === 'string' || typeof arg === 'number') {
124 | className += ` ${arg}`;
125 | } else if (
126 | typeof arg === 'object' &&
127 | !Array.isArray(arg) &&
128 | arg !== null
129 | ) {
130 | Object.keys(arg).forEach(key => {
131 | if (arg[key]) {
132 | className += ` ${key}`;
133 | }
134 | });
135 | } else if (Array.isArray(arg)) {
136 | className += ` ${arg.join(' ')}`;
137 | }
138 | }
139 |
140 | return className.trim();
141 | }
142 |
143 | static yieldingLoop(count, chunksize, callback, finished) {
144 | let i = 0;
145 |
146 | (function chunk() {
147 | const end = Math.min(i + chunksize, count);
148 |
149 | for (; i < end; ++i) {
150 | callback.call(null, i);
151 | }
152 |
153 | if (i < count) {
154 | setTimeout(chunk, 0);
155 | } else {
156 | finished && finished.call(null);
157 | }
158 | })();
159 | }
160 |
161 | // retained for backwards compatibility
162 | static hasNodeShallowChanged(prevNode: INode, newNode: INode) {
163 | return !this.isEqual(prevNode, newNode);
164 | }
165 |
166 | static isEqual(prevNode: any, newNode: any) {
167 | return fastDeepEqual(prevNode, newNode);
168 | }
169 | }
170 |
171 | export default GraphUtils;
172 |
--------------------------------------------------------------------------------
/__tests__/utilities/transformers/bwdl-transformer.test.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import * as React from 'react';
4 | import BwdlTransformer from '../../../src/utilities/transformers/bwdl-transformer';
5 |
6 | describe('BwdlTransformer', () => {
7 | const output = null;
8 |
9 | describe('class', () => {
10 | it('is defined', () => {
11 | expect(BwdlTransformer).toBeDefined();
12 | });
13 | });
14 |
15 | describe('transform static method', () => {
16 | it('returns a default response when the input has no states', () => {
17 | const input = {};
18 | const expected = {
19 | edges: [],
20 | nodes: [],
21 | };
22 | const result = BwdlTransformer.transform(input);
23 |
24 | expect(JSON.stringify(result)).toEqual(JSON.stringify(expected));
25 | });
26 |
27 | it('returns a default node edge array for a single State node', () => {
28 | const input = {
29 | StartAt: 'test',
30 | States: {
31 | test: {},
32 | },
33 | };
34 | const expected = {
35 | edges: [],
36 | nodes: [
37 | {
38 | title: 'test',
39 | x: 0,
40 | y: 0,
41 | },
42 | ],
43 | };
44 | const result = BwdlTransformer.transform(input);
45 |
46 | expect(JSON.stringify(result)).toEqual(JSON.stringify(expected));
47 | });
48 |
49 | it('handles Choice nodes', () => {
50 | const input = {
51 | StartAt: 'test',
52 | States: {
53 | test: {
54 | Type: 'Choice',
55 | Choices: [
56 | {
57 | Next: 'test2',
58 | },
59 | ],
60 | },
61 | test2: {},
62 | },
63 | };
64 | const expected = {
65 | edges: [
66 | {
67 | source: 'test',
68 | target: 'test2',
69 | },
70 | ],
71 | nodes: [
72 | {
73 | title: 'test',
74 | type: 'Choice',
75 | x: 0,
76 | y: 0,
77 | },
78 | {
79 | title: 'test2',
80 | x: 0,
81 | y: 0,
82 | },
83 | ],
84 | };
85 | const result = BwdlTransformer.transform(input);
86 |
87 | expect(JSON.stringify(result)).toEqual(JSON.stringify(expected));
88 | });
89 |
90 | it('handles a Choice node with a Default value', () => {
91 | const input = {
92 | StartAt: 'test',
93 | States: {
94 | test: {
95 | Type: 'Choice',
96 | Choices: [],
97 | Default: 'test2',
98 | },
99 | test2: {},
100 | },
101 | };
102 | const expected = {
103 | edges: [
104 | {
105 | source: 'test',
106 | target: 'test2',
107 | },
108 | ],
109 | nodes: [
110 | {
111 | title: 'test',
112 | type: 'Choice',
113 | x: 0,
114 | y: 0,
115 | },
116 | {
117 | title: 'test2',
118 | x: 0,
119 | y: 0,
120 | },
121 | ],
122 | };
123 | const result = BwdlTransformer.transform(input);
124 |
125 | expect(JSON.stringify(result)).toEqual(JSON.stringify(expected));
126 | });
127 |
128 | it('handles a regular node with a Next property', () => {
129 | const input = {
130 | StartAt: 'test',
131 | States: {
132 | test: {
133 | Type: 'Default',
134 | Next: 'test2',
135 | },
136 | test2: {},
137 | },
138 | };
139 | const expected = {
140 | edges: [
141 | {
142 | source: 'test',
143 | target: 'test2',
144 | },
145 | ],
146 | nodes: [
147 | {
148 | title: 'test',
149 | type: 'Default',
150 | x: 0,
151 | y: 0,
152 | },
153 | {
154 | title: 'test2',
155 | x: 0,
156 | y: 0,
157 | },
158 | ],
159 | };
160 | const result = BwdlTransformer.transform(input);
161 |
162 | expect(JSON.stringify(result)).toEqual(JSON.stringify(expected));
163 | });
164 |
165 | it('returns a default set when does not have a current node', () => {
166 | const input = {
167 | StartAt: 'test',
168 | States: {},
169 | };
170 | const expected = {
171 | edges: [],
172 | nodes: [],
173 | };
174 | const result = BwdlTransformer.transform(input);
175 |
176 | expect(JSON.stringify(result)).toEqual(JSON.stringify(expected));
177 | });
178 | });
179 |
180 | describe('revert static method', () => {
181 | it('returns the input', () => {
182 | const input = {
183 | test: true,
184 | };
185 | const result = BwdlTransformer.revert(input);
186 |
187 | expect(JSON.stringify(result)).toEqual(JSON.stringify(input));
188 | });
189 | });
190 | });
191 |
--------------------------------------------------------------------------------
/src/examples/bwdl/bwdl-example-data.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | /*
3 | Copyright(c) 2018 Uber Technologies, Inc.
4 |
5 | Licensed under the Apache License, Version 2.0 (the "License");
6 | you may not use this file except in compliance with the License.
7 | You may obtain a copy of the License at
8 |
9 | http://www.apache.org/licenses/LICENSE-2.0
10 |
11 | Unless required by applicable law or agreed to in writing, software
12 | distributed under the License is distributed on an "AS IS" BASIS,
13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | See the License for the specific language governing permissions and
15 | limitations under the License.
16 | */
17 |
18 | export default {
19 | ExampleSource:
20 | 'https://code.uberinternal.com/file/data/aioyv5yrrs3dadbmxlap/PHID-FILE-v36jeiyn4y3gphtdwjsm/1.json',
21 | Name: 'Colombo_Intercity_Driver_dispatch',
22 | Comment:
23 | 'Send SMS message to drivers accept dispatch for Colombo intercity trip',
24 | Version: 1,
25 | Domain: '//Autobots',
26 | Id: '//Autobots/ColomboIntercityDriverDispatch',
27 | StartAt: 'Init',
28 | AllowReentry: true,
29 | States: {
30 | Init: {
31 | Type: 'Terminator',
32 | Resource: 'kafka://hp_demand_job-assigned',
33 | ResultPath: '$.event',
34 | Next: 'Check City and Vehicle View',
35 | },
36 | 'Check City and Vehicle View': {
37 | Type: 'Choice',
38 | InputPath: '$.event',
39 | Choices: [
40 | {
41 | And: [
42 | {
43 | Variable: '$.region.id',
44 | NumberEquals: 478,
45 | },
46 | {
47 | Variable: '$.vehicleViewId',
48 | NumberEquals: 20006733,
49 | },
50 | ],
51 | Next: 'SMS for Dispatch accepted',
52 | },
53 | {
54 | And: [
55 | {
56 | Variable: '$.region.id',
57 | NumberEquals: 999,
58 | },
59 | ],
60 | Next: 'SMS for Dispatch denied',
61 | },
62 | ],
63 | },
64 | 'Check Other City': {
65 | Type: 'Choice',
66 | InputPath: '$.event',
67 | Choices: [
68 | {
69 | And: [
70 | {
71 | Variable: '$.region.id',
72 | NumberEquals: 478,
73 | },
74 | ],
75 | Next: 'Wait for six hours',
76 | },
77 | {
78 | And: [
79 | {
80 | Variable: '$.region.id',
81 | NumberEquals: 999,
82 | },
83 | ],
84 | Next: 'Wait for twenty four hours',
85 | },
86 | ],
87 | },
88 | 'SMS for Dispatch accepted': {
89 | Type: 'Pass',
90 | InputPath: '$.event',
91 | Result: {
92 | expirationMinutes: 60,
93 | fromUserUUID: '71af5aea-9eaa-45a1-9825-2c124030b063',
94 | toUserUUID: 'Eval($.supplyUUID)',
95 | getSMSReply: false,
96 | message:
97 | 'Hithawath Partner, Oba labegena athi mema trip eka UberGALLE trip ekaki, Karunakara rider wa amatha drop location eka confirm karaganna. Sthuthi',
98 | messageType: 'SEND_SMS',
99 | priority: 1,
100 | actionUUID: 'd259c34d-457a-411e-8c93-6edd63a7ddc6',
101 | },
102 | ResultPath: '$.actionParam',
103 | Next: 'Send SMS',
104 | },
105 | 'SMS for Dispatch denied': {
106 | Type: 'Pass',
107 | InputPath: '$.event',
108 | Result: {
109 | expirationMinutes: 60,
110 | fromUserUUID: '71af5aea-9eaa-45a1-9825-2c124030b063',
111 | toUserUUID: 'Eval($.supplyUUID)',
112 | getSMSReply: false,
113 | message:
114 | 'Hithawath Partner, Oba labegena athi mema trip eka UberGALLE trip ekaki, Karunakara rider wa amatha drop location eka confirm karaganna. Sthuthi',
115 | messageType: 'SEND_SMS',
116 | priority: 1,
117 | actionUUID: 'd259c34d-457a-411e-8c93-6edd63a7ddc6',
118 | },
119 | ResultPath: '$.actionParam',
120 | Next: 'Send SMS',
121 | },
122 | 'Send SMS': {
123 | Type: 'Task',
124 | InputPath: '$.actionParam',
125 | Resource: 'uns://sjc1/sjc1-prod01/us1/cleopatra/Cleopatra::sendSMS',
126 | InputSchema: {
127 | '$.expirationMinutes': 'int',
128 | '$.toUserUUID': 'string',
129 | '$.fromUserUUID': 'string',
130 | '$.getSMSReply': 'bool',
131 | '$.message': 'string',
132 | '$.messageType': 'string',
133 | '$.priority': 'int',
134 | '$.actionUUID': 'string',
135 | },
136 | OutputSchema: {
137 | '$.fraudDriverUUIDs[*]': 'string',
138 | },
139 | Next: 'Check Other City',
140 | },
141 | 'Wait for six hours': {
142 | Type: 'Wait',
143 | Seconds: 21600,
144 | Next: 'Exit',
145 | },
146 | 'Wait for twenty four hours': {
147 | Type: 'Wait',
148 | Seconds: 86400,
149 | Next: 'Exit',
150 | },
151 | Exit: {
152 | Type: 'Terminator',
153 | End: true,
154 | },
155 | },
156 | };
157 |
--------------------------------------------------------------------------------
/src/examples/bwdl-editable/bwdl-example-data.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | /*
3 | Copyright(c) 2018 Uber Technologies, Inc.
4 |
5 | Licensed under the Apache License, Version 2.0 (the "License");
6 | you may not use this file except in compliance with the License.
7 | You may obtain a copy of the License at
8 |
9 | http://www.apache.org/licenses/LICENSE-2.0
10 |
11 | Unless required by applicable law or agreed to in writing, software
12 | distributed under the License is distributed on an "AS IS" BASIS,
13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | See the License for the specific language governing permissions and
15 | limitations under the License.
16 | */
17 |
18 | export default {
19 | ExampleSource:
20 | 'https://code.uberinternal.com/file/data/aioyv5yrrs3dadbmxlap/PHID-FILE-v36jeiyn4y3gphtdwjsm/1.json',
21 | Name: 'Colombo_Intercity_Driver_dispatch',
22 | Comment:
23 | 'Send SMS message to drivers accept dispatch for Colombo intercity trip',
24 | Version: 1,
25 | Domain: '//Autobots',
26 | Id: '//Autobots/ColomboIntercityDriverDispatch',
27 | StartAt: 'Init',
28 | AllowReentry: true,
29 | States: {
30 | Init: {
31 | Type: 'Terminator',
32 | Resource: 'kafka://hp_demand_job-assigned',
33 | ResultPath: '$.event',
34 | Next: 'Check City and Vehicle View',
35 | },
36 | 'Check City and Vehicle View': {
37 | Type: 'Choice',
38 | InputPath: '$.event',
39 | Choices: [
40 | {
41 | And: [
42 | {
43 | Variable: '$.region.id',
44 | NumberEquals: 478,
45 | },
46 | {
47 | Variable: '$.vehicleViewId',
48 | NumberEquals: 20006733,
49 | },
50 | ],
51 | Next: 'SMS for Dispatch accepted',
52 | },
53 | {
54 | And: [
55 | {
56 | Variable: '$.region.id',
57 | NumberEquals: 999,
58 | },
59 | ],
60 | Next: 'SMS for Dispatch denied',
61 | },
62 | ],
63 | },
64 | 'Check Other City': {
65 | Type: 'Choice',
66 | InputPath: '$.event',
67 | Choices: [
68 | {
69 | And: [
70 | {
71 | Variable: '$.region.id',
72 | NumberEquals: 478,
73 | },
74 | ],
75 | Next: 'Wait for six hours',
76 | },
77 | {
78 | And: [
79 | {
80 | Variable: '$.region.id',
81 | NumberEquals: 999,
82 | },
83 | ],
84 | Next: 'Wait for twenty four hours',
85 | },
86 | ],
87 | },
88 | 'SMS for Dispatch accepted': {
89 | Type: 'Pass',
90 | InputPath: '$.event',
91 | Result: {
92 | expirationMinutes: 60,
93 | fromUserUUID: '71af5aea-9eaa-45a1-9825-2c124030b063',
94 | toUserUUID: 'Eval($.supplyUUID)',
95 | getSMSReply: false,
96 | message:
97 | 'Hithawath Partner, Oba labegena athi mema trip eka UberGALLE trip ekaki, Karunakara rider wa amatha drop location eka confirm karaganna. Sthuthi',
98 | messageType: 'SEND_SMS',
99 | priority: 1,
100 | actionUUID: 'd259c34d-457a-411e-8c93-6edd63a7ddc6',
101 | },
102 | ResultPath: '$.actionParam',
103 | Next: 'Send SMS',
104 | },
105 | 'SMS for Dispatch denied': {
106 | Type: 'Pass',
107 | InputPath: '$.event',
108 | Result: {
109 | expirationMinutes: 60,
110 | fromUserUUID: '71af5aea-9eaa-45a1-9825-2c124030b063',
111 | toUserUUID: 'Eval($.supplyUUID)',
112 | getSMSReply: false,
113 | message:
114 | 'Hithawath Partner, Oba labegena athi mema trip eka UberGALLE trip ekaki, Karunakara rider wa amatha drop location eka confirm karaganna. Sthuthi',
115 | messageType: 'SEND_SMS',
116 | priority: 1,
117 | actionUUID: 'd259c34d-457a-411e-8c93-6edd63a7ddc6',
118 | },
119 | ResultPath: '$.actionParam',
120 | Next: 'Send SMS',
121 | },
122 | 'Send SMS': {
123 | Type: 'Task',
124 | InputPath: '$.actionParam',
125 | Resource: 'uns://sjc1/sjc1-prod01/us1/cleopatra/Cleopatra::sendSMS',
126 | InputSchema: {
127 | '$.expirationMinutes': 'int',
128 | '$.toUserUUID': 'string',
129 | '$.fromUserUUID': 'string',
130 | '$.getSMSReply': 'bool',
131 | '$.message': 'string',
132 | '$.messageType': 'string',
133 | '$.priority': 'int',
134 | '$.actionUUID': 'string',
135 | },
136 | OutputSchema: {
137 | '$.fraudDriverUUIDs[*]': 'string',
138 | },
139 | Next: 'Check Other City',
140 | },
141 | 'Wait for six hours': {
142 | Type: 'Wait',
143 | Seconds: 21600,
144 | Next: 'Exit',
145 | },
146 | 'Wait for twenty four hours': {
147 | Type: 'Wait',
148 | Seconds: 86400,
149 | Next: 'Exit',
150 | },
151 | Exit: {
152 | Type: 'Terminator',
153 | End: true,
154 | },
155 | },
156 | };
157 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-digraph",
3 | "description": "directed graph react component",
4 | "version": "6.6.3",
5 | "keywords": [
6 | "uber-library",
7 | "babel",
8 | "es6",
9 | "d3",
10 | "react",
11 | "graph",
12 | "digraph"
13 | ],
14 | "repository": {
15 | "type": "git",
16 | "url": "https://github.com/uber/react-digraph.git"
17 | },
18 | "bugs": {
19 | "url": "https://github.com/uber/react-digraph/issues/new",
20 | "email": "antonb@uber.com"
21 | },
22 | "engines": {
23 | "node": ">= 0.10.0"
24 | },
25 | "license": "MIT",
26 | "main": "dist/main.min.js",
27 | "types": "./typings/index.d.ts",
28 | "peerDependencies": {
29 | "react": "^16.4.1",
30 | "react-dom": "^16.4.1"
31 | },
32 | "dependencies": {
33 | "d3": "^5.7.0",
34 | "dagre": "^0.8.2",
35 | "fast-deep-equal": "^2.0.1",
36 | "html-react-parser": "^0.6.1",
37 | "kld-affine": "2.0.4",
38 | "kld-intersections": "^0.4.3",
39 | "line-intersect": "^2.1.1",
40 | "svg-intersections": "^0.4.0"
41 | },
42 | "devDependencies": {
43 | "@fortawesome/fontawesome-free": "^5.7.2",
44 | "babel-cli": "^6.6.5",
45 | "babel-core": "^6.26.0",
46 | "babel-eslint": "^10.0.1",
47 | "babel-jest": "^23.6.0",
48 | "babel-loader": "^7.1.5",
49 | "babel-plugin-react": "^1.0.0",
50 | "babel-plugin-transform-es2015-destructuring": "^6.23.0",
51 | "babel-plugin-transform-flow-strip-types": "^6.22.0",
52 | "babel-plugin-transform-object-assign": "^6.8.0",
53 | "babel-plugin-transform-object-rest-spread": "^6.26.0",
54 | "babel-preset-env": "^1.7.0",
55 | "babel-preset-es2015": "^6.24.1",
56 | "babel-preset-react": "^6.24.1",
57 | "babel-preset-stage-2": "^6.24.1",
58 | "brace": "^0.11.1",
59 | "browserify": "^14.4.0",
60 | "copy-webpack-plugin": "^4.5.2",
61 | "css-loader": "^1.0.0",
62 | "enzyme": "3.8.0",
63 | "enzyme-adapter-react-16": "^1.10.0",
64 | "eslint": "^5.2.0",
65 | "eslint-config-fusion": "^6.0.0",
66 | "eslint-config-prettier": "^4.3.0",
67 | "eslint-loader": "^2.1.0",
68 | "eslint-plugin-cup": "^2.0.1",
69 | "eslint-plugin-es6-recommended": "^0.1.2",
70 | "eslint-plugin-flowtype": "^2.50.0",
71 | "eslint-plugin-import": "^2.14.0",
72 | "eslint-plugin-import-order": "^2.1.4",
73 | "eslint-plugin-jest": "^22.6.4",
74 | "eslint-plugin-jsx-a11y": "^6.1.1",
75 | "eslint-plugin-prettier": "^3.1.0",
76 | "eslint-plugin-react": "^7.5.1",
77 | "eslint-plugin-react-hooks": "^1.6.0",
78 | "flow-bin": "^0.86.0",
79 | "husky": "^2.4.0",
80 | "jest": "^22.4.3",
81 | "jsdom": "^11.12.0",
82 | "lint-staged": "^8.2.0",
83 | "live-server": "^1.2.0",
84 | "node-sass": "^4.9.2",
85 | "npm-run-all": "^4.1.3",
86 | "opn-cli": "3.1.0",
87 | "prettier": "^1.12.0",
88 | "prop-types": "^15.6.0",
89 | "react": "^16.4.2",
90 | "react-ace": "^6.1.4",
91 | "react-dom": "^16.4.2",
92 | "react-router-dom": "^4.3.1",
93 | "rimraf": "^2.6.2",
94 | "sass-loader": "^7.0.3",
95 | "source-map-loader": "^0.2.3",
96 | "style-loader": "^0.23.1",
97 | "svg-inline-loader": "^0.8.0",
98 | "uglifyjs-webpack-plugin": "^2.0.1",
99 | "webpack": "^4.26.1",
100 | "webpack-bundle-analyzer": "3.6.0",
101 | "webpack-cli": "^3.1.2"
102 | },
103 | "scripts": {
104 | "build": "webpack",
105 | "build:prod": "webpack --config webpack.prod.js",
106 | "clean": "rimraf ./dist",
107 | "watch": "webpack --watch",
108 | "build-css": "node-sass --include-path scss src/styles/main.scss dist/styles/main.css && node-sass --include-path scss src/examples/app.scss dist/examples/app.css",
109 | "cover": "npm run test",
110 | "example": "npm run serve",
111 | "flow": "flow .",
112 | "jenkins-install": "npm install",
113 | "jenkins-jshint": "npm run lint -- --o=jshint.xml --f=checkstyle",
114 | "jenkins-test": "npm run jenkins-jshint && npm run test",
115 | "live-server": "live-server ./dist --entry-file=./index.html",
116 | "live-serve": "npm-run-all --parallel watch live-server",
117 | "lint": "eslint src",
118 | "lint-fix": "eslint --fix src",
119 | "precommit": "lint-staged && npm run test",
120 | "prefast-test": "npm run prepublish",
121 | "prepublish": "npm run package",
122 | "serve": "npm run live-serve",
123 | "test": "jest",
124 | "test:debug": "node --inspect node_modules/.bin/jest --watch --runInBand",
125 | "view-cover": "npm run cover -- --report=html && opn ./coverage/index.html",
126 | "package": "npm-run-all clean lint test build build:prod",
127 | "analyze-bundle": "babel-node ./tools/analyzeBundle.js"
128 | },
129 | "jest": {
130 | "testURL": "http://localhost",
131 | "moduleFileExtensions": [
132 | "ts",
133 | "tsx",
134 | "js"
135 | ],
136 | "transformIgnorePatterns": [
137 | "node_modules"
138 | ],
139 | "testMatch": [
140 | "**/__tests__/**/*.+(ts|tsx|js)"
141 | ],
142 | "collectCoverage": true,
143 | "coverageDirectory": "/coverage",
144 | "collectCoverageFrom": [
145 | "**/src/**/*.{js,ts,tsx}",
146 | "!**/node_modules/**",
147 | "!**/vendor/**",
148 | "!**/*.d.ts",
149 | "!**/examples/**"
150 | ],
151 | "coverageReporters": [
152 | "json",
153 | "lcov",
154 | "text",
155 | "html",
156 | "cobertura"
157 | ],
158 | "setupTestFrameworkScriptFile": "/jest-setup.js",
159 | "moduleNameMapper": {
160 | "\\.(scss)$": "/__mocks__/styles.mock.js",
161 | "@fortawesome/fontawesome-free/svgs/solid/expand.svg": "/__mocks__/icon.mock.js"
162 | }
163 | },
164 | "publishConfig": {
165 | "registry": "https://registry.npmjs.org"
166 | }
167 | }
168 |
--------------------------------------------------------------------------------
/src/styles/main.scss:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright(c) 2018 Uber Technologies, Inc.
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 |
17 | $primary-color: dodgerblue;
18 | $light-color: white;
19 | $dark-color: black;
20 | $light-grey: lightgrey;
21 | $task-done-color: lightgreen;
22 | $task-in-progress-color: lightblue;
23 |
24 | $background-color: #f9f9f9;
25 |
26 | .view-wrapper {
27 | height: 100%;
28 | width: 100%;
29 | margin: 0;
30 | display: flex;
31 | box-shadow: none;
32 | background: $background-color;
33 | transition: opacity 0.167s;
34 | opacity: 1;
35 | outline: none;
36 | user-select: none;
37 |
38 | > .graph {
39 | align-content: stretch;
40 | flex: 1;
41 | width: 100%;
42 | height: 100%;
43 | }
44 |
45 | .node-todo {
46 | .shape {
47 | > use.node-todo {
48 | color: $primary-color;
49 | stroke: $dark-color;
50 | fill: $light-color;
51 | filter: url(#dropshadow);
52 | stroke-width: 0.5px;
53 | cursor: pointer;
54 | user-select: none;
55 |
56 | &.hovered {
57 | stroke: $primary-color;
58 | }
59 | &.selected {
60 | color: $light-color;
61 | stroke: $primary-color;
62 | stroke-width: 1px;
63 | fill: $primary-color;
64 | }
65 | }
66 | }
67 |
68 | .node-text {
69 | fill: $dark-color;
70 | cursor: pointer;
71 | user-select: none;
72 | &.selected {
73 | fill: $light-color;
74 | stroke: $light-color;
75 | }
76 | }
77 | }
78 |
79 | .node-in-progress {
80 | .shape {
81 | > use.node-in-progress {
82 | color: $primary-color;
83 | stroke: $dark-color;
84 | fill: $task-in-progress-color;
85 | filter: url(#dropshadow);
86 | stroke-width: 0.5px;
87 | cursor: pointer;
88 | user-select: none;
89 |
90 | &.hovered {
91 | stroke: $primary-color;
92 | }
93 | &.selected {
94 | color: $light-color;
95 | stroke: $primary-color;
96 | stroke-width: 1px;
97 | fill: $primary-color;
98 | }
99 | }
100 | }
101 |
102 | .node-text {
103 | fill: $dark-color;
104 | cursor: pointer;
105 | user-select: none;
106 | &.selected {
107 | fill: $light-color;
108 | stroke: $light-color;
109 | }
110 | }
111 | }
112 |
113 | .node-done {
114 | .shape {
115 | > use.node-done {
116 | color: $primary-color;
117 | stroke: $dark-color;
118 | fill: $task-done-color;
119 | filter: url(#dropshadow);
120 | stroke-width: 0.5px;
121 | cursor: pointer;
122 | user-select: none;
123 |
124 | &.hovered {
125 | stroke: $primary-color;
126 | }
127 | &.selected {
128 | color: $light-color;
129 | stroke: $primary-color;
130 | stroke-width: 1px;
131 | fill: $primary-color;
132 | }
133 | }
134 | }
135 |
136 | .node-text {
137 | fill: $dark-color;
138 | cursor: pointer;
139 | user-select: none;
140 | &.selected {
141 | fill: $light-color;
142 | stroke: $light-color;
143 | }
144 | }
145 | }
146 |
147 | .edge {
148 | color: $light-color;
149 | stroke: $primary-color;
150 | stroke-width: 2px;
151 | marker-end: url(#end-arrow);
152 | cursor: pointer;
153 |
154 | .edge-text {
155 | stroke-width: 0.5px;
156 | fill: $primary-color;
157 | stroke: $primary-color;
158 |
159 | cursor: pointer;
160 | user-select: none;
161 | }
162 |
163 | &.selected {
164 | color: $primary-color;
165 | stroke: $primary-color;
166 |
167 | .edge-text {
168 | fill: $light-color;
169 | stroke: $light-color;
170 | }
171 | }
172 |
173 |
174 | }
175 |
176 | .edge-mouse-handler {
177 | stroke: black;
178 | opacity: 0;
179 | color: transparent;
180 | stroke-width: 15px;
181 | cursor: pointer;
182 | pointer-events: all;
183 | }
184 |
185 | .arrow {
186 | fill: $primary-color;
187 | }
188 |
189 | .graph-controls {
190 | position: absolute;
191 | bottom: 30px;
192 | left: 15px;
193 | z-index: 100;
194 | display: grid;
195 | grid-template-columns: auto auto;
196 | grid-gap: 15px;
197 | align-items: center;
198 | user-select: none;
199 |
200 | > .slider-wrapper {
201 | background-color: white;
202 | color: $primary-color;
203 | border: solid 1px lightgray;
204 | padding: 6.5px;
205 | border-radius: 2px;
206 |
207 | > span {
208 | display: inline-block;
209 | vertical-align: top;
210 | margin-top: 4px;
211 | }
212 |
213 | > .slider {
214 | position: relative;
215 | margin-left: 4px;
216 | margin-right: 4px;
217 | cursor: pointer;
218 | }
219 | }
220 |
221 | > .slider-button {
222 | background-color: white;
223 | fill: $primary-color;
224 | border: solid 1px lightgray;
225 | outline: none;
226 | width: 31px;
227 | height: 31px;
228 | border-radius: 2px;
229 | cursor: pointer;
230 | margin: 0;
231 | }
232 | }
233 |
234 | .circle {
235 | fill: $light-grey;
236 | }
237 | }
238 |
--------------------------------------------------------------------------------
/src/examples/bwdl/index.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | /*
3 | Copyright(c) 2018 Uber Technologies, Inc.
4 |
5 | Licensed under the Apache License, Version 2.0 (the "License");
6 | you may not use this file except in compliance with the License.
7 | You may obtain a copy of the License at
8 |
9 | http://www.apache.org/licenses/LICENSE-2.0
10 |
11 | Unless required by applicable law or agreed to in writing, software
12 | distributed under the License is distributed on an "AS IS" BASIS,
13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | See the License for the specific language governing permissions and
15 | limitations under the License.
16 | */
17 |
18 | import * as React from 'react';
19 | import AceEditor from 'react-ace';
20 | import 'brace/mode/json';
21 | import 'brace/theme/monokai';
22 | import { type IEdge } from '../../components/edge';
23 | import GraphView from '../../components/graph-view';
24 | import { type INode } from '../../components/node';
25 | import { type LayoutEngineType } from '../../utilities/layout-engine/layout-engine-types';
26 | import BwdlTransformer from '../../utilities/transformers/bwdl-transformer';
27 | import Sidebar from '../sidebar';
28 | import GraphConfig, { NODE_KEY } from './bwdl-config'; // Configures node/edge types
29 | import bwdlExample from './bwdl-example-data';
30 | import BwdlNodeForm from './bwdl-node-form';
31 |
32 | type IBwdlState = {
33 | nodes: INode[],
34 | edges: IEdge[],
35 | selected: INode | IEdge | null,
36 | layoutEngineType: LayoutEngineType,
37 | bwdlText: string,
38 | bwdlJson: any,
39 | copiedNode: any,
40 | selectedBwdlNode: any,
41 | };
42 |
43 | class Bwdl extends React.Component<{}, IBwdlState> {
44 | GraphView: GraphView | null;
45 |
46 | constructor(props: any) {
47 | super(props);
48 |
49 | const transformed = BwdlTransformer.transform(bwdlExample);
50 |
51 | this.state = {
52 | bwdlJson: bwdlExample,
53 | bwdlText: JSON.stringify(bwdlExample, null, 2),
54 | copiedNode: null,
55 | edges: transformed.edges,
56 | layoutEngineType: 'VerticalTree',
57 | nodes: transformed.nodes,
58 | selected: null,
59 | selectedBwdlNode: null,
60 | };
61 | }
62 |
63 | updateBwdl = () => {
64 | const transformed = BwdlTransformer.transform(this.state.bwdlJson);
65 |
66 | this.setState({
67 | edges: transformed.edges,
68 | nodes: transformed.nodes,
69 | });
70 | };
71 |
72 | handleTextAreaChange = (value: string, event: any) => {
73 | let input = null;
74 | const bwdlText = value;
75 |
76 | this.setState({
77 | bwdlText,
78 | });
79 |
80 | try {
81 | input = JSON.parse(bwdlText);
82 | } catch (e) {
83 | return;
84 | }
85 |
86 | this.setState({
87 | bwdlJson: input,
88 | });
89 |
90 | this.updateBwdl();
91 | };
92 |
93 | onSelectNode = (node: INode | null) => {
94 | this.setState({
95 | selected: node,
96 | selectedBwdlNode: node ? this.state.bwdlJson.States[node.title] : null,
97 | });
98 | };
99 |
100 | onCreateNode = () => {
101 | return;
102 | };
103 | onUpdateNode = () => {
104 | return;
105 | };
106 | onDeleteNode = () => {
107 | return;
108 | };
109 | onSelectEdge = () => {
110 | return;
111 | };
112 | onCreateEdge = () => {
113 | return;
114 | };
115 | onSwapEdge = () => {
116 | return;
117 | };
118 | onDeleteEdge = () => {
119 | return;
120 | };
121 |
122 | renderLeftSidebar() {
123 | return (
124 |
125 |
145 |
146 | );
147 | }
148 |
149 | renderRightSidebar() {
150 | if (!this.state.selected) {
151 | return null;
152 | }
153 |
154 | return (
155 |
156 |
157 |
162 |
163 |
164 | );
165 | }
166 |
167 | renderGraph() {
168 | const { nodes, edges, selected } = this.state;
169 | const { NodeTypes, NodeSubtypes, EdgeTypes } = GraphConfig;
170 |
171 | return (
172 | (this.GraphView = el)}
174 | nodeKey={NODE_KEY}
175 | readOnly={true}
176 | nodes={nodes}
177 | edges={edges}
178 | selected={selected}
179 | nodeTypes={NodeTypes}
180 | nodeSubtypes={NodeSubtypes}
181 | edgeTypes={EdgeTypes}
182 | onSelectNode={this.onSelectNode}
183 | onCreateNode={this.onCreateNode}
184 | onUpdateNode={this.onUpdateNode}
185 | onDeleteNode={this.onDeleteNode}
186 | onSelectEdge={this.onSelectEdge}
187 | onCreateEdge={this.onCreateEdge}
188 | onSwapEdge={this.onSwapEdge}
189 | onDeleteEdge={this.onDeleteEdge}
190 | layoutEngineType={this.state.layoutEngineType}
191 | />
192 | );
193 | }
194 |
195 | render() {
196 | return (
197 |
198 | {this.renderLeftSidebar()}
199 |
{this.renderGraph()}
200 | {this.state.selected && this.renderRightSidebar()}
201 |
202 | );
203 | }
204 | }
205 |
206 | export default Bwdl;
207 |
--------------------------------------------------------------------------------
/typings/index.d.ts:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright(c) 2018 Uber Technologies, Inc.
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License");
5 | you may not use this file except in compliance with the License.
6 | You may obtain a copy of the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS,
12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | See the License for the specific language governing permissions and
14 | limitations under the License.
15 | */
16 |
17 | declare module 'react-digraph' {
18 | export type INode = {
19 | title: string;
20 | x?: number | null;
21 | y?: number | null;
22 | type?: string;
23 | subtype?: string | null;
24 | [key: string]: any;
25 | };
26 |
27 | export type IPoint = {
28 | x: number;
29 | y: number;
30 | };
31 |
32 | export type IBBox = {
33 | x: number,
34 | y: number,
35 | width: number,
36 | height: number,
37 | };
38 |
39 | export type INodeProps = {
40 | data: INode;
41 | id: string;
42 | nodeTypes: any; // TODO: make a nodeTypes interface
43 | nodeSubtypes: any; // TODO: make a nodeSubtypes interface
44 | opacity?: number;
45 | nodeKey: string;
46 | nodeSize?: number;
47 | onNodeMouseEnter: (event: any, data: any, hovered: boolean) => void;
48 | onNodeMouseLeave: (event: any, data: any) => void;
49 | onNodeMove: (point: IPoint, id: string, shiftKey: boolean) => void;
50 | onNodeSelected: (data: any, id: string, shiftKey: boolean) => void;
51 | onNodeUpdate: (point: IPoint, id: string, shiftKey: boolean) => void;
52 | renderNode?: (
53 | nodeRef: any,
54 | data: any,
55 | id: string,
56 | selected: boolean,
57 | hovered: boolean
58 | ) => any;
59 | renderNodeText?: (
60 | data: any,
61 | id: string | number,
62 | isSelected: boolean
63 | ) => any;
64 | isSelected: boolean;
65 | layoutEngine?: any;
66 | viewWrapperElem: HTMLDivElement;
67 | };
68 |
69 | export const Node: React.ComponentClass;
70 |
71 | export type IEdge = {
72 | source: string;
73 | target: string;
74 | type?: string;
75 | handleText?: string;
76 | handleTooltipText?: string;
77 | [key: string]: any;
78 | };
79 |
80 | export type ITargetPosition = {
81 | x: number;
82 | y: number;
83 | };
84 |
85 | export type IEdgeProps = {
86 | data: IEdge;
87 | edgeTypes: any; // TODO: create an edgeTypes interface
88 | edgeHandleSize?: number;
89 | nodeSize?: number;
90 | sourceNode: INode | null;
91 | targetNode: INode | ITargetPosition;
92 | isSelected: boolean;
93 | nodeKey: string;
94 | viewWrapperElem: HTMLDivElement;
95 | };
96 |
97 | export const Edge: React.Component;
98 |
99 | export type IGraphViewProps = {
100 | backgroundFillId?: string;
101 | edges: any[];
102 | edgeArrowSize?: number;
103 | edgeHandleSize?: number;
104 | edgeTypes: any;
105 | gridDotSize?: number;
106 | gridSize?: number;
107 | gridSpacing?: number;
108 | layoutEngineType?: LayoutEngineType;
109 | maxTitleChars?: number;
110 | maxZoom?: number;
111 | minZoom?: number;
112 | nodeKey: string;
113 | nodes: any[];
114 | nodeSize?: number;
115 | nodeHeight?: number,
116 | nodeWidth?: number,
117 | nodeSpacingMultiplier?: number,
118 | nodeSubtypes: any;
119 | nodeTypes: any;
120 | readOnly?: boolean;
121 | selected: any;
122 | showGraphControls?: boolean;
123 | zoomDelay?: number;
124 | zoomDur?: number;
125 | canCreateEdge?: (startNode?: INode, endNode?: INode) => boolean;
126 | canDeleteEdge?: (selected: any) => boolean;
127 | canDeleteNode?: (selected: any) => boolean;
128 | onBackgroundClick?: (x: number, y: number, event: any) => void,
129 | onCopySelected?: () => void;
130 | onCreateEdge: (sourceNode: INode, targetNode: INode) => void;
131 | onCreateNode: (x: number, y: number, event: any) => void;
132 | onDeleteEdge: (selectedEdge: IEdge, edges: IEdge[]) => void;
133 | onDeleteNode: (selected: any, nodeId: string, nodes: any[]) => void;
134 | onPasteSelected?: () => void;
135 | onSelectEdge: (selectedEdge: IEdge) => void;
136 | onSelectNode: (node: INode | null) => void;
137 | onSwapEdge: (sourceNode: INode, targetNode: INode, edge: IEdge) => void;
138 | onUndo?: () => void;
139 | onUpdateNode: (node: INode) => void;
140 | renderBackground?: (gridSize?: number) => any;
141 | renderDefs?: () => any;
142 | renderNode?: (
143 | nodeRef: any,
144 | data: any,
145 | id: string,
146 | selected: boolean,
147 | hovered: boolean
148 | ) => any;
149 | afterRenderEdge?: (
150 | id: string,
151 | element: any,
152 | edge: IEdge,
153 | edgeContainer: any,
154 | isEdgeSelected: boolean
155 | ) => void;
156 | renderNodeText?: (
157 | data: any,
158 | id: string | number,
159 | isSelected: boolean
160 | ) => any;
161 | rotateEdgeHandle?: boolean;
162 | centerNodeOnMove?: boolean;
163 | initialBBox: IBBox;
164 | };
165 |
166 | export type IGraphInput = {
167 | nodes: INode[];
168 | edges: IEdge[];
169 | };
170 |
171 | export class BwdlTransformer extends Transformer {}
172 |
173 | export class Transformer {
174 | /**
175 | * Converts an input from the specified type to IGraphInput type.
176 | * @param input
177 | * @returns IGraphInput
178 | */
179 | static transform(input: any): IGraphInput;
180 |
181 | /**
182 | * Converts a graphInput to the specified transformer type.
183 | * @param graphInput
184 | * @returns any
185 | */
186 | static revert(graphInput: IGraphInput): any;
187 | }
188 |
189 | export type LayoutEngineType = 'None' | 'SnapToGrid' | 'VerticalTree';
190 |
191 | export const GraphView: React.ComponentClass;
192 | export type INodeMapNode = {
193 | node: INode;
194 | originalArrIndex: number;
195 | incomingEdges: IEdge[];
196 | outgoingEdges: IEdge[];
197 | parents: INode[];
198 | children: INode[];
199 | };
200 |
201 | type ObjectMap = { [key: string]: T };
202 |
203 | export type NodesMap = ObjectMap;
204 |
205 | export type EdgesMap = ObjectMap;
206 |
207 | export interface IEdgeMapNode {
208 | edge: IEdge;
209 | originalArrIndex: number;
210 | }
211 |
212 | export type Element = any;
213 |
214 | export class GraphUtils {
215 | static getNodesMap(arr: INode[], key: string): NodesMap;
216 |
217 | static getEdgesMap(arr: IEdge[]): EdgesMap;
218 |
219 | static linkNodesAndEdges(nodesMap: NodesMap, edges: IEdge[]): void;
220 |
221 | static removeElementFromDom(id: string): boolean;
222 |
223 | static findParent(element: Element, selector: string): Element | null;
224 |
225 | static classNames(...args: any[]): string;
226 |
227 | static yieldingLoop(
228 | count: number,
229 | chunksize: number,
230 | callback: (i: number) => void,
231 | finished?: () => void
232 | ): void;
233 |
234 | static hasNodeShallowChanged(prevNode: INode, newNode: INode): boolean;
235 |
236 | static isEqual(prevNode: any, newNode: any): boolean;
237 | }
238 | }
239 |
--------------------------------------------------------------------------------
/__tests__/components/graph-util.test.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import GraphUtils from '../../src/utilities/graph-util';
4 |
5 | describe('GraphUtils class', () => {
6 | describe('getNodesMap method', () => {
7 | it('converts an array of nodes to a hash map', () => {
8 | const nodes = [
9 | {
10 | id: 'foo',
11 | name: 'bar',
12 | },
13 | ];
14 | const nodesMap = GraphUtils.getNodesMap(nodes, 'id');
15 |
16 | expect(JSON.stringify(nodesMap)).toEqual(
17 | JSON.stringify({
18 | 'key-foo': {
19 | children: [],
20 | incomingEdges: [],
21 | node: nodes[0],
22 | originalArrIndex: 0,
23 | outgoingEdges: [],
24 | parents: [],
25 | },
26 | })
27 | );
28 | });
29 | });
30 |
31 | describe('getEdgesMap method', () => {
32 | it('converts an array of edges to a hash map', () => {
33 | const edges = [
34 | {
35 | source: 'foo',
36 | target: 'bar',
37 | },
38 | ];
39 | const edgesMap = GraphUtils.getEdgesMap(edges);
40 |
41 | expect(JSON.stringify(edgesMap)).toEqual(
42 | JSON.stringify({
43 | foo_bar: {
44 | edge: edges[0],
45 | originalArrIndex: 0,
46 | },
47 | })
48 | );
49 | });
50 | });
51 |
52 | describe('linkNodesAndEdges method', () => {
53 | let nodesMap;
54 |
55 | beforeEach(() => {
56 | nodesMap = {
57 | 'key-bar': {
58 | children: [],
59 | incomingEdges: [],
60 | node: { id: 'bar' },
61 | originalArrIndex: 0,
62 | outgoingEdges: [],
63 | parents: [],
64 | },
65 | 'key-foo': {
66 | children: [],
67 | incomingEdges: [],
68 | node: { id: 'foo' },
69 | originalArrIndex: 0,
70 | outgoingEdges: [],
71 | parents: [],
72 | },
73 | };
74 | });
75 |
76 | it('fills in various properties of a nodeMapNode', () => {
77 | const edges = [
78 | {
79 | source: 'foo',
80 | target: 'bar',
81 | },
82 | ];
83 |
84 | GraphUtils.linkNodesAndEdges(nodesMap, edges);
85 |
86 | expect(nodesMap['key-bar'].incomingEdges.length).toEqual(1);
87 | expect(nodesMap['key-bar'].incomingEdges[0]).toEqual(edges[0]);
88 | expect(nodesMap['key-foo'].outgoingEdges.length).toEqual(1);
89 | expect(nodesMap['key-foo'].outgoingEdges[0]).toEqual(edges[0]);
90 | expect(nodesMap['key-foo'].children.length).toEqual(1);
91 | expect(nodesMap['key-foo'].children[0]).toEqual(nodesMap['key-bar']);
92 | expect(nodesMap['key-bar'].parents.length).toEqual(1);
93 | expect(nodesMap['key-bar'].parents[0]).toEqual(nodesMap['key-foo']);
94 | });
95 |
96 | it('does not modify nodes if there is no matching target', () => {
97 | const edges = [
98 | {
99 | source: 'foo',
100 | target: 'fake',
101 | },
102 | ];
103 |
104 | GraphUtils.linkNodesAndEdges(nodesMap, edges);
105 |
106 | expect(nodesMap['key-foo'].outgoingEdges.length).toEqual(0);
107 | expect(nodesMap['key-foo'].children.length).toEqual(0);
108 | });
109 |
110 | it('does not modify nodes if there is no matching source', () => {
111 | const edges = [
112 | {
113 | source: 'fake',
114 | target: 'bar',
115 | },
116 | ];
117 |
118 | GraphUtils.linkNodesAndEdges(nodesMap, edges);
119 |
120 | expect(nodesMap['key-bar'].incomingEdges.length).toEqual(0);
121 | expect(nodesMap['key-bar'].parents.length).toEqual(0);
122 | });
123 | });
124 |
125 | describe('removeElementFromDom method', () => {
126 | it('removes an element using an id', () => {
127 | const fakeElement = {
128 | parentNode: {
129 | removeChild: jest.fn(),
130 | },
131 | };
132 |
133 | jest.spyOn(document, 'getElementById').mockReturnValue(fakeElement);
134 | const result = GraphUtils.removeElementFromDom('fake');
135 |
136 | expect(fakeElement.parentNode.removeChild).toHaveBeenCalledWith(
137 | fakeElement
138 | );
139 | expect(result).toEqual(true);
140 | });
141 |
142 | it("does nothing when it can't find the element", () => {
143 | jest.spyOn(document, 'getElementById').mockReturnValue(undefined);
144 | const result = GraphUtils.removeElementFromDom('fake');
145 |
146 | expect(result).toEqual(false);
147 | });
148 | });
149 |
150 | describe('findParent method', () => {
151 | it('returns the element if an element matches a selector', () => {
152 | const element = {
153 | matches: jest.fn().mockReturnValue(true),
154 | };
155 | const parent = GraphUtils.findParent(element, 'fake');
156 |
157 | expect(parent).toEqual(element);
158 | });
159 |
160 | it('returns the parent if an element contains a parentNode property', () => {
161 | const element = {
162 | parentNode: {
163 | matches: jest.fn().mockReturnValue(true),
164 | },
165 | };
166 | const parent = GraphUtils.findParent(element, 'fake');
167 |
168 | expect(parent).toEqual(element.parentNode);
169 | });
170 |
171 | it('returns null when there is no match', () => {
172 | const element = {
173 | parentNode: {
174 | matches: jest.fn().mockReturnValue(false),
175 | },
176 | };
177 | const parent = GraphUtils.findParent(element, 'fake');
178 |
179 | expect(parent).toEqual(null);
180 | });
181 | });
182 |
183 | describe('classNames static method', () => {
184 | it('handles multiple string-based arguments', () => {
185 | const result = GraphUtils.classNames('test', 'hello');
186 |
187 | expect(result).toEqual('test hello');
188 | });
189 |
190 | it('handles a string and an array', () => {
191 | const result = GraphUtils.classNames('test', ['hello', 'world']);
192 |
193 | expect(result).toEqual('test hello world');
194 | });
195 |
196 | it('handles a string and object', () => {
197 | const result = GraphUtils.classNames('test', {
198 | hello: true,
199 | world: false,
200 | });
201 |
202 | expect(result).toEqual('test hello');
203 | });
204 | });
205 |
206 | describe('hasNodeShallowChanged', () => {
207 | it('calls isEqual', () => {
208 | jest.spyOn(GraphUtils, 'isEqual');
209 | const node1 = { x: 0, y: 1 };
210 | const node2 = { x: 0, y: 1 };
211 |
212 | GraphUtils.hasNodeShallowChanged(node1, node2);
213 |
214 | expect(GraphUtils.isEqual).toHaveBeenCalled();
215 | });
216 |
217 | it('does not find differences in 2 objects', () => {
218 | const node1 = { x: 0, y: 1 };
219 | const node2 = { x: 0, y: 1 };
220 | const changed = GraphUtils.hasNodeShallowChanged(node1, node2);
221 |
222 | expect(changed).toEqual(false);
223 | });
224 | });
225 |
226 | describe('isEqual', () => {
227 | it('finds differences in 2 objects', () => {
228 | const node1 = { x: 0, y: 1 };
229 | const node2 = { x: 1, y: 2 };
230 | const changed = GraphUtils.hasNodeShallowChanged(node1, node2);
231 |
232 | expect(changed).toEqual(true);
233 | });
234 |
235 | it('does not find differences in 2 objects', () => {
236 | const node1 = { x: 0, y: 1 };
237 | const node2 = { x: 0, y: 1 };
238 | const changed = GraphUtils.hasNodeShallowChanged(node1, node2);
239 |
240 | expect(changed).toEqual(false);
241 | });
242 | });
243 | });
244 |
--------------------------------------------------------------------------------
/src/examples/bwdl-editable/index.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | /*
3 | Copyright(c) 2018 Uber Technologies, Inc.
4 |
5 | Licensed under the Apache License, Version 2.0 (the "License");
6 | you may not use this file except in compliance with the License.
7 | You may obtain a copy of the License at
8 |
9 | http://www.apache.org/licenses/LICENSE-2.0
10 |
11 | Unless required by applicable law or agreed to in writing, software
12 | distributed under the License is distributed on an "AS IS" BASIS,
13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | See the License for the specific language governing permissions and
15 | limitations under the License.
16 | */
17 |
18 | import * as React from 'react';
19 | import AceEditor from 'react-ace';
20 | import 'brace/mode/json';
21 | import 'brace/theme/monokai';
22 | import { type IEdge } from '../../components/edge';
23 | import GraphView from '../../components/graph-view';
24 | import { type INode } from '../../components/node';
25 | import { type LayoutEngineType } from '../../utilities/layout-engine/layout-engine-types';
26 | import BwdlTransformer from '../../utilities/transformers/bwdl-transformer';
27 | import Sidebar from '../sidebar';
28 | import GraphConfig, { EMPTY_TYPE, NODE_KEY } from './bwdl-config'; // Configures node/edge types
29 | import bwdlExample from './bwdl-example-data';
30 |
31 | type IBwdlState = {
32 | nodes: INode[],
33 | edges: IEdge[],
34 | selected: INode | IEdge | null,
35 | layoutEngineType: LayoutEngineType,
36 | bwdlText: string,
37 | bwdlJson: any,
38 | copiedNode: any,
39 | };
40 |
41 | class BwdlEditable extends React.Component<{}, IBwdlState> {
42 | GraphView: GraphView | null;
43 |
44 | constructor(props: any) {
45 | super(props);
46 |
47 | const transformed = BwdlTransformer.transform(bwdlExample);
48 |
49 | this.state = {
50 | bwdlJson: bwdlExample,
51 | bwdlText: JSON.stringify(bwdlExample, null, 2),
52 | copiedNode: null,
53 | edges: transformed.edges,
54 | layoutEngineType: 'VerticalTree',
55 | nodes: transformed.nodes,
56 | selected: null,
57 | };
58 | }
59 |
60 | linkEdge(sourceNode: INode, targetNode: INode, edge?: IEdge) {
61 | const newBwdlJson = {
62 | ...this.state.bwdlJson,
63 | };
64 | const sourceNodeBwdl = newBwdlJson.States[sourceNode.title];
65 |
66 | if (sourceNodeBwdl.Type === 'Choice') {
67 | const newChoice = {
68 | Next: targetNode.title,
69 | };
70 |
71 | if (sourceNodeBwdl.Choices) {
72 | // check if swapping edge
73 | let swapped = false;
74 |
75 | if (edge) {
76 | sourceNodeBwdl.Choices.forEach(choice => {
77 | if (edge && choice.Next === edge.target) {
78 | choice.Next = targetNode.title;
79 | swapped = true;
80 | }
81 | });
82 | }
83 |
84 | if (!swapped) {
85 | sourceNodeBwdl.Choices.push(newChoice);
86 | }
87 | } else {
88 | sourceNodeBwdl.Choices = [newChoice];
89 | }
90 | } else {
91 | sourceNodeBwdl.Next = targetNode.title;
92 | }
93 |
94 | this.setState({
95 | bwdlJson: newBwdlJson,
96 | bwdlText: JSON.stringify(newBwdlJson, null, 2),
97 | });
98 | this.updateBwdl();
99 | }
100 |
101 | onSelectNode = (node: INode | null) => {
102 | this.setState({
103 | selected: node,
104 | });
105 | };
106 |
107 | onCreateNode = (x: number, y: number) => {
108 | const newBwdlJson = {
109 | ...this.state.bwdlJson,
110 | };
111 |
112 | newBwdlJson.States[`New Item ${Date.now()}`] = {
113 | Type: EMPTY_TYPE,
114 | x,
115 | y,
116 | };
117 | this.setState({
118 | bwdlJson: newBwdlJson,
119 | bwdlText: JSON.stringify(newBwdlJson, null, 2),
120 | });
121 | this.updateBwdl();
122 | };
123 | onUpdateNode = (node: INode) => {
124 | return;
125 | };
126 |
127 | onDeleteNode = (selected: INode, nodeId: string, nodes: any[]) => {
128 | const newBwdlJson = {
129 | ...this.state.bwdlJson,
130 | };
131 |
132 | delete newBwdlJson.States[selected.title];
133 | this.setState({
134 | bwdlJson: newBwdlJson,
135 | bwdlText: JSON.stringify(newBwdlJson, null, 2),
136 | });
137 | this.updateBwdl();
138 | };
139 |
140 | onSelectEdge = (edge: IEdge) => {
141 | this.setState({
142 | selected: edge,
143 | });
144 | };
145 |
146 | onCreateEdge = (sourceNode: INode, targetNode: INode) => {
147 | this.linkEdge(sourceNode, targetNode);
148 | };
149 |
150 | onSwapEdge = (sourceNode: INode, targetNode: INode, edge: IEdge) => {
151 | this.linkEdge(sourceNode, targetNode, edge);
152 | };
153 |
154 | onDeleteEdge = (selectedEdge: IEdge, edges: IEdge[]) => {
155 | const newBwdlJson = {
156 | ...this.state.bwdlJson,
157 | };
158 | const sourceNodeBwdl = newBwdlJson.States[selectedEdge.source];
159 |
160 | if (sourceNodeBwdl.Choices) {
161 | sourceNodeBwdl.Choices = sourceNodeBwdl.Choices.filter(choice => {
162 | return choice.Next !== selectedEdge.target;
163 | });
164 | } else {
165 | delete sourceNodeBwdl.Next;
166 | }
167 |
168 | this.setState({
169 | bwdlJson: newBwdlJson,
170 | bwdlText: JSON.stringify(newBwdlJson, null, 2),
171 | });
172 | this.updateBwdl();
173 | };
174 |
175 | onUndo() {
176 | alert('Undo is not supported yet.');
177 | }
178 |
179 | onCopySelected = () => {
180 | const { selected, bwdlJson } = this.state;
181 |
182 | if (!selected) {
183 | return;
184 | }
185 |
186 | const original = bwdlJson.States[selected.title];
187 | const newItem = JSON.parse(JSON.stringify(original));
188 |
189 | this.setState({
190 | copiedNode: newItem,
191 | });
192 | };
193 |
194 | onPasteSelected = () => {
195 | const { copiedNode, bwdlJson } = this.state;
196 |
197 | bwdlJson.States[`New Item ${Date.now()}`] = copiedNode;
198 |
199 | const newBwdlJson = {
200 | ...bwdlJson,
201 | };
202 |
203 | this.setState({
204 | bwdlJson: newBwdlJson,
205 | bwdlText: JSON.stringify(newBwdlJson, null, 2),
206 | });
207 | this.updateBwdl();
208 | };
209 |
210 | updateBwdl = () => {
211 | const transformed = BwdlTransformer.transform(this.state.bwdlJson);
212 |
213 | this.setState({
214 | edges: transformed.edges,
215 | nodes: transformed.nodes,
216 | });
217 | };
218 |
219 | handleTextAreaChange = (value: string, event: any) => {
220 | let input = null;
221 | const bwdlText = value;
222 |
223 | this.setState({
224 | bwdlText,
225 | });
226 |
227 | try {
228 | input = JSON.parse(bwdlText);
229 | } catch (e) {
230 | return;
231 | }
232 |
233 | this.setState({
234 | bwdlJson: input,
235 | });
236 |
237 | this.updateBwdl();
238 | };
239 |
240 | renderSidebar() {
241 | return (
242 |
243 |
263 |
264 | );
265 | }
266 |
267 | renderGraph() {
268 | const { nodes, edges, selected } = this.state;
269 | const { NodeTypes, NodeSubtypes, EdgeTypes } = GraphConfig;
270 |
271 | return (
272 | (this.GraphView = el)}
274 | nodeKey={NODE_KEY}
275 | nodes={nodes}
276 | edges={edges}
277 | selected={selected}
278 | nodeTypes={NodeTypes}
279 | nodeSubtypes={NodeSubtypes}
280 | edgeTypes={EdgeTypes}
281 | onSelectNode={this.onSelectNode}
282 | onCreateNode={this.onCreateNode}
283 | onUpdateNode={this.onUpdateNode}
284 | onDeleteNode={this.onDeleteNode}
285 | onSelectEdge={this.onSelectEdge}
286 | onCreateEdge={this.onCreateEdge}
287 | onSwapEdge={this.onSwapEdge}
288 | onDeleteEdge={this.onDeleteEdge}
289 | onUndo={this.onUndo}
290 | onCopySelected={this.onCopySelected}
291 | onPasteSelected={this.onPasteSelected}
292 | layoutEngineType={this.state.layoutEngineType}
293 | />
294 | );
295 | }
296 |
297 | render() {
298 | return (
299 |
300 | {this.renderSidebar()}
301 |
{this.renderGraph()}
302 |
303 | );
304 | }
305 | }
306 |
307 | export default BwdlEditable;
308 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 |
2 | # Contributing to react-digraph
3 |
4 | :+1::tada: First off, thanks for taking the time to contribute! :tada::+1:
5 |
6 | The following is a set of guidelines for contributing to react-digraph, which are hosted in the [Uber Organization](https://github.com/uber) on GitHub. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request.
7 |
8 | ## Code of Conduct
9 |
10 | We ask that everyone participating in this project be respectful, non-discriminatory, and maintain decorum and diplomacy. We are here to code and contribute to the world at large, and that means we must respect all individuals. We also respect discussion and differing opinions, but please remember to keep those opinions civil and based on the technology and code, never personalized. We also ask everyone participating to learn and have fun!
11 |
12 | ## How Can I Contribute?
13 |
14 | ### Reporting Bugs
15 |
16 | Bug reports are essential to keeping react-digraph stable.
17 |
18 | First, [go to the issues tab](https://github.com/uber/react-digraph/issues) and make sure to search for your issue in case it has already been answered. Be sure to check [Closed issues](https://github.com/uber/react-digraph/issues?q=is%3Aissue+is%3Aclosed) as well.
19 |
20 | If the issue is not already present, then click the [New Issue](https://github.com/uber/react-digraph/issues/new/choose) button. You will be presented with some options, either you can create a bug report or a feature request.
21 |
22 | When creating [a new bug report](https://github.com/uber/react-digraph/issues/new?template=bug_report.md) you will notice some instructions, such as description, reproduction steps (aka repro steps), expected behavior, screenshots, OS type, browser, and version (which refers to your react-digraph version), and some additional context. Please fill in as much as possible, but not all of it is required. The more information we have the better we can help.
23 |
24 | Sometimes it's necessary to create an example demo for the developers. We recommend [plnkr.co](https://plnkr.co/), [jsfiddle](http://jsfiddle.net/), or [codepen.io](https://codepen.io/). We ask that you limit your example to the bare minimum amount of code which reproduces your issue. You can also [create a Gist in Github](https://gist.github.com/), which will allow us to see the code but we won't be able to run it.
25 |
26 | ### Requesting Features
27 |
28 | Feature requests help drive the development of our project. Since this project is also driven by Uber goals, as it's under the Uber umbrella, some features may be added by internal teams. Hopefully all developers create feature requests in Github in order to make the public aware of the design decisions, but unfortunately sometimes that is missed.
29 |
30 | If you think react-digraph needs a new feature, please create a new Feature request by [going to the issues tab](https://github.com/uber/react-digraph/issues). Again, make sure to search for your issue in case it has already been answered. Be sure to check [Closed issues](https://github.com/uber/react-digraph/issues?q=is%3Aissue+is%3Aclosed) as well.
31 |
32 | If the issue is not already present, then click the [New Issue](https://github.com/uber/react-digraph/issues/new/choose) button. You will be presented with some options, either you can create a bug report or a feature request.
33 |
34 | When creating [a new feature request](https://github.com/uber/react-digraph/issues/new?template=feature_request.md) you will notice some instructions, such as relation to a problem, solution you'd like, alternatives, and some additional context. Please fill in as much as possible, but not all of it is required. The more information we have the better we can help.
35 |
36 | ### Your First Code Contribution
37 |
38 | #### Setup
39 |
40 | In order to work on react-digraph you will need to fork the project to your user account in Github. Navigate to [react-digraph's main page](https://github.com/uber/react-digraph), then press the **Fork** button in the upper right. If the fork is successful you should see the URL and project name switch from "**uber/react-digraph**" to "**yourusername/react-digraph**".
41 |
42 |
43 | First you will need to download the project and install the project dependencies. These instructions are based on [using a remote upstream repository](https://medium.com/sweetmeat/how-to-keep-a-downstream-git-repository-current-with-upstream-repository-changes-10b76fad6d97).
44 |
45 | ```bash
46 | git clone git@github.com:yourusername/react-digraph.git
47 | cd react-digraph
48 | git remote add upstream git@github.com:uber/react-digraph.git # adds the parent repository as 'upstream'
49 | git fetch upstream
50 | npm install
51 | ```
52 |
53 | #### Creating a working branch
54 |
55 | Ideally, all work should be done on a working branch. This branch is then referenced when creating a Pull Request (PR).
56 |
57 | First, you must rebase your own master on upstream's master.
58 |
59 | ```bash
60 | git fetch upstream
61 | git checkout master
62 | git rebase upstream/master
63 | ```
64 |
65 | ```bash
66 | git checkout -b my_new_feature # use any naming convention you want
67 | ```
68 |
69 | Some people like to reference the issue number if their pull request is related to a bug or feature request. When doing so you should make sure your commit tells Github that you've fixed the issue in reference.
70 |
71 | ```bash
72 | git checkout -b 71-fix-click-issue # use any naming convention you want
73 | # make changes
74 | git add .
75 | git commit -m "Resolved #71"
76 | ```
77 |
78 | #### Using the example site
79 |
80 | react-digraph includes a simple example site. Every time the webpage is refreshed the data will reset. We would love more examples, so feel free to add more pages or modifications to suit your use cases.
81 |
82 | The site should automatically open in the browser, and upon making changes to the code it should automatically refresh the page.
83 |
84 | ```bash
85 | npm run example
86 | ```
87 |
88 | #### Linking to react-digraph
89 |
90 | By using npm linking you can point your website or project to a local version of react-digraph. Then you can make changes within react-digraph and, after a restart of your app, you can see the changes in your website.
91 |
92 | Clone the website using the instructions above. Then use the following commands.
93 |
94 | ```bash
95 | cd react-digraph
96 | npm link
97 |
98 | cd /path/to/your/project
99 | npm link react-digraph
100 | ```
101 |
102 | **Note:** Once you've linked a package within your project, you cannot run `npm install react-digraph` without breaking the link. If you break the link you should run `npm link react-digraph` again within your project directory. Your project's `package.json` file may be modified by npm when linking packages, be careful when submitting your code to a repository.
103 |
104 | Now that the project is linked to your local react-digraph you may modify react-digraph and see the changes in your project.
105 |
106 | ```bash
107 | # make modifications to react-digraph then run
108 | cd react-digraph # make sure you're in the react-digraph directory
109 | npm run package # this runs the linter, tests, and builds a production distribution file
110 | ```
111 |
112 | Now you may stop your project's server and restart it to see the changes in your project.
113 |
114 | #### Creating tests
115 |
116 | Please make sure to test all of your code. We would prefer 100% code coverage. All tests are located in the `__tests__` folder, and all mocks in the `__mocks__` folder.
117 |
118 | Tests are created using [Jest](https://jestjs.io/) and [Enzyme](https://github.com/airbnb/enzyme). See the documentation on those projects for help. Use the existing examples in `__tests__` to see the structure and other examples.
119 |
120 | Test file and folder structure is as follows:
121 |
122 | ```
123 | __tests__
124 | - components
125 | my-component.test.js
126 | - utilities
127 | - layout-engine
128 | snap-to-grid.test.js
129 | ```
130 |
131 | The components under the `__tests__` folder should match the folder structure in `src`.
132 | If you are more comfortable creating E2E tests, please create a `__tests__/e2e` folder and place them there.
133 |
134 |
135 | #### Committing code
136 |
137 | We ask that you limit the number of commits to a reasonable amount. If you're comfortable with [squashing your commits](https://github.com/todotxt/todo.txt-android/wiki/Squash-All-Commits-Related-to-a-Single-Issue-into-a-Single-Commit), please do that, otherwise you should be careful with how many commits you are making.
138 |
139 | ```
140 | git add . #add your changes
141 | git commit -m "Resolved #71"
142 | git push origin 71-fix-click-issue # use whatever branch name you're on
143 | ```
144 |
145 | Navigate to Github and select your new branch.
146 |
147 | Press the "New pull request" button.
148 |
149 | You should see a comparison with base: `master` with `yourusername/react-digraph` compare: `71-fix-click-issue`.
150 |
151 | **Note:** If you performed a `git checkout -b` based on the react-digraph `v4.x.x` branch, then change base: `master` to `v4.x.x` instead.
152 |
153 | ## Testing
154 |
155 | As mentioned before, react-digraph uses Jest and Enzyme. Tests are located in the `__tests__` folder.
156 |
157 | To run the tests run `npm run test`.
158 |
159 | # NPM Package Maintainers Only!
160 |
161 | ## Creating a new version
162 |
163 | **Checkout master and pull updates**
164 |
165 | ```
166 | git checkout master
167 | git pull
168 | ```
169 |
170 | **Create a new version**
171 |
172 | Create a new version using `npm version [major|minor|patch]` depending on the version type. `major` for major breaking changes, `minor` for non-breaking backwards-compatible changes that introduce new features or improvements, `patch` for bugfixes.
173 |
174 | ```
175 | npm version minor
176 | npm publish
177 | ```
178 |
179 | **Push updates**
180 |
181 | ```
182 | git push origin master
183 | git push origin --tags
184 | ```
--------------------------------------------------------------------------------
/src/components/node.js:
--------------------------------------------------------------------------------
1 | // @flow
2 | /*
3 | Copyright(c) 2018 Uber Technologies, Inc.
4 |
5 | Licensed under the Apache License, Version 2.0 (the "License");
6 | you may not use this file except in compliance with the License.
7 | You may obtain a copy of the License at
8 |
9 | http://www.apache.org/licenses/LICENSE-2.0
10 |
11 | Unless required by applicable law or agreed to in writing, software
12 | distributed under the License is distributed on an "AS IS" BASIS,
13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | See the License for the specific language governing permissions and
15 | limitations under the License.
16 | */
17 |
18 | import * as d3 from 'd3';
19 | import * as React from 'react';
20 | // This works in Typescript but causes an import loop for Flowtype. We'll just use `any` below.
21 | // import { type LayoutEngine } from '../utilities/layout-engine/layout-engine-config';
22 | import Edge from './edge';
23 | import GraphUtils from '../utilities/graph-util';
24 | import NodeText from './node-text';
25 |
26 | export type IPoint = {
27 | x: number,
28 | y: number,
29 | };
30 |
31 | export type INode = {
32 | title: string,
33 | description: string,
34 | timeEstimate: number,
35 | status: string,
36 | totalTimeEstimate?: number | null,
37 | x?: number | null,
38 | y?: number | null,
39 | type?: string | null,
40 | subtype?: string | null,
41 | [key: string]: any,
42 | };
43 |
44 | export const Status = {
45 | todo: "todo",
46 | inProgress: "in-progress",
47 | done: "done"
48 | };
49 |
50 | type INodeProps = {
51 | data: INode,
52 | id: string,
53 | nodeTypes: any, // TODO: make a nodeTypes interface
54 | nodeSubtypes: any, // TODO: make a nodeSubtypes interface
55 | opacity?: number,
56 | nodeKey: string,
57 | nodeSize?: number,
58 | onNodeMouseEnter: (event: any, data: any, hovered: boolean) => void,
59 | onNodeMouseLeave: (event: any, data: any) => void,
60 | onNodeMove: (point: IPoint, id: string, shiftKey: boolean) => void,
61 | onNodeSelected: (
62 | data: any,
63 | id: string,
64 | shiftKey: boolean,
65 | event?: any
66 | ) => void,
67 | onNodeUpdate: (point: IPoint, id: string, shiftKey: boolean) => void,
68 | renderNode?: (
69 | nodeRef: any,
70 | data: any,
71 | id: string,
72 | selected: boolean,
73 | hovered: boolean
74 | ) => any,
75 | renderNodeText?: (data: any, id: string | number, isSelected: boolean) => any,
76 | isSelected: boolean,
77 | layoutEngine?: any,
78 | viewWrapperElem: HTMLDivElement,
79 | centerNodeOnMove: boolean,
80 | maxTitleChars: number,
81 | };
82 |
83 | type INodeState = {
84 | hovered: boolean,
85 | x: number,
86 | y: number,
87 | selected: boolean,
88 | mouseDown: boolean,
89 | drawingEdge: boolean,
90 | pointerOffset: ?{ x: number, y: number },
91 | };
92 |
93 | class Node extends React.Component {
94 | static defaultProps = {
95 | isSelected: false,
96 | nodeSize: 200,
97 | maxTitleChars: 30,
98 | onNodeMouseEnter: () => {
99 | return;
100 | },
101 | onNodeMouseLeave: () => {
102 | return;
103 | },
104 | onNodeMove: () => {
105 | return;
106 | },
107 | onNodeSelected: () => {
108 | return;
109 | },
110 | onNodeUpdate: () => {
111 | return;
112 | },
113 | centerNodeOnMove: true,
114 | };
115 |
116 | static getDerivedStateFromProps(
117 | nextProps: INodeProps,
118 | prevState: INodeState
119 | ) {
120 | return {
121 | selected: nextProps.isSelected,
122 | x: nextProps.data.x,
123 | y: nextProps.data.y,
124 | };
125 | }
126 |
127 | nodeRef: any;
128 | oldSibling: any;
129 |
130 | constructor(props: INodeProps) {
131 | super(props);
132 |
133 | this.state = {
134 | drawingEdge: false,
135 | hovered: false,
136 | mouseDown: false,
137 | selected: false,
138 | x: props.data.x || 0,
139 | y: props.data.y || 0,
140 | pointerOffset: null,
141 | };
142 |
143 | this.nodeRef = React.createRef();
144 | }
145 |
146 | componentDidMount() {
147 | const dragFunction = d3
148 | .drag()
149 | .on('drag', () => {
150 | this.handleMouseMove(d3.event);
151 | })
152 | .on('start', this.handleDragStart)
153 | .on('end', () => {
154 | this.handleDragEnd(d3.event);
155 | });
156 |
157 | d3.select(this.nodeRef.current)
158 | .on('mouseout', this.handleMouseOut)
159 | .call(dragFunction);
160 | }
161 |
162 | handleMouseMove = (event: any) => {
163 | const mouseButtonDown = event.sourceEvent.buttons === 1;
164 | const shiftKey = event.sourceEvent.shiftKey;
165 | const {
166 | nodeSize,
167 | layoutEngine,
168 | nodeKey,
169 | viewWrapperElem,
170 | data,
171 | } = this.props;
172 | const {pointerOffset} = this.state;
173 |
174 | if (!mouseButtonDown) {
175 | return;
176 | }
177 |
178 | // While the mouse is down, this function handles all mouse movement
179 | const newState = {
180 | x: event.x,
181 | y: event.y,
182 | pointerOffset,
183 | };
184 |
185 | if (!this.props.centerNodeOnMove) {
186 | newState.pointerOffset = pointerOffset || {
187 | x: event.x - (data.x || 0),
188 | y: event.y - (data.y || 0),
189 | };
190 | newState.x -= newState.pointerOffset.x;
191 | newState.y -= newState.pointerOffset.y;
192 | }
193 |
194 | if (shiftKey) {
195 | this.setState({drawingEdge: true});
196 | // draw edge
197 | // undo the target offset subtraction done by Edge
198 | const off = Edge.calculateOffset(
199 | nodeSize,
200 | this.props.data,
201 | newState,
202 | nodeKey,
203 | true,
204 | viewWrapperElem
205 | );
206 |
207 | newState.x += off.xOff;
208 | newState.y += off.yOff;
209 | // now tell the graph that we're actually drawing an edge
210 | } else if (!this.state.drawingEdge && layoutEngine) {
211 | // move node using the layout engine
212 | Object.assign(newState, layoutEngine.getPositionForNode(newState));
213 | }
214 |
215 | this.setState(newState);
216 | this.props.onNodeMove(newState, this.props.data[nodeKey], shiftKey);
217 | };
218 |
219 | handleDragStart = () => {
220 | if (!this.nodeRef.current) {
221 | return;
222 | }
223 |
224 | if (!this.oldSibling) {
225 | this.oldSibling = this.nodeRef.current.parentElement.nextSibling;
226 | }
227 |
228 | // Moves child to the end of the element stack to re-arrange the z-index
229 | this.nodeRef.current.parentElement.parentElement.appendChild(
230 | this.nodeRef.current.parentElement
231 | );
232 | };
233 |
234 | handleDragEnd = (event: any) => {
235 | if (!this.nodeRef.current) {
236 | return;
237 | }
238 |
239 | const {x, y, drawingEdge} = this.state;
240 | const {data, nodeKey, onNodeSelected, onNodeUpdate} = this.props;
241 | const {sourceEvent} = event;
242 |
243 | this.setState({
244 | mouseDown: false,
245 | drawingEdge: false,
246 | pointerOffset: null,
247 | });
248 |
249 | if (this.oldSibling && this.oldSibling.parentElement) {
250 | this.oldSibling.parentElement.insertBefore(
251 | this.nodeRef.current.parentElement,
252 | this.oldSibling
253 | );
254 | }
255 |
256 | const shiftKey = sourceEvent.shiftKey;
257 |
258 | onNodeUpdate({x, y}, data[nodeKey], shiftKey || drawingEdge);
259 |
260 | onNodeSelected(data, data[nodeKey], shiftKey || drawingEdge, sourceEvent);
261 | };
262 |
263 | handleMouseOver = (event: any) => {
264 | // Detect if mouse is already down and do nothing.
265 | let hovered = false;
266 |
267 | if (event && event.buttons !== 1) {
268 | hovered = true;
269 | this.setState({hovered});
270 | }
271 |
272 | this.props.onNodeMouseEnter(event, this.props.data, hovered);
273 | };
274 |
275 | handleMouseOut = (event: any) => {
276 | // Detect if mouse is already down and do nothing. Sometimes the system lags on
277 | // drag and we don't want the mouseOut to fire while the user is moving the
278 | // node around
279 |
280 | this.setState({hovered: false});
281 | this.props.onNodeMouseLeave(event, this.props.data);
282 | };
283 |
284 | static getNodeTypeXlinkHref(data: INode, nodeTypes: any) {
285 | if (data.type && nodeTypes[data.type]) {
286 | return nodeTypes[data.type].shapeId;
287 | } else if (nodeTypes.emptyNode) {
288 | return nodeTypes.emptyNode.shapeId;
289 | }
290 |
291 | return null;
292 | }
293 |
294 | static getNodeSubtypeXlinkHref(data: INode, nodeSubtypes?: any) {
295 | if (data.subtype && nodeSubtypes && nodeSubtypes[data.subtype]) {
296 | return nodeSubtypes[data.subtype].shapeId;
297 | } else if (nodeSubtypes && nodeSubtypes.emptyNode) {
298 | return nodeSubtypes.emptyNode.shapeId;
299 | }
300 |
301 | return null;
302 | }
303 |
304 | renderShape(nodeClassName) {
305 | const {renderNode, data, nodeTypes, nodeSubtypes, nodeKey} = this.props;
306 | const {hovered, selected} = this.state;
307 | const props = {
308 | height: this.props.nodeSize || 0,
309 | width: this.props.nodeSize || 0,
310 | };
311 | const nodeShapeContainerClassName = GraphUtils.classNames('shape');
312 | const nodeTypeXlinkHref = Node.getNodeTypeXlinkHref(data, nodeTypes) || '';
313 | // get width and height defined on def element
314 | const defSvgNodeElement: any = nodeTypeXlinkHref
315 | ? document.querySelector(`defs>${nodeTypeXlinkHref}`)
316 | : null;
317 |
318 | const nodeWidthAttr = defSvgNodeElement
319 | ? defSvgNodeElement.getAttribute('width')
320 | : 0;
321 | const nodeHeightAttr = defSvgNodeElement
322 | ? defSvgNodeElement.getAttribute('height')
323 | : 0;
324 |
325 | props.width = nodeWidthAttr ? parseInt(nodeWidthAttr, 10) : props.width;
326 | props.height = nodeHeightAttr ? parseInt(nodeHeightAttr, 10) : props.height;
327 |
328 | if (renderNode) {
329 | // Originally: graphView, domNode, datum, index, elements.
330 | return renderNode(this.nodeRef, data, data[nodeKey], selected, hovered);
331 | } else {
332 | return (
333 |
334 |
342 |
343 | );
344 | }
345 | }
346 |
347 | renderText() {
348 | const {
349 | data,
350 | id,
351 | nodeTypes,
352 | renderNodeText,
353 | isSelected,
354 | maxTitleChars,
355 | } = this.props;
356 |
357 | if (renderNodeText) {
358 | return renderNodeText(data, id, isSelected);
359 | }
360 |
361 | return (
362 |
368 | );
369 | }
370 |
371 | render() {
372 | const {x, y, hovered, selected} = this.state;
373 | const {opacity, id, data} = this.props;
374 | const className = GraphUtils.classNames('node-' + data.status, data.type, {
375 | hovered,
376 | selected,
377 | });
378 |
379 | console.log(className)
380 |
381 |
382 | return (
383 |
393 | {this.renderShape(className)}
394 | {this.renderText()}
395 |
396 | );
397 | }
398 | }
399 |
400 | export default Node;
401 |
--------------------------------------------------------------------------------
/__tests__/components/node.test.js:
--------------------------------------------------------------------------------
1 | // @flow
2 |
3 | import * as React from 'react';
4 | import { shallow, ShallowWrapper } from 'enzyme';
5 | import Node from '../../src/components/node';
6 | import NodeText from '../../src/components/node-text';
7 |
8 | // jest.mock('d3');
9 |
10 | describe('Node component', () => {
11 | let output = null;
12 | let nodeData;
13 | let nodeTypes;
14 | let nodeSubtypes;
15 | let onNodeMouseEnter;
16 | let onNodeMouseLeave;
17 | let onNodeMove;
18 | let onNodeSelected;
19 | let onNodeUpdate;
20 |
21 | beforeEach(() => {
22 | nodeData = {
23 | uuid: '1',
24 | title: 'Test',
25 | type: 'emptyNode',
26 | x: 5,
27 | y: 10,
28 | };
29 |
30 | nodeTypes = {
31 | emptyNode: {
32 | shapeId: '#test',
33 | },
34 | };
35 |
36 | nodeSubtypes = {};
37 |
38 | onNodeMouseEnter = jest.fn();
39 | onNodeMouseLeave = jest.fn();
40 | onNodeMove = jest.fn();
41 | onNodeSelected = jest.fn();
42 | onNodeUpdate = jest.fn();
43 |
44 | jest.spyOn(document, 'querySelector').mockReturnValue({
45 | getAttribute: jest.fn().mockReturnValue(100),
46 | getBoundingClientRect: jest.fn().mockReturnValue({
47 | width: 0,
48 | height: 0,
49 | }),
50 | });
51 |
52 | // this gets around d3 being readonly, we need to customize the event object
53 | // const globalEvent = {
54 | // sourceEvent: {},
55 | // };
56 |
57 | output = shallow(
58 |
73 | );
74 |
75 | // Object.defineProperty(d3, 'event', {
76 | // get: () => {
77 | // return globalEvent;
78 | // },
79 | // set: event => {
80 | // globalEvent = event;
81 | // },
82 | // });
83 | });
84 |
85 | describe('render method', () => {
86 | it('renders', () => {
87 | expect(output.props().className).toEqual('node emptyNode');
88 | expect(output.props().transform).toEqual('translate(5, 10)');
89 |
90 | const nodeShape = output.find('.shape > use');
91 |
92 | expect(nodeShape.props().className).toEqual('node');
93 | expect(nodeShape.props().x).toEqual(-50);
94 | expect(nodeShape.props().y).toEqual(-50);
95 | expect(nodeShape.props().width).toEqual(100);
96 | expect(nodeShape.props().height).toEqual(100);
97 | expect(nodeShape.props().xlinkHref).toEqual('#test');
98 |
99 | const nodeText = output.find(NodeText);
100 |
101 | expect(nodeText.length).toEqual(1);
102 | });
103 |
104 | it('calls handleMouseOver', () => {
105 | const event = {
106 | test: true,
107 | };
108 |
109 | output
110 | .find('g.node')
111 | .props()
112 | .onMouseOver(event);
113 | expect(onNodeMouseEnter).toHaveBeenCalledWith(event, nodeData, true);
114 | });
115 |
116 | it('calls handleMouseOut', () => {
117 | const event = {
118 | test: true,
119 | };
120 |
121 | output.setState({
122 | hovered: true,
123 | });
124 | output
125 | .find('g.node')
126 | .props()
127 | .onMouseOut(event);
128 | expect(onNodeMouseLeave).toHaveBeenCalledWith(event, nodeData);
129 | expect(output.state().hovered).toEqual(false);
130 | });
131 | });
132 |
133 | describe('renderText method', () => {
134 | let renderNodeText;
135 |
136 | beforeEach(() => {
137 | renderNodeText = jest.fn().mockReturnValue('success');
138 | });
139 |
140 | it('calls the renderNodeText callback', () => {
141 | output.setProps({
142 | renderNodeText,
143 | });
144 |
145 | const result = output.instance().renderText();
146 |
147 | expect(renderNodeText).toHaveBeenCalledWith(nodeData, 'test-node', false);
148 | expect(result).toEqual('success');
149 | });
150 |
151 | it('creates its own NodeText element', () => {
152 | const result = output.instance().renderText();
153 |
154 | expect(renderNodeText).not.toHaveBeenCalled();
155 | expect(result.type.prototype.constructor.name).toEqual('NodeText');
156 | });
157 | });
158 |
159 | describe('renderShape method', () => {
160 | let renderNode;
161 |
162 | beforeEach(() => {
163 | renderNode = jest.fn().mockReturnValue('success');
164 | });
165 |
166 | it('calls the renderNode callback', () => {
167 | output.setProps({
168 | renderNode,
169 | });
170 |
171 | const result = output.instance().renderShape();
172 |
173 | expect(renderNode).toHaveBeenCalledWith(
174 | output.instance().nodeRef,
175 | nodeData,
176 | '1',
177 | false,
178 | false
179 | );
180 | expect(result).toEqual('success');
181 | });
182 |
183 | it('returns a node shape without a subtype', () => {
184 | const result: ShallowWrapper = shallow(
185 | output.instance().renderShape()
186 | );
187 |
188 | expect(renderNode).not.toHaveBeenCalledWith();
189 | expect(result.props().className).toEqual('shape');
190 | expect(result.props().height).toEqual(100);
191 | expect(result.props().width).toEqual(100);
192 |
193 | const nodeShape = result.find('.node');
194 | const nodeSubtypeShape = result.find('.subtype-shape');
195 |
196 | expect(nodeShape.length).toEqual(1);
197 | expect(nodeSubtypeShape.length).toEqual(0);
198 |
199 | expect(nodeShape.props().className).toEqual('node');
200 | expect(nodeShape.props().x).toEqual(-50);
201 | expect(nodeShape.props().y).toEqual(-50);
202 | expect(nodeShape.props().width).toEqual(100);
203 | expect(nodeShape.props().height).toEqual(100);
204 | expect(nodeShape.props().xlinkHref).toEqual('#test');
205 | });
206 |
207 | it('returns a node shape with a subtype', () => {
208 | nodeData.subtype = 'fake';
209 | nodeSubtypes.fake = {
210 | shapeId: '#blah',
211 | };
212 | output.setProps({
213 | data: nodeData,
214 | nodeSubtypes,
215 | });
216 | const result: ShallowWrapper = shallow(
217 | output.instance().renderShape()
218 | );
219 | const nodeSubtypeShape = result.find('.subtype-shape');
220 |
221 | expect(nodeSubtypeShape.length).toEqual(1);
222 | expect(nodeSubtypeShape.props().className).toEqual('subtype-shape');
223 | expect(nodeSubtypeShape.props().x).toEqual(-50);
224 | expect(nodeSubtypeShape.props().y).toEqual(-50);
225 | expect(nodeSubtypeShape.props().width).toEqual(100);
226 | expect(nodeSubtypeShape.props().height).toEqual(100);
227 | expect(nodeSubtypeShape.props().xlinkHref).toEqual('#blah');
228 | });
229 | });
230 |
231 | describe('getNodeSubtypeXlinkHref method', () => {
232 | it('returns the shapeId from the nodeSubtypes object', () => {
233 | nodeData.subtype = 'fake';
234 | nodeSubtypes.fake = {
235 | shapeId: '#blah',
236 | };
237 |
238 | const result = Node.getNodeSubtypeXlinkHref(nodeData, nodeSubtypes);
239 |
240 | expect(result).toEqual('#blah');
241 | });
242 |
243 | it('returns the emptyNode shapeId from the nodeSubtypes object', () => {
244 | nodeSubtypes.emptyNode = {
245 | shapeId: '#empty',
246 | };
247 |
248 | const result = Node.getNodeSubtypeXlinkHref(nodeData, nodeSubtypes);
249 |
250 | expect(result).toEqual('#empty');
251 | });
252 |
253 | it('returns null', () => {
254 | const result = Node.getNodeSubtypeXlinkHref(nodeData, nodeSubtypes);
255 |
256 | expect(result).toEqual(null);
257 | });
258 | });
259 |
260 | describe('getNodeTypeXlinkHref method', () => {
261 | beforeEach(() => {
262 | nodeData.type = 'fake';
263 | });
264 |
265 | it('returns the shapeId from the nodeTypes object', () => {
266 | nodeTypes.fake = {
267 | shapeId: '#blah',
268 | };
269 |
270 | const result = Node.getNodeTypeXlinkHref(nodeData, nodeTypes);
271 |
272 | expect(result).toEqual('#blah');
273 | });
274 |
275 | it('returns the emptyNode shapeId from the nodeTypes object', () => {
276 | nodeTypes.emptyNode = {
277 | shapeId: '#empty',
278 | };
279 |
280 | const result = Node.getNodeTypeXlinkHref(nodeData, nodeTypes);
281 |
282 | expect(result).toEqual('#empty');
283 | });
284 |
285 | it('returns null', () => {
286 | delete nodeTypes.emptyNode;
287 | const result = Node.getNodeTypeXlinkHref(nodeData, nodeTypes);
288 |
289 | expect(result).toEqual(null);
290 | });
291 | });
292 |
293 | describe('handleMouseOut method', () => {
294 | it('sets hovered to false and calls the onNodeMouseLeave callback', () => {
295 | const event = {
296 | test: true,
297 | };
298 |
299 | output.setState({
300 | hovered: true,
301 | });
302 | output.instance().handleMouseOut(event);
303 | expect(output.state().hovered).toEqual(false);
304 | expect(onNodeMouseLeave).toHaveBeenCalledWith(event, nodeData);
305 | });
306 | });
307 |
308 | describe('handleMouseOver method', () => {
309 | it('calls the onNodeMouseEnter callback with the mouse down', () => {
310 | // this test cares about the passed-in event
311 | const event = {
312 | buttons: 1,
313 | };
314 |
315 | output.setState({
316 | hovered: false,
317 | });
318 | output.instance().handleMouseOver(event);
319 | expect(output.state().hovered).toEqual(false);
320 | expect(onNodeMouseEnter).toHaveBeenCalledWith(event, nodeData, false);
321 | });
322 |
323 | it('sets hovered to true when the mouse is not down', () => {
324 | const event = {
325 | buttons: 0,
326 | };
327 |
328 | output.setState({
329 | hovered: false,
330 | });
331 | output.instance().handleMouseOver(event);
332 | expect(output.state().hovered).toEqual(true);
333 | expect(onNodeMouseEnter).toHaveBeenCalledWith(event, nodeData, true);
334 | });
335 | });
336 |
337 | describe('handleDragEnd method', () => {
338 | it('updates and selects the node using the callbacks', () => {
339 | output.instance().nodeRef = {
340 | current: {
341 | parentElement: null,
342 | },
343 | };
344 |
345 | output.instance().handleDragEnd({
346 | sourceEvent: {
347 | shiftKey: true,
348 | },
349 | });
350 | expect(onNodeUpdate).toHaveBeenCalledWith({ x: 5, y: 10 }, '1', true);
351 | expect(onNodeSelected).toHaveBeenCalledWith(nodeData, '1', true, {
352 | shiftKey: true,
353 | });
354 | });
355 |
356 | it('moves the element back to the original DOM position', () => {
357 | const insertBefore = jest.fn();
358 |
359 | output.instance().nodeRef.current = {
360 | parentElement: 'blah',
361 | };
362 | output.instance().oldSibling = {
363 | parentElement: {
364 | insertBefore,
365 | },
366 | };
367 |
368 | output.instance().handleDragEnd({
369 | sourceEvent: {
370 | shiftKey: true,
371 | },
372 | });
373 | expect(insertBefore).toHaveBeenCalledWith(
374 | 'blah',
375 | output.instance().oldSibling
376 | );
377 | });
378 | });
379 |
380 | describe('handleDragStart method', () => {
381 | let grandparent;
382 | let parentElement;
383 |
384 | beforeEach(() => {
385 | grandparent = {
386 | appendChild: jest.fn(),
387 | };
388 | parentElement = {
389 | nextSibling: 'blah',
390 | parentElement: grandparent,
391 | };
392 | output.instance().nodeRef.current = {
393 | parentElement,
394 | };
395 | });
396 |
397 | it('assigns an oldSibling so that the element can be put back', () => {
398 | output.instance().nodeRef.current = {
399 | parentElement,
400 | };
401 |
402 | output.instance().handleDragStart();
403 |
404 | expect(output.instance().oldSibling).toEqual('blah');
405 | expect(grandparent).toEqual(grandparent);
406 | });
407 |
408 | it('moves the element in the DOM', () => {
409 | output.instance().oldSibling = {};
410 | output.instance().handleDragStart();
411 | expect(grandparent).toEqual(grandparent);
412 | });
413 | });
414 |
415 | describe('handleMouseMove method', () => {
416 | it('calls the onNodeMove callback', () => {
417 | output.instance().handleMouseMove({
418 | sourceEvent: {
419 | buttons: 0,
420 | },
421 | });
422 | expect(onNodeMove).not.toHaveBeenCalled();
423 | });
424 |
425 | it('calls the onNodeMove callback with the shiftKey pressed', () => {
426 | const event = {
427 | sourceEvent: {
428 | buttons: 1,
429 | shiftKey: true,
430 | },
431 | x: 20,
432 | y: 50,
433 | };
434 |
435 | output.instance().handleMouseMove(event);
436 | expect(onNodeMove).toHaveBeenCalledWith(
437 | { pointerOffset: null, x: 20, y: 50 },
438 | '1',
439 | true
440 | );
441 | });
442 |
443 | it('calls the onNodeMove callback with the shiftKey not pressed', () => {
444 | const event = {
445 | sourceEvent: {
446 | buttons: 1,
447 | shiftKey: false,
448 | },
449 | x: 20,
450 | y: 50,
451 | };
452 |
453 | output.instance().handleMouseMove(event);
454 | expect(onNodeMove).toHaveBeenCalledWith(
455 | { pointerOffset: null, x: 20, y: 50 },
456 | '1',
457 | false
458 | );
459 | });
460 |
461 | it('uses a layoutEngine to obtain a new position', () => {
462 | const layoutEngine = {
463 | getPositionForNode: jest.fn().mockImplementation(newState => {
464 | return {
465 | x: 100,
466 | y: 200,
467 | };
468 | }),
469 | };
470 |
471 | output.setProps({
472 | layoutEngine,
473 | });
474 |
475 | const event = {
476 | sourceEvent: {
477 | buttons: 1,
478 | shiftKey: false,
479 | },
480 | x: 20,
481 | y: 50,
482 | };
483 |
484 | output.instance().handleMouseMove(event);
485 |
486 | expect(onNodeMove).toHaveBeenCalledWith(
487 | { pointerOffset: null, x: 100, y: 200 },
488 | '1',
489 | false
490 | );
491 | });
492 | });
493 | });
494 |
--------------------------------------------------------------------------------