otomato-sdk 2.0.35 → 2.0.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/constants/version.js +1 -1
- package/dist/src/models/Workflow.js +126 -40
- package/dist/src/utils/WorkflowNodePositioner.js +77 -155
- package/dist/types/examples/UseCases/LendingAggregator/lending-aggregator-2-protocols.d.ts +1 -1
- package/dist/types/examples/UseCases/LendingAggregator/lending-aggregator-3-protocols.d.ts +4 -0
- package/dist/types/examples/UseCases/LendingAggregator/lending-aggregator-v3.d.ts +4 -0
- package/dist/types/examples/test.d.ts +7 -1
- package/dist/types/src/constants/version.d.ts +1 -1
- package/dist/types/src/models/Workflow.d.ts +36 -13
- package/dist/types/src/utils/WorkflowNodePositioner.d.ts +19 -11
- package/package.json +3 -1
|
@@ -10,8 +10,10 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
10
10
|
import { Node } from './Node.js';
|
|
11
11
|
import { Edge } from './Edge.js';
|
|
12
12
|
import { apiServices } from '../services/ApiService.js';
|
|
13
|
+
import { Action } from './Action.js';
|
|
13
14
|
import { Note } from './Note.js';
|
|
14
|
-
import { getEndNodePositions,
|
|
15
|
+
import { getEndNodePositions, positionWorkflowNodes } from '../utils/WorkflowNodePositioner.js';
|
|
16
|
+
import { ACTIONS } from '../constants/Blocks.js';
|
|
15
17
|
export class Workflow {
|
|
16
18
|
constructor(name = '', nodes = [], edges = []) {
|
|
17
19
|
this.id = null;
|
|
@@ -23,18 +25,18 @@ export class Workflow {
|
|
|
23
25
|
this.nodes = nodes;
|
|
24
26
|
this.edges = edges;
|
|
25
27
|
this.state = 'inactive';
|
|
26
|
-
|
|
28
|
+
positionWorkflowNodes(this);
|
|
27
29
|
}
|
|
28
30
|
setName(name) {
|
|
29
31
|
this.name = name;
|
|
30
32
|
}
|
|
31
33
|
addNode(node) {
|
|
32
34
|
this.nodes.push(node);
|
|
33
|
-
|
|
35
|
+
positionWorkflowNodes(this);
|
|
34
36
|
}
|
|
35
37
|
addNodes(nodes) {
|
|
36
38
|
this.nodes.push(...nodes);
|
|
37
|
-
|
|
39
|
+
positionWorkflowNodes(this);
|
|
38
40
|
}
|
|
39
41
|
deleteNode(nodeToDelete) {
|
|
40
42
|
// Remove the node from the nodes array
|
|
@@ -57,68 +59,152 @@ export class Workflow {
|
|
|
57
59
|
this.edges = this.edges.filter(edge => edge.source !== nodeToDelete && edge.target !== nodeToDelete);
|
|
58
60
|
this.edges.push(...newEdges);
|
|
59
61
|
// Recalculate positions
|
|
60
|
-
|
|
62
|
+
positionWorkflowNodes(this);
|
|
61
63
|
}
|
|
62
64
|
/**
|
|
63
|
-
* Inserts a new node into the workflow
|
|
64
|
-
* - If both nodeBefore and nodeAfter are provided, we expect an existing edge
|
|
65
|
-
* from nodeBefore -> nodeAfter, which gets replaced by two edges:
|
|
66
|
-
* nodeBefore -> nodeToInsert and nodeToInsert -> nodeAfter.
|
|
65
|
+
* Inserts a new node into the workflow, potentially splitting an existing edge.
|
|
67
66
|
*
|
|
68
|
-
* - If nodeAfter is
|
|
69
|
-
*
|
|
70
|
-
*
|
|
67
|
+
* - If `nodeAfter` is specified, we expect an edge nodeBefore->nodeAfter to exist.
|
|
68
|
+
* That edge is removed, and replaced by two new edges:
|
|
69
|
+
* nodeBefore->nodeToInsert (with `edgeLabelBefore`, `edgeValueBefore`)
|
|
70
|
+
* nodeToInsert->nodeAfter (with `edgeLabelAfter`, `edgeValueAfter`)
|
|
71
71
|
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* @param
|
|
76
|
-
* @param
|
|
72
|
+
* - If `nodeAfter` is not specified, we create only one edge:
|
|
73
|
+
* nodeBefore->nodeToInsert (with `edgeLabelBefore`, `edgeValueBefore`)
|
|
74
|
+
*
|
|
75
|
+
* @param nodeToInsert The node to insert
|
|
76
|
+
* @param nodeBefore The existing node that precedes `nodeToInsert`
|
|
77
|
+
* @param nodeAfter (Optional) The existing node that should follow `nodeToInsert`
|
|
78
|
+
* @param edgeLabelBefore (Optional) Label for the edge nodeBefore->nodeToInsert
|
|
79
|
+
* @param edgeValueBefore (Optional) Value for the edge nodeBefore->nodeToInsert
|
|
80
|
+
* @param edgeLabelAfter (Optional) Label for the edge nodeToInsert->nodeAfter
|
|
81
|
+
* @param edgeValueAfter (Optional) Value for the edge nodeToInsert->nodeAfter
|
|
77
82
|
*/
|
|
78
|
-
insertNode(nodeToInsert, nodeBefore, nodeAfter,
|
|
83
|
+
insertNode(nodeToInsert, nodeBefore, nodeAfter, edgeLabelBefore = null, edgeValueBefore = null, edgeLabelAfter = null, edgeValueAfter = null) {
|
|
79
84
|
// Ensure nodeBefore exists in the workflow
|
|
80
85
|
if (!this.nodes.includes(nodeBefore)) {
|
|
81
86
|
throw new Error('The nodeBefore must exist in the workflow.');
|
|
82
87
|
}
|
|
83
|
-
// If nodeAfter
|
|
84
|
-
if (nodeAfter && (edgeLabel !== null || edgeValue !== null)) {
|
|
85
|
-
throw new Error('Cannot provide edgeLabel/edgeValue if nodeAfter is specified.');
|
|
86
|
-
}
|
|
87
|
-
// If nodeAfter is not provided, insert the new node as a child of nodeBefore
|
|
88
|
+
// CASE A: If no nodeAfter => we create a single edge from nodeBefore to nodeToInsert
|
|
88
89
|
if (!nodeAfter) {
|
|
89
90
|
// Add the new node to the workflow
|
|
90
91
|
this.addNode(nodeToInsert);
|
|
91
|
-
//
|
|
92
|
+
// Create the edge nodeBefore->nodeToInsert
|
|
92
93
|
const newEdge = new Edge({
|
|
93
94
|
source: nodeBefore,
|
|
94
95
|
target: nodeToInsert,
|
|
95
|
-
label:
|
|
96
|
-
value:
|
|
96
|
+
label: edgeLabelBefore !== null && edgeLabelBefore !== void 0 ? edgeLabelBefore : undefined,
|
|
97
|
+
value: edgeValueBefore !== null && edgeValueBefore !== void 0 ? edgeValueBefore : undefined
|
|
97
98
|
});
|
|
98
99
|
this.addEdge(newEdge);
|
|
99
100
|
// Recalculate positions
|
|
100
|
-
|
|
101
|
+
positionWorkflowNodes(this);
|
|
101
102
|
return;
|
|
102
103
|
}
|
|
103
|
-
//
|
|
104
|
+
// CASE B: nodeAfter is provided => we expect an edge nodeBefore->nodeAfter to exist
|
|
104
105
|
if (!this.nodes.includes(nodeAfter)) {
|
|
105
106
|
throw new Error('The nodeAfter must exist in the workflow.');
|
|
106
107
|
}
|
|
107
|
-
//
|
|
108
|
-
const edgeBetween = this.edges.find(edge => edge.source === nodeBefore && edge.target === nodeAfter);
|
|
108
|
+
// Find the existing edge nodeBefore->nodeAfter
|
|
109
|
+
const edgeBetween = this.edges.find((edge) => edge.source === nodeBefore && edge.target === nodeAfter);
|
|
109
110
|
if (!edgeBetween) {
|
|
110
111
|
throw new Error('No edge exists between nodeBefore and nodeAfter.');
|
|
111
112
|
}
|
|
112
113
|
// Add the new node to the workflow
|
|
113
114
|
this.addNode(nodeToInsert);
|
|
114
115
|
// Remove the existing edge between nodeBefore and nodeAfter
|
|
115
|
-
this.edges = this.edges.filter(edge => edge !== edgeBetween);
|
|
116
|
-
//
|
|
117
|
-
|
|
118
|
-
const
|
|
116
|
+
this.edges = this.edges.filter((edge) => edge !== edgeBetween);
|
|
117
|
+
// Create the two new edges:
|
|
118
|
+
// 1) nodeBefore->nodeToInsert
|
|
119
|
+
const newEdge1 = new Edge({
|
|
120
|
+
source: nodeBefore,
|
|
121
|
+
target: nodeToInsert,
|
|
122
|
+
label: edgeLabelBefore !== null && edgeLabelBefore !== void 0 ? edgeLabelBefore : undefined,
|
|
123
|
+
value: edgeValueBefore !== null && edgeValueBefore !== void 0 ? edgeValueBefore : undefined
|
|
124
|
+
});
|
|
125
|
+
// 2) nodeToInsert->nodeAfter
|
|
126
|
+
const newEdge2 = new Edge({
|
|
127
|
+
source: nodeToInsert,
|
|
128
|
+
target: nodeAfter,
|
|
129
|
+
label: edgeLabelAfter !== null && edgeLabelAfter !== void 0 ? edgeLabelAfter : undefined,
|
|
130
|
+
value: edgeValueAfter !== null && edgeValueAfter !== void 0 ? edgeValueAfter : undefined
|
|
131
|
+
});
|
|
119
132
|
this.addEdges([newEdge1, newEdge2]);
|
|
120
133
|
// Recalculate positions
|
|
121
|
-
|
|
134
|
+
positionWorkflowNodes(this);
|
|
135
|
+
}
|
|
136
|
+
insertCondition(nodeToInsert, nodeBefore, nodeAfter, addElseCase, addEmptyNodes = true) {
|
|
137
|
+
if (nodeAfter) {
|
|
138
|
+
this.insertNode(nodeToInsert, nodeBefore, nodeAfter, null, null, 'true', 'true');
|
|
139
|
+
}
|
|
140
|
+
// Otherwise, create both "true" and "false" paths.
|
|
141
|
+
// 1) "true" path: nodeBefore -> nodeToInsert -> nodeAfter
|
|
142
|
+
if (!nodeAfter) {
|
|
143
|
+
this.insertNode(nodeToInsert, nodeBefore, undefined);
|
|
144
|
+
if (addEmptyNodes) {
|
|
145
|
+
const emptyNode1 = new Action(ACTIONS.CORE.EMPTYBLOCK.EMPTYBLOCK);
|
|
146
|
+
this.insertNode(emptyNode1, nodeToInsert, undefined, 'true', 'true');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// 2) "false" path: nodeBefore -> nodeToInsert,
|
|
150
|
+
// but here we pass no nodeAfter (so nodeAfter=null),
|
|
151
|
+
// and label/value are "false".
|
|
152
|
+
if (addElseCase && addEmptyNodes) {
|
|
153
|
+
const emptyNode2 = new Action(ACTIONS.CORE.EMPTYBLOCK.EMPTYBLOCK);
|
|
154
|
+
this.insertNode(emptyNode2, nodeToInsert, undefined, 'false', 'false');
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Inserts a "split" node, creating `numberOfBranches` parallel branches.
|
|
159
|
+
*
|
|
160
|
+
* If `nodeAfter` is given, we call `insertNode(...)` to splice the split node
|
|
161
|
+
* between `nodeBefore` and `nodeAfter`. That yields two edges:
|
|
162
|
+
* nodeBefore->nodeToInsert and nodeToInsert->nodeAfter
|
|
163
|
+
* The first branch is effectively "nodeAfter."
|
|
164
|
+
* Then we add additional empty blocks for the remaining branches.
|
|
165
|
+
*
|
|
166
|
+
* If `nodeAfter` is NOT given, we just insertNode(split, nodeBefore),
|
|
167
|
+
* which yields an edge: nodeBefore->split.
|
|
168
|
+
* Then we create `numberOfBranches` empty blocks off the split node.
|
|
169
|
+
*
|
|
170
|
+
* @param nodeToInsert The split node to insert (e.g. type="Split").
|
|
171
|
+
* @param nodeBefore The node after which the split is inserted.
|
|
172
|
+
* @param nodeAfter (Optional) If we’re splitting in the middle of a flow, the node that was originally after `nodeBefore`.
|
|
173
|
+
* @param numberOfBranches The total number of branches to create from `nodeToInsert`.
|
|
174
|
+
*/
|
|
175
|
+
insertSplit(nodeToInsert, nodeBefore, nodeAfter, numberOfBranches) {
|
|
176
|
+
// Basic validation
|
|
177
|
+
if (!this.nodes.includes(nodeBefore)) {
|
|
178
|
+
throw new Error('nodeBefore must exist in the workflow.');
|
|
179
|
+
}
|
|
180
|
+
if (numberOfBranches < 2) {
|
|
181
|
+
throw new Error('numberOfBranches must be at least 2.');
|
|
182
|
+
}
|
|
183
|
+
// Step 1: Insert the "split" node via insertNode
|
|
184
|
+
// - If nodeAfter is defined, it removes nodeBefore->nodeAfter
|
|
185
|
+
// and creates nodeBefore->nodeToInsert & nodeToInsert->nodeAfter.
|
|
186
|
+
// - If nodeAfter is undefined, we simply get nodeBefore->nodeToInsert.
|
|
187
|
+
this.insertNode(nodeToInsert, nodeBefore, nodeAfter);
|
|
188
|
+
// Step 2: Create the parallel branches
|
|
189
|
+
if (nodeAfter) {
|
|
190
|
+
// The first branch is already nodeToInsert->nodeAfter
|
|
191
|
+
// So we only need to create the remaining (numberOfBranches - 1) branches
|
|
192
|
+
const remaining = numberOfBranches - 1;
|
|
193
|
+
for (let i = 0; i < remaining; i++) {
|
|
194
|
+
const emptyBlock = new Action(ACTIONS.CORE.EMPTYBLOCK.EMPTYBLOCK);
|
|
195
|
+
// Insert the emptyBlock after nodeToInsert
|
|
196
|
+
// => this creates a new edge nodeToInsert->emptyBlock
|
|
197
|
+
this.insertNode(emptyBlock, nodeToInsert);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
// nodeAfter is undefined => all branches must be created
|
|
202
|
+
// Each branch is nodeToInsert->(new empty block)
|
|
203
|
+
for (let i = 0; i < numberOfBranches; i++) {
|
|
204
|
+
const emptyBlock = new Action(ACTIONS.CORE.EMPTYBLOCK.EMPTYBLOCK);
|
|
205
|
+
this.insertNode(emptyBlock, nodeToInsert);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
122
208
|
}
|
|
123
209
|
swapNode(oldNode, newNode) {
|
|
124
210
|
// Find the index of the node to replace
|
|
@@ -138,11 +224,11 @@ export class Workflow {
|
|
|
138
224
|
}
|
|
139
225
|
});
|
|
140
226
|
// Recalculate positions
|
|
141
|
-
|
|
227
|
+
positionWorkflowNodes(this);
|
|
142
228
|
}
|
|
143
229
|
addEdge(edge) {
|
|
144
230
|
this.edges.push(edge);
|
|
145
|
-
|
|
231
|
+
positionWorkflowNodes(this);
|
|
146
232
|
}
|
|
147
233
|
updateEdge(edgeId, newEdge) {
|
|
148
234
|
const edgeToUpdate = this.edges.find(e => e.id === edgeId);
|
|
@@ -153,11 +239,11 @@ export class Workflow {
|
|
|
153
239
|
else {
|
|
154
240
|
throw new Error(`Edge with id ${edgeId} not found`);
|
|
155
241
|
}
|
|
156
|
-
|
|
242
|
+
positionWorkflowNodes(this);
|
|
157
243
|
}
|
|
158
244
|
addEdges(edges) {
|
|
159
245
|
this.edges.push(...edges);
|
|
160
|
-
|
|
246
|
+
positionWorkflowNodes(this);
|
|
161
247
|
}
|
|
162
248
|
getState() {
|
|
163
249
|
return this.state;
|
|
@@ -286,7 +372,7 @@ export class Workflow {
|
|
|
286
372
|
this.nodes = yield Promise.all(response.nodes.map((nodeData) => __awaiter(this, void 0, void 0, function* () { return yield Node.fromJSON(nodeData); })));
|
|
287
373
|
this.edges = response.edges.map((edgeData) => Edge.fromJSON(edgeData, this.nodes));
|
|
288
374
|
this.notes = response.notes.map((noteData) => Note.fromJSON(noteData));
|
|
289
|
-
|
|
375
|
+
positionWorkflowNodes(this);
|
|
290
376
|
return this;
|
|
291
377
|
}
|
|
292
378
|
catch (error) {
|
|
@@ -1,176 +1,85 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export const
|
|
1
|
+
// Note: Using 'dagre' for layered DAG layout
|
|
2
|
+
import dagre from 'dagre';
|
|
3
|
+
// or: import * as dagre from 'dagre';
|
|
4
|
+
export const xSpacing = 700; // used by dagre as node separation
|
|
5
|
+
export const ySpacing = 75;
|
|
6
|
+
export const ROOT_X = 400; // we’ll keep references, but Dagre decides actual positions
|
|
5
7
|
export const ROOT_Y = 120;
|
|
8
|
+
/**
|
|
9
|
+
* Dagre-based layout for the “top-down” pass. We’re effectively ignoring the old
|
|
10
|
+
* manual code. Instead, we:
|
|
11
|
+
* 1) Build a dagre graph
|
|
12
|
+
* 2) Add all nodes & edges
|
|
13
|
+
* 3) dagre.layout(g)
|
|
14
|
+
* 4) Extract positions & write them back to workflow.nodes
|
|
15
|
+
*/
|
|
6
16
|
export function positionWorkflowNodes(workflow) {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const nodesToPosition = workflow.nodes.filter((node) => !startingNodes.includes(node));
|
|
18
|
-
nodesToPosition.forEach((node) => positionNode(node, workflow.edges, xSpacing, ySpacing, workflow));
|
|
19
|
-
}
|
|
20
|
-
catch (e) {
|
|
21
|
-
console.error(e);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
export function positionNode(node, edges, xSpacing, ySpacing, workflow) {
|
|
25
|
-
const parents = getParents(node, edges);
|
|
26
|
-
// todo: what if we have multiple parents?
|
|
27
|
-
const children = getChildren(parents[0], edges);
|
|
28
|
-
const sortedChildren = children.sort((a, b) => {
|
|
29
|
-
var _a, _b;
|
|
30
|
-
const edgeA = edges.find(edge => edge.source === parents[0] && edge.target === a);
|
|
31
|
-
const edgeB = edges.find(edge => edge.source === parents[0] && edge.target === b);
|
|
32
|
-
const labelA = (_a = edgeA === null || edgeA === void 0 ? void 0 : edgeA.label) !== null && _a !== void 0 ? _a : "";
|
|
33
|
-
const labelB = (_b = edgeB === null || edgeB === void 0 ? void 0 : edgeB.label) !== null && _b !== void 0 ? _b : "";
|
|
34
|
-
if (labelA === "true" && labelB !== "true")
|
|
35
|
-
return -1;
|
|
36
|
-
if (labelB === "true" && labelA !== "true")
|
|
37
|
-
return 1;
|
|
38
|
-
if (labelA === "false" && labelB !== "false")
|
|
39
|
-
return 1;
|
|
40
|
-
if (labelB === "false" && labelA !== "false")
|
|
41
|
-
return -1;
|
|
42
|
-
return 0;
|
|
17
|
+
// 1) Create a new directed graph
|
|
18
|
+
// doc: https://github.com/dagrejs/dagre
|
|
19
|
+
const g = new dagre.graphlib.Graph({ multigraph: false, compound: false });
|
|
20
|
+
g.setGraph({
|
|
21
|
+
// Some layout options:
|
|
22
|
+
rankdir: 'TB', // "top-bottom" layering
|
|
23
|
+
nodesep: xSpacing * 0.5, // horizontal spacing
|
|
24
|
+
ranksep: ySpacing, // vertical spacing between levels
|
|
25
|
+
marginx: 20, // how much margin to leave around the left/right
|
|
26
|
+
marginy: 20, // how much margin to leave around the top/bottom
|
|
43
27
|
});
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (childrenCountOfParent === 1) {
|
|
48
|
-
node.setPosition(parentX, parentY + ySpacing);
|
|
49
|
-
}
|
|
50
|
-
else {
|
|
51
|
-
const index = sortedChildren.indexOf(node); // Get the position of this node among its siblings
|
|
52
|
-
const totalChildren = sortedChildren.length;
|
|
53
|
-
// Compute the x position for this node
|
|
54
|
-
const offset = index - (totalChildren - 1) / 2; // Center the children around the parent
|
|
55
|
-
node.setPosition(parentX + offset * xSpacing, parentY + ySpacing);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
export function positionWorkflowNodesAvoidOverlap(workflow) {
|
|
59
|
-
const levels = new Map();
|
|
60
|
-
function addToLevel(node) {
|
|
61
|
-
const level = Math.round(node.position.y / ySpacing);
|
|
62
|
-
if (!levels.has(level)) {
|
|
63
|
-
levels.set(level, []);
|
|
64
|
-
}
|
|
65
|
-
levels.get(level).push(node);
|
|
66
|
-
}
|
|
67
|
-
// 1) Lay out nodes using existing logic
|
|
68
|
-
positionWorkflowNodes(workflow);
|
|
69
|
-
// 2) Fill the `levels` map
|
|
28
|
+
g.setDefaultEdgeLabel(() => ({}));
|
|
29
|
+
// 2) Add nodes to the graph
|
|
30
|
+
// Dagre requires each node have an id and some approximate width/height
|
|
70
31
|
workflow.nodes.forEach((node) => {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
// Group nodes by their parent
|
|
78
|
-
const parentGroups = new Map();
|
|
79
|
-
nodes.forEach((node) => {
|
|
80
|
-
const parents = getParents(node, workflow.edges);
|
|
81
|
-
if (parents.length > 0) {
|
|
82
|
-
const parent = parents[0]; // Assuming single parent for simplicity
|
|
83
|
-
if (!parentGroups.has(parent)) {
|
|
84
|
-
parentGroups.set(parent, []);
|
|
85
|
-
}
|
|
86
|
-
parentGroups.get(parent).push(node);
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
// Flatten the groups back into a sorted array
|
|
90
|
-
const groupedNodes = [];
|
|
91
|
-
parentGroups.forEach((group) => {
|
|
92
|
-
groupedNodes.push(...group);
|
|
32
|
+
const nodeId = node.getRef();
|
|
33
|
+
// For a typical text-based node, approximate width & height
|
|
34
|
+
g.setNode(nodeId, {
|
|
35
|
+
label: nodeId,
|
|
36
|
+
width: 100,
|
|
37
|
+
height: 50
|
|
93
38
|
});
|
|
94
|
-
// Sort nodes by x within each group to detect overlaps
|
|
95
|
-
groupedNodes.sort((a, b) => { var _a, _b; return ((_a = a.position.x) !== null && _a !== void 0 ? _a : 0) - ((_b = b.position.x) !== null && _b !== void 0 ? _b : 0); });
|
|
96
|
-
// Shift nodes within groups to resolve overlaps
|
|
97
|
-
for (let i = 1; i < groupedNodes.length; i++) {
|
|
98
|
-
const prev = groupedNodes[i - 1];
|
|
99
|
-
const current = groupedNodes[i];
|
|
100
|
-
if (current.position.x - prev.position.x < xSpacing) {
|
|
101
|
-
const shift = xSpacing - (current.position.x - prev.position.x);
|
|
102
|
-
moveNodeAndChildren(current, shift, workflow.edges);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
39
|
});
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
// Move parent to that average, keep same y
|
|
127
|
-
parent.setPosition(avgX, (_b = (_a = parent.position) === null || _a === void 0 ? void 0 : _a.y) !== null && _b !== void 0 ? _b : 0);
|
|
128
|
-
}
|
|
129
|
-
// Add this parent to the queue so we can recurse upward
|
|
130
|
-
if (!queue.includes(parent)) {
|
|
131
|
-
queue.push(parent);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
function moveNodeAndChildren(node, shift, edges) {
|
|
137
|
-
// Move the node
|
|
138
|
-
node.setPosition(node.position.x + shift, node.position.y);
|
|
139
|
-
// Propagate to children
|
|
140
|
-
edges
|
|
141
|
-
.filter((edge) => edge.source === node)
|
|
142
|
-
.forEach((edge) => {
|
|
143
|
-
moveNodeAndChildren(edge.target, shift, edges);
|
|
40
|
+
// 3) Add edges
|
|
41
|
+
// Dagre identifies edges by (sourceId, targetId). We don’t need labels for layout,
|
|
42
|
+
// but we can store them if we want. If you rely on “true/false” ordering, we can tweak
|
|
43
|
+
// e.g. use edge label as a tie-break. (See advanced docs.)
|
|
44
|
+
workflow.edges.forEach((edge) => {
|
|
45
|
+
var _a;
|
|
46
|
+
const fromId = edge.source.getRef();
|
|
47
|
+
const toId = edge.target.getRef();
|
|
48
|
+
g.setEdge(fromId, toId, { label: (_a = edge.label) !== null && _a !== void 0 ? _a : '' });
|
|
49
|
+
});
|
|
50
|
+
// 4) Run Dagre layout
|
|
51
|
+
dagre.layout(g);
|
|
52
|
+
// 5) Extract positions from Dagre graph and store them back to your Node objects
|
|
53
|
+
g.nodes().forEach((nodeId) => {
|
|
54
|
+
const dagreNode = g.node(nodeId); // { x, y, width, height, ... }
|
|
55
|
+
const nodeObj = workflow.nodes.find((n) => n.getRef() === nodeId);
|
|
56
|
+
if (!nodeObj || !dagreNode)
|
|
57
|
+
return;
|
|
58
|
+
// Store x,y in your node’s position
|
|
59
|
+
nodeObj.setPosition(dagreNode.x, dagreNode.y);
|
|
144
60
|
});
|
|
145
|
-
}
|
|
146
|
-
export function identifyLeafNodes(workflow) {
|
|
147
|
-
const nonLeafNodes = new Set(workflow.edges.map(edge => edge.source.getRef()));
|
|
148
|
-
return workflow.nodes.filter(node => !nonLeafNodes.has(node.getRef()));
|
|
149
61
|
}
|
|
150
62
|
/**
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
* @param workflow The workflow to analyze.
|
|
155
|
-
* @returns An array of nodes that have no incoming edges.
|
|
63
|
+
* We keep these helper functions the same for external usage (if your code or tests need them),
|
|
64
|
+
* but we’re not actively using them in the new Dagre-based layout.
|
|
156
65
|
*/
|
|
157
|
-
export function identityStartingNodes(workflow) {
|
|
158
|
-
const childRefs = new Set(workflow.edges.map((edge) => edge.target.getRef()));
|
|
159
|
-
return workflow.nodes.filter((node) => !childRefs.has(node.getRef()));
|
|
160
|
-
}
|
|
161
66
|
export function getChildren(node, edges) {
|
|
162
|
-
return edges
|
|
67
|
+
return edges
|
|
68
|
+
.filter((edge) => edge.source === node)
|
|
69
|
+
.map((edge) => edge.target);
|
|
163
70
|
}
|
|
164
71
|
export function getParents(node, edges) {
|
|
165
|
-
return edges
|
|
72
|
+
return edges
|
|
73
|
+
.filter((edge) => edge.target === node)
|
|
74
|
+
.map((edge) => edge.source);
|
|
166
75
|
}
|
|
167
76
|
export function getEdges(node, edges) {
|
|
168
|
-
return edges.filter(edge => edge.source === node || edge.target === node);
|
|
77
|
+
return edges.filter((edge) => edge.source === node || edge.target === node);
|
|
169
78
|
}
|
|
170
79
|
export function getEndNodePositions(workflow) {
|
|
171
80
|
return workflow.nodes
|
|
172
|
-
.filter(node => getChildren(node, workflow.edges).length === 0)
|
|
173
|
-
.map(node => {
|
|
81
|
+
.filter((node) => getChildren(node, workflow.edges).length === 0)
|
|
82
|
+
.map((node) => {
|
|
174
83
|
var _a, _b, _c, _d;
|
|
175
84
|
return ({
|
|
176
85
|
x: (_b = (_a = node.position) === null || _a === void 0 ? void 0 : _a.x) !== null && _b !== void 0 ? _b : 0,
|
|
@@ -178,3 +87,16 @@ export function getEndNodePositions(workflow) {
|
|
|
178
87
|
});
|
|
179
88
|
});
|
|
180
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* In Dagre, we no longer do an explicit “identify leaf nodes” or “center parents by average,”
|
|
92
|
+
* because the layout library handles that.
|
|
93
|
+
* But here’s your old method if something else depends on it.
|
|
94
|
+
*/
|
|
95
|
+
export function identifyLeafNodes(workflow) {
|
|
96
|
+
const sources = new Set(workflow.edges.map((edge) => edge.source.getRef()));
|
|
97
|
+
return workflow.nodes.filter((node) => !sources.has(node.getRef()));
|
|
98
|
+
}
|
|
99
|
+
export function identityStartingNodes(workflow) {
|
|
100
|
+
const childRefs = new Set(workflow.edges.map((edge) => edge.target.getRef()));
|
|
101
|
+
return workflow.nodes.filter((node) => !childRefs.has(node.getRef()));
|
|
102
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const SDK_VERSION = "2.0.
|
|
1
|
+
export declare const SDK_VERSION = "2.0.36";
|
|
2
2
|
export declare function compareVersions(v1: string, v2: string): number;
|
|
@@ -19,22 +19,45 @@ export declare class Workflow {
|
|
|
19
19
|
addNodes(nodes: Node[]): void;
|
|
20
20
|
deleteNode(nodeToDelete: Node): void;
|
|
21
21
|
/**
|
|
22
|
-
* Inserts a new node into the workflow
|
|
23
|
-
* - If both nodeBefore and nodeAfter are provided, we expect an existing edge
|
|
24
|
-
* from nodeBefore -> nodeAfter, which gets replaced by two edges:
|
|
25
|
-
* nodeBefore -> nodeToInsert and nodeToInsert -> nodeAfter.
|
|
22
|
+
* Inserts a new node into the workflow, potentially splitting an existing edge.
|
|
26
23
|
*
|
|
27
|
-
* - If nodeAfter is
|
|
28
|
-
*
|
|
29
|
-
*
|
|
24
|
+
* - If `nodeAfter` is specified, we expect an edge nodeBefore->nodeAfter to exist.
|
|
25
|
+
* That edge is removed, and replaced by two new edges:
|
|
26
|
+
* nodeBefore->nodeToInsert (with `edgeLabelBefore`, `edgeValueBefore`)
|
|
27
|
+
* nodeToInsert->nodeAfter (with `edgeLabelAfter`, `edgeValueAfter`)
|
|
30
28
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* @param
|
|
35
|
-
* @param
|
|
29
|
+
* - If `nodeAfter` is not specified, we create only one edge:
|
|
30
|
+
* nodeBefore->nodeToInsert (with `edgeLabelBefore`, `edgeValueBefore`)
|
|
31
|
+
*
|
|
32
|
+
* @param nodeToInsert The node to insert
|
|
33
|
+
* @param nodeBefore The existing node that precedes `nodeToInsert`
|
|
34
|
+
* @param nodeAfter (Optional) The existing node that should follow `nodeToInsert`
|
|
35
|
+
* @param edgeLabelBefore (Optional) Label for the edge nodeBefore->nodeToInsert
|
|
36
|
+
* @param edgeValueBefore (Optional) Value for the edge nodeBefore->nodeToInsert
|
|
37
|
+
* @param edgeLabelAfter (Optional) Label for the edge nodeToInsert->nodeAfter
|
|
38
|
+
* @param edgeValueAfter (Optional) Value for the edge nodeToInsert->nodeAfter
|
|
39
|
+
*/
|
|
40
|
+
insertNode(nodeToInsert: Node, nodeBefore: Node, nodeAfter?: Node, edgeLabelBefore?: string | null, edgeValueBefore?: any | null, edgeLabelAfter?: string | null, edgeValueAfter?: any | null): void;
|
|
41
|
+
insertCondition(nodeToInsert: Node, nodeBefore: Node, nodeAfter?: Node, addElseCase?: boolean, addEmptyNodes?: boolean): void;
|
|
42
|
+
/**
|
|
43
|
+
* Inserts a "split" node, creating `numberOfBranches` parallel branches.
|
|
44
|
+
*
|
|
45
|
+
* If `nodeAfter` is given, we call `insertNode(...)` to splice the split node
|
|
46
|
+
* between `nodeBefore` and `nodeAfter`. That yields two edges:
|
|
47
|
+
* nodeBefore->nodeToInsert and nodeToInsert->nodeAfter
|
|
48
|
+
* The first branch is effectively "nodeAfter."
|
|
49
|
+
* Then we add additional empty blocks for the remaining branches.
|
|
50
|
+
*
|
|
51
|
+
* If `nodeAfter` is NOT given, we just insertNode(split, nodeBefore),
|
|
52
|
+
* which yields an edge: nodeBefore->split.
|
|
53
|
+
* Then we create `numberOfBranches` empty blocks off the split node.
|
|
54
|
+
*
|
|
55
|
+
* @param nodeToInsert The split node to insert (e.g. type="Split").
|
|
56
|
+
* @param nodeBefore The node after which the split is inserted.
|
|
57
|
+
* @param nodeAfter (Optional) If we’re splitting in the middle of a flow, the node that was originally after `nodeBefore`.
|
|
58
|
+
* @param numberOfBranches The total number of branches to create from `nodeToInsert`.
|
|
36
59
|
*/
|
|
37
|
-
|
|
60
|
+
insertSplit(nodeToInsert: Node, nodeBefore: Node, nodeAfter: Node | undefined, numberOfBranches: number): void;
|
|
38
61
|
swapNode(oldNode: Node, newNode: Node): void;
|
|
39
62
|
addEdge(edge: Edge): void;
|
|
40
63
|
updateEdge(edgeId: string, newEdge: Edge): void;
|
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
import { Workflow } from '../models/Workflow.js';
|
|
2
2
|
import { Node } from '../models/Node.js';
|
|
3
3
|
import { Edge } from '../models/Edge.js';
|
|
4
|
-
export declare const xSpacing =
|
|
5
|
-
export declare const ySpacing =
|
|
4
|
+
export declare const xSpacing = 700;
|
|
5
|
+
export declare const ySpacing = 75;
|
|
6
6
|
export declare const ROOT_X = 400;
|
|
7
7
|
export declare const ROOT_Y = 120;
|
|
8
|
+
/**
|
|
9
|
+
* Dagre-based layout for the “top-down” pass. We’re effectively ignoring the old
|
|
10
|
+
* manual code. Instead, we:
|
|
11
|
+
* 1) Build a dagre graph
|
|
12
|
+
* 2) Add all nodes & edges
|
|
13
|
+
* 3) dagre.layout(g)
|
|
14
|
+
* 4) Extract positions & write them back to workflow.nodes
|
|
15
|
+
*/
|
|
8
16
|
export declare function positionWorkflowNodes(workflow: Workflow): void;
|
|
9
|
-
export declare function positionNode(node: Node, edges: Edge[], xSpacing: number, ySpacing: number, workflow: Workflow): void;
|
|
10
|
-
export declare function positionWorkflowNodesAvoidOverlap(workflow: Workflow): void;
|
|
11
|
-
export declare function identifyLeafNodes(workflow: Workflow): Node[];
|
|
12
17
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* @param workflow The workflow to analyze.
|
|
17
|
-
* @returns An array of nodes that have no incoming edges.
|
|
18
|
+
* We keep these helper functions the same for external usage (if your code or tests need them),
|
|
19
|
+
* but we’re not actively using them in the new Dagre-based layout.
|
|
18
20
|
*/
|
|
19
|
-
export declare function identityStartingNodes(workflow: Workflow): Node[];
|
|
20
21
|
export declare function getChildren(node: Node, edges: Edge[]): Node[];
|
|
21
22
|
export declare function getParents(node: Node, edges: Edge[]): Node[];
|
|
22
23
|
export declare function getEdges(node: Node, edges: Edge[]): Edge[];
|
|
@@ -24,3 +25,10 @@ export declare function getEndNodePositions(workflow: Workflow): {
|
|
|
24
25
|
x: number;
|
|
25
26
|
y: number;
|
|
26
27
|
}[];
|
|
28
|
+
/**
|
|
29
|
+
* In Dagre, we no longer do an explicit “identify leaf nodes” or “center parents by average,”
|
|
30
|
+
* because the layout library handles that.
|
|
31
|
+
* But here’s your old method if something else depends on it.
|
|
32
|
+
*/
|
|
33
|
+
export declare function identifyLeafNodes(workflow: Workflow): Node[];
|
|
34
|
+
export declare function identityStartingNodes(workflow: Workflow): Node[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "otomato-sdk",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.37",
|
|
4
4
|
"description": "An SDK for building and managing automations on Otomato",
|
|
5
5
|
"main": "dist/src/index.js",
|
|
6
6
|
"types": "dist/types/src/index.d.ts",
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@types/axios": "^0.14.0",
|
|
33
33
|
"@types/chai": "^4.3.16",
|
|
34
|
+
"@types/dagre": "^0.7.52",
|
|
34
35
|
"@types/jsonwebtoken": "^9.0.6",
|
|
35
36
|
"@types/mocha": "^10.0.6",
|
|
36
37
|
"@types/mustache": "^4.2.5",
|
|
@@ -43,6 +44,7 @@
|
|
|
43
44
|
},
|
|
44
45
|
"dependencies": {
|
|
45
46
|
"axios": "^1.7.2",
|
|
47
|
+
"dagre": "^0.8.5",
|
|
46
48
|
"ethers": "^6.13.1",
|
|
47
49
|
"jsonwebtoken": "^9.0.2",
|
|
48
50
|
"mustache": "^4.2.0"
|