@waron97/prbot 3.1.4 → 3.2.1
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/README.md +50 -11
- package/agrippa-pb.md +375 -0
- package/package.json +7 -3
- package/src/agrippa/commands/clone.js +29 -13
- package/src/agrippa/commands/clonePb.js +107 -0
- package/src/agrippa/commands/diff.js +109 -20
- package/src/agrippa/commands/init.js +66 -12
- package/src/agrippa/commands/initPhase.js +16 -17
- package/src/agrippa/commands/pb.js +291 -0
- package/src/agrippa/commands/pull.js +122 -16
- package/src/agrippa/commands/pullPb.js +54 -0
- package/src/agrippa/commands/push.js +112 -48
- package/src/agrippa/commands/pushPb.js +87 -0
- package/src/agrippa/commands/repair.js +3 -1
- package/src/agrippa/index.js +144 -14
- package/src/agrippa/lib/api.js +17 -3
- package/src/agrippa/lib/checksum.js +3 -1
- package/src/agrippa/lib/config.js +2 -2
- package/src/agrippa/lib/pbApi.js +71 -0
- package/src/agrippa/lib/pbEdit.js +543 -0
- package/src/agrippa/lib/pbLayout.js +304 -0
- package/src/agrippa/lib/pbModel.js +701 -0
- package/src/agrippa/lib/pbPreview.js +153 -0
- package/src/agrippa/lib/pbProject.js +401 -0
- package/src/agrippa/lib/pbWorkspace.js +30 -0
- package/src/agrippa/lib/workspace.js +23 -3
- package/src/commands/autopr.js +5 -2
- package/src/commands/changelog.js +4 -1
- package/src/commands/export.js +3 -3
- package/src/commands/exportEmailTemplates.js +25 -15
- package/src/commands/exportImperex.js +4 -4
- package/src/commands/exportLrp.js +10 -7
- package/src/commands/exportPb.js +4 -5
- package/src/commands/exportWorkflow.js +27 -14
- package/src/commands/init.js +7 -0
- package/src/commands/routine.js +7 -5
- package/src/index.js +24 -7
- package/src/lib/premigrate.js +3 -3
- package/src/lib/updateCheck.js +5 -2
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
// Auto-layout for a decomposed process-builder graph, via elkjs.
|
|
2
|
+
//
|
|
3
|
+
// `autoLayout(structure)` rewrites every node's `layout {x,y,width,height}` and
|
|
4
|
+
// every edge's `waypoints` (plus annotations/associations) in place, producing a
|
|
5
|
+
// left→right (start-left, end-right) layered diagram. The decomposed diagram is
|
|
6
|
+
// regenerated from this geometry on recompose, so formatting here is all it takes.
|
|
7
|
+
//
|
|
8
|
+
// elkjs handles subProcess/transaction natively as compound nodes: children are
|
|
9
|
+
// laid out and the container is auto-sized. Two coordinate subtleties are handled:
|
|
10
|
+
// 1. Node coords ELK returns are parent-relative → accumulated to absolute (BPMN
|
|
11
|
+
// bounds are absolute regardless of nesting).
|
|
12
|
+
// 2. Edge section coords are relative to the lowest common ancestor (LCA)
|
|
13
|
+
// container of the edge's endpoints → offset by that container's absolute
|
|
14
|
+
// origin (root → 0,0). See the LCA computation below.
|
|
15
|
+
// boundaryEvents are excluded from the ELK node graph and modelled as ports
|
|
16
|
+
// on their attachedToRef node, which makes ELK route their outgoing edge as a
|
|
17
|
+
// real hierarchy-crossing flow. ELK does NOT honour any port side/position for
|
|
18
|
+
// such edges (it exits the border facing the target) and the port rectangle it
|
|
19
|
+
// reports often disagrees with where the edge is actually drawn — so the
|
|
20
|
+
// boundary glyph is snapped onto the edge's start point afterwards, not the
|
|
21
|
+
// reported port.
|
|
22
|
+
|
|
23
|
+
import ELK from 'elkjs/lib/elk.bundled.js';
|
|
24
|
+
import { CONTAINER, eachNode, SIZE } from './pbEdit.js';
|
|
25
|
+
|
|
26
|
+
const elk = new ELK();
|
|
27
|
+
|
|
28
|
+
const ROOT_OPTS = {
|
|
29
|
+
'elk.algorithm': 'layered',
|
|
30
|
+
'elk.direction': 'RIGHT',
|
|
31
|
+
'elk.edgeRouting': 'ORTHOGONAL',
|
|
32
|
+
'elk.hierarchyHandling': 'INCLUDE_CHILDREN',
|
|
33
|
+
'elk.layered.spacing.nodeNodeBetweenLayers': '60',
|
|
34
|
+
'elk.spacing.nodeNode': '80',
|
|
35
|
+
'elk.spacing.edgeNode': '20',
|
|
36
|
+
'elk.spacing.edgeEdge': '20',
|
|
37
|
+
'elk.layered.spacing.edgeEdgeBetweenLayers': '20',
|
|
38
|
+
'elk.layered.spacing.edgeNodeBetweenLayers': '20',
|
|
39
|
+
};
|
|
40
|
+
const CONTAINER_OPTS = {
|
|
41
|
+
'elk.padding': '[top=50.0,left=30.0,bottom=50.0,right=30.0]',
|
|
42
|
+
'elk.layered.spacing.nodeNodeBetweenLayers': '60',
|
|
43
|
+
'elk.spacing.nodeNode': '80',
|
|
44
|
+
'elk.spacing.edgeNode': '20',
|
|
45
|
+
'elk.layered.spacing.edgeNodeBetweenLayers': '20',
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
function sizeOf(n) {
|
|
49
|
+
if (SIZE[n.type]) return SIZE[n.type];
|
|
50
|
+
if (n.layout) return [n.layout.width || 84, n.layout.height || 84];
|
|
51
|
+
return [84, 84];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// structure node -> elk node (recursive for containers; container size omitted so
|
|
55
|
+
// ELK computes it from its children). boundaryEvents are skipped — they appear as
|
|
56
|
+
// ports on their attachedToRef node instead (beByRef collected by the caller).
|
|
57
|
+
function toElk(n, beByRef) {
|
|
58
|
+
if (n.type === 'boundaryEvent') return null;
|
|
59
|
+
const en = { id: n.id };
|
|
60
|
+
if (CONTAINER.has(n.type)) {
|
|
61
|
+
en.layoutOptions = CONTAINER_OPTS;
|
|
62
|
+
en.children = (n.nodes || []).map((c) => toElk(c, beByRef)).filter(Boolean);
|
|
63
|
+
} else {
|
|
64
|
+
const [w, h] = sizeOf(n);
|
|
65
|
+
en.width = w;
|
|
66
|
+
en.height = h;
|
|
67
|
+
}
|
|
68
|
+
const bes = beByRef?.[n.id];
|
|
69
|
+
if (bes?.length) {
|
|
70
|
+
// The port exists only so ELK reserves the edge and routes it as a real
|
|
71
|
+
// hierarchy-crossing flow. ELK ignores any port side/position constraint
|
|
72
|
+
// for these edges — it always exits the border facing the target — so we
|
|
73
|
+
// don't bother setting one; the boundary glyph is later snapped onto the
|
|
74
|
+
// edge's actual start point instead (see below).
|
|
75
|
+
en.ports = bes.map((be) => ({ id: `__port_${be.id}` }));
|
|
76
|
+
}
|
|
77
|
+
return en;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function round(v) {
|
|
81
|
+
return Math.round(v);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ----- happy-flow edges -----
|
|
85
|
+
// The happy flow is every edge crossed walking from a scope's startEvent to
|
|
86
|
+
// any dead end, taking ONLY the `default` branch at an exclusiveGateway and
|
|
87
|
+
// ALL outgoing edges at any other node (forks like parallel gateways are not
|
|
88
|
+
// a "choice", so every branch counts as happy). Computed per scope (root,
|
|
89
|
+
// then recursively inside each subProcess/transaction's own `nodes`) since a
|
|
90
|
+
// sequenceFlow only ever connects siblings within the same container.
|
|
91
|
+
function happyEdgesInScope(nodes, acc) {
|
|
92
|
+
const byId = new Map((nodes || []).map((n) => [n.id, n]));
|
|
93
|
+
const visited = new Set();
|
|
94
|
+
const visit = (n) => {
|
|
95
|
+
if (visited.has(n.id)) return;
|
|
96
|
+
visited.add(n.id);
|
|
97
|
+
const edges = n.edges || [];
|
|
98
|
+
if (n.type === 'exclusiveGateway' && edges.length > 1) {
|
|
99
|
+
const def = n.attrs?.default;
|
|
100
|
+
const e = edges.find((e) => e.id === def) || edges[0];
|
|
101
|
+
acc.add(e.id);
|
|
102
|
+
const next = byId.get(e.target);
|
|
103
|
+
if (next) visit(next);
|
|
104
|
+
} else {
|
|
105
|
+
for (const e of edges) {
|
|
106
|
+
acc.add(e.id);
|
|
107
|
+
const next = byId.get(e.target);
|
|
108
|
+
if (next) visit(next);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
for (const n of nodes || []) {
|
|
113
|
+
if (n.type === 'startEvent') visit(n);
|
|
114
|
+
if (n.nodes) happyEdgesInScope(n.nodes, acc);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function computeHappyEdges(structure) {
|
|
119
|
+
const acc = new Set();
|
|
120
|
+
happyEdgesInScope(structure.nodes, acc);
|
|
121
|
+
return acc;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function autoLayout(structure) {
|
|
125
|
+
// ----- build the elk graph (all edges declared at root) -----
|
|
126
|
+
// boundaryEvents are removed from the node set and surfaced as ports on
|
|
127
|
+
// their attachedToRef node, so ELK routes their outgoing edges from the
|
|
128
|
+
// correct border position natively.
|
|
129
|
+
const beByRef = {};
|
|
130
|
+
eachNode(structure.nodes, null, (n) => {
|
|
131
|
+
if (n.type === 'boundaryEvent' && n.attachedToRef)
|
|
132
|
+
(beByRef[n.attachedToRef] ||= []).push(n);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const children = (structure.nodes || []).map((n) => toElk(n, beByRef)).filter(Boolean);
|
|
136
|
+
for (const a of structure.annotations || []) {
|
|
137
|
+
children.push({ id: a.id, width: a.layout?.width || 100, height: a.layout?.height || 30 });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const happyEdges = computeHappyEdges(structure);
|
|
141
|
+
const edges = [];
|
|
142
|
+
eachNode(structure.nodes, null, (n) => {
|
|
143
|
+
for (const e of n.edges || []) {
|
|
144
|
+
const isHappy = happyEdges.has(e.id);
|
|
145
|
+
let source = n.id;
|
|
146
|
+
let sourcePort;
|
|
147
|
+
if (n.type === 'boundaryEvent' && n.attachedToRef) {
|
|
148
|
+
source = n.attachedToRef;
|
|
149
|
+
sourcePort = `__port_${n.id}`;
|
|
150
|
+
}
|
|
151
|
+
edges.push({
|
|
152
|
+
id: e.id,
|
|
153
|
+
sources: [source],
|
|
154
|
+
targets: [e.target],
|
|
155
|
+
sourcePort,
|
|
156
|
+
labels: e.name
|
|
157
|
+
? [{ id: `${e.id}__lbl`, text: e.name, width: Math.min(e.name.length * 6, 140), height: 14 }]
|
|
158
|
+
: [],
|
|
159
|
+
layoutOptions: {
|
|
160
|
+
'elk.layered.priority.straightness': isHappy ? '10' : 1,
|
|
161
|
+
'elk.layered.priority.shortness': isHappy ? '10' : 1,
|
|
162
|
+
'elk.layered.priority.direction': isHappy ? '10' : 1,
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
for (const a of structure.associations || []) {
|
|
168
|
+
edges.push({ id: a.id, sources: [a.sourceRef], targets: [a.targetRef] });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const graph = { id: 'root', layoutOptions: ROOT_OPTS, children, edges };
|
|
172
|
+
const res = await elk.layout(graph);
|
|
173
|
+
|
|
174
|
+
// ----- node absolute positions (accumulate parent-relative coords) -----
|
|
175
|
+
const pos = {};
|
|
176
|
+
const accumulate = (node, ox, oy) => {
|
|
177
|
+
for (const c of node.children || []) {
|
|
178
|
+
const ax = ox + (c.x || 0);
|
|
179
|
+
const ay = oy + (c.y || 0);
|
|
180
|
+
pos[c.id] = { x: ax, y: ay, width: c.width, height: c.height };
|
|
181
|
+
if (c.children) accumulate(c, ax, ay);
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
accumulate(res, 0, 0);
|
|
185
|
+
|
|
186
|
+
// ----- port positions (boundaryEvent ports on their attachedToRef node) -----
|
|
187
|
+
const portPos = {};
|
|
188
|
+
const accumulatePorts = (node, ox, oy) => {
|
|
189
|
+
for (const p of node.ports || []) {
|
|
190
|
+
portPos[p.id] = {
|
|
191
|
+
x: ox + (p.x || 0),
|
|
192
|
+
y: oy + (p.y || 0),
|
|
193
|
+
width: p.width || 0,
|
|
194
|
+
height: p.height || 0,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
for (const c of node.children || []) {
|
|
198
|
+
accumulatePorts(c, ox + (c.x || 0), oy + (c.y || 0));
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
accumulatePorts(res, 0, 0);
|
|
202
|
+
|
|
203
|
+
// ----- container path per node (for edge LCA offset) -----
|
|
204
|
+
const pathOf = {};
|
|
205
|
+
const walkPaths = (nodes, stack) => {
|
|
206
|
+
for (const n of nodes || []) {
|
|
207
|
+
pathOf[n.id] = stack;
|
|
208
|
+
if (n.nodes) walkPaths(n.nodes, [...stack, n.id]);
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
walkPaths(structure.nodes, []);
|
|
212
|
+
for (const a of structure.annotations || []) pathOf[a.id] = pathOf[a.id] || [];
|
|
213
|
+
|
|
214
|
+
const lcaOffset = (a, b) => {
|
|
215
|
+
const pa = pathOf[a] || [];
|
|
216
|
+
const pb = pathOf[b] || [];
|
|
217
|
+
let i = 0;
|
|
218
|
+
while (i < pa.length && i < pb.length && pa[i] === pb[i]) i++;
|
|
219
|
+
const lca = i > 0 ? pa[i - 1] : null;
|
|
220
|
+
return lca && pos[lca] ? pos[lca] : { x: 0, y: 0 };
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
// ----- collect returned elk edges by id (coords are LCA-relative) -----
|
|
224
|
+
const elkEdges = {};
|
|
225
|
+
const collectEdges = (node) => {
|
|
226
|
+
for (const e of node.edges || []) elkEdges[e.id] = e;
|
|
227
|
+
for (const c of node.children || []) collectEdges(c);
|
|
228
|
+
};
|
|
229
|
+
collectEdges(res);
|
|
230
|
+
|
|
231
|
+
const waypointsFor = (edgeId, off) => {
|
|
232
|
+
const sec = elkEdges[edgeId]?.sections?.[0];
|
|
233
|
+
if (!sec) return null;
|
|
234
|
+
const pts = [sec.startPoint, ...(sec.bendPoints || []), sec.endPoint];
|
|
235
|
+
return pts.map((p) => [round(p.x + off.x), round(p.y + off.y)]);
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
// ----- write geometry back into the structure -----
|
|
239
|
+
eachNode(structure.nodes, null, (n) => {
|
|
240
|
+
const p = pos[n.id];
|
|
241
|
+
if (p)
|
|
242
|
+
n.layout = {
|
|
243
|
+
x: round(p.x),
|
|
244
|
+
y: round(p.y),
|
|
245
|
+
width: round(p.width),
|
|
246
|
+
height: round(p.height),
|
|
247
|
+
};
|
|
248
|
+
for (const e of n.edges || []) {
|
|
249
|
+
const off = lcaOffset(n.id, e.target);
|
|
250
|
+
const wp = waypointsFor(e.id, off);
|
|
251
|
+
if (wp) e.waypoints = wp;
|
|
252
|
+
if (e.name) {
|
|
253
|
+
const lbl = elkEdges[e.id]?.labels?.[0];
|
|
254
|
+
if (lbl) {
|
|
255
|
+
e.labelPos = {
|
|
256
|
+
x: round(lbl.x + off.x),
|
|
257
|
+
y: round(lbl.y + off.y),
|
|
258
|
+
width: round(lbl.width),
|
|
259
|
+
height: round(lbl.height),
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// boundaryEvents: positioned from the first waypoint of their outgoing edge.
|
|
267
|
+
// With free port constraints, elkjs reports the port rect on one border but
|
|
268
|
+
// routes the edge from another, so the reported port position and the actual
|
|
269
|
+
// edge origin disagree (the glyph would detach from its own arrow). The edge
|
|
270
|
+
// section is the source of truth — its startPoint sits exactly on the
|
|
271
|
+
// attachedToRef border where the flow leaves — so we center the 36×36 glyph
|
|
272
|
+
// there. Fall back to the port rect only for a boundary event with no edge.
|
|
273
|
+
eachNode(structure.nodes, null, (n) => {
|
|
274
|
+
if (n.type !== 'boundaryEvent' || !n.attachedToRef) return;
|
|
275
|
+
const wp0 = n.edges?.find((e) => e.waypoints?.length)?.waypoints[0];
|
|
276
|
+
const center = wp0 ? { x: wp0[0], y: wp0[1] } : portPos[`__port_${n.id}`];
|
|
277
|
+
if (center)
|
|
278
|
+
n.layout = {
|
|
279
|
+
x: round(center.x - 18),
|
|
280
|
+
y: round(center.y - 18),
|
|
281
|
+
width: 36,
|
|
282
|
+
height: 36,
|
|
283
|
+
};
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
for (const a of structure.annotations || []) {
|
|
287
|
+
const p = pos[a.id];
|
|
288
|
+
if (p)
|
|
289
|
+
a.layout = {
|
|
290
|
+
x: round(p.x),
|
|
291
|
+
y: round(p.y),
|
|
292
|
+
width: round(p.width),
|
|
293
|
+
height: round(p.height),
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
for (const a of structure.associations || []) {
|
|
297
|
+
const wp = waypointsFor(a.id, lcaOffset(a.sourceRef, a.targetRef));
|
|
298
|
+
if (wp) a.waypoints = wp;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return structure;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export { autoLayout };
|