@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.
Files changed (39) hide show
  1. package/README.md +50 -11
  2. package/agrippa-pb.md +375 -0
  3. package/package.json +7 -3
  4. package/src/agrippa/commands/clone.js +29 -13
  5. package/src/agrippa/commands/clonePb.js +107 -0
  6. package/src/agrippa/commands/diff.js +109 -20
  7. package/src/agrippa/commands/init.js +66 -12
  8. package/src/agrippa/commands/initPhase.js +16 -17
  9. package/src/agrippa/commands/pb.js +291 -0
  10. package/src/agrippa/commands/pull.js +122 -16
  11. package/src/agrippa/commands/pullPb.js +54 -0
  12. package/src/agrippa/commands/push.js +112 -48
  13. package/src/agrippa/commands/pushPb.js +87 -0
  14. package/src/agrippa/commands/repair.js +3 -1
  15. package/src/agrippa/index.js +144 -14
  16. package/src/agrippa/lib/api.js +17 -3
  17. package/src/agrippa/lib/checksum.js +3 -1
  18. package/src/agrippa/lib/config.js +2 -2
  19. package/src/agrippa/lib/pbApi.js +71 -0
  20. package/src/agrippa/lib/pbEdit.js +543 -0
  21. package/src/agrippa/lib/pbLayout.js +304 -0
  22. package/src/agrippa/lib/pbModel.js +701 -0
  23. package/src/agrippa/lib/pbPreview.js +153 -0
  24. package/src/agrippa/lib/pbProject.js +401 -0
  25. package/src/agrippa/lib/pbWorkspace.js +30 -0
  26. package/src/agrippa/lib/workspace.js +23 -3
  27. package/src/commands/autopr.js +5 -2
  28. package/src/commands/changelog.js +4 -1
  29. package/src/commands/export.js +3 -3
  30. package/src/commands/exportEmailTemplates.js +25 -15
  31. package/src/commands/exportImperex.js +4 -4
  32. package/src/commands/exportLrp.js +10 -7
  33. package/src/commands/exportPb.js +4 -5
  34. package/src/commands/exportWorkflow.js +27 -14
  35. package/src/commands/init.js +7 -0
  36. package/src/commands/routine.js +7 -5
  37. package/src/index.js +24 -7
  38. package/src/lib/premigrate.js +3 -3
  39. 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 };