@waron97/prbot 3.1.4 → 3.2.0

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 +350 -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 +94 -18
  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 +279 -0
  10. package/src/agrippa/commands/pull.js +119 -13
  11. package/src/agrippa/commands/pullPb.js +54 -0
  12. package/src/agrippa/commands/push.js +112 -47
  13. package/src/agrippa/commands/pushPb.js +87 -0
  14. package/src/agrippa/commands/repair.js +3 -1
  15. package/src/agrippa/index.js +138 -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 +467 -0
  21. package/src/agrippa/lib/pbLayout.js +283 -0
  22. package/src/agrippa/lib/pbModel.js +698 -0
  23. package/src/agrippa/lib/pbPreview.js +151 -0
  24. package/src/agrippa/lib/pbProject.js +390 -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,283 @@
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 = { 'elk.padding': '[top=40.0,left=20.0,bottom=20.0,right=20.0]' };
41
+
42
+ function sizeOf(n) {
43
+ if (SIZE[n.type]) return SIZE[n.type];
44
+ if (n.layout) return [n.layout.width || 84, n.layout.height || 84];
45
+ return [84, 84];
46
+ }
47
+
48
+ // structure node -> elk node (recursive for containers; container size omitted so
49
+ // ELK computes it from its children). boundaryEvents are skipped — they appear as
50
+ // ports on their attachedToRef node instead (beByRef collected by the caller).
51
+ function toElk(n, beByRef) {
52
+ if (n.type === 'boundaryEvent') return null;
53
+ const en = { id: n.id };
54
+ if (CONTAINER.has(n.type)) {
55
+ en.layoutOptions = CONTAINER_OPTS;
56
+ en.children = (n.nodes || []).map((c) => toElk(c, beByRef)).filter(Boolean);
57
+ } else {
58
+ const [w, h] = sizeOf(n);
59
+ en.width = w;
60
+ en.height = h;
61
+ }
62
+ const bes = beByRef?.[n.id];
63
+ if (bes?.length) {
64
+ // The port exists only so ELK reserves the edge and routes it as a real
65
+ // hierarchy-crossing flow. ELK ignores any port side/position constraint
66
+ // for these edges — it always exits the border facing the target — so we
67
+ // don't bother setting one; the boundary glyph is later snapped onto the
68
+ // edge's actual start point instead (see below).
69
+ en.ports = bes.map((be) => ({ id: `__port_${be.id}` }));
70
+ }
71
+ return en;
72
+ }
73
+
74
+ function round(v) {
75
+ return Math.round(v);
76
+ }
77
+
78
+ // ----- happy-flow edges -----
79
+ // The happy flow is every edge crossed walking from a scope's startEvent to
80
+ // any dead end, taking ONLY the `default` branch at an exclusiveGateway and
81
+ // ALL outgoing edges at any other node (forks like parallel gateways are not
82
+ // a "choice", so every branch counts as happy). Computed per scope (root,
83
+ // then recursively inside each subProcess/transaction's own `nodes`) since a
84
+ // sequenceFlow only ever connects siblings within the same container.
85
+ function happyEdgesInScope(nodes, acc) {
86
+ const byId = new Map((nodes || []).map((n) => [n.id, n]));
87
+ const visited = new Set();
88
+ const visit = (n) => {
89
+ if (visited.has(n.id)) return;
90
+ visited.add(n.id);
91
+ const edges = n.edges || [];
92
+ if (n.type === 'exclusiveGateway' && edges.length > 1) {
93
+ const def = n.attrs?.default;
94
+ const e = edges.find((e) => e.id === def) || edges[0];
95
+ acc.add(e.id);
96
+ const next = byId.get(e.target);
97
+ if (next) visit(next);
98
+ } else {
99
+ for (const e of edges) {
100
+ acc.add(e.id);
101
+ const next = byId.get(e.target);
102
+ if (next) visit(next);
103
+ }
104
+ }
105
+ };
106
+ for (const n of nodes || []) {
107
+ if (n.type === 'startEvent') visit(n);
108
+ if (n.nodes) happyEdgesInScope(n.nodes, acc);
109
+ }
110
+ }
111
+
112
+ function computeHappyEdges(structure) {
113
+ const acc = new Set();
114
+ happyEdgesInScope(structure.nodes, acc);
115
+ return acc;
116
+ }
117
+
118
+ async function autoLayout(structure) {
119
+ // ----- build the elk graph (all edges declared at root) -----
120
+ // boundaryEvents are removed from the node set and surfaced as ports on
121
+ // their attachedToRef node, so ELK routes their outgoing edges from the
122
+ // correct border position natively.
123
+ const beByRef = {};
124
+ eachNode(structure.nodes, null, (n) => {
125
+ if (n.type === 'boundaryEvent' && n.attachedToRef)
126
+ (beByRef[n.attachedToRef] ||= []).push(n);
127
+ });
128
+
129
+ const children = (structure.nodes || []).map((n) => toElk(n, beByRef)).filter(Boolean);
130
+ for (const a of structure.annotations || []) {
131
+ children.push({ id: a.id, width: a.layout?.width || 100, height: a.layout?.height || 30 });
132
+ }
133
+
134
+ const happyEdges = computeHappyEdges(structure);
135
+ const edges = [];
136
+ eachNode(structure.nodes, null, (n) => {
137
+ for (const e of n.edges || []) {
138
+ const isHappy = happyEdges.has(e.id);
139
+ let source = n.id;
140
+ let sourcePort;
141
+ if (n.type === 'boundaryEvent' && n.attachedToRef) {
142
+ source = n.attachedToRef;
143
+ sourcePort = `__port_${n.id}`;
144
+ }
145
+ edges.push({
146
+ id: e.id,
147
+ sources: [source],
148
+ targets: [e.target],
149
+ sourcePort,
150
+ layoutOptions: {
151
+ 'elk.layered.priority.straightness': isHappy ? '10' : 1,
152
+ 'elk.layered.priority.shortness': isHappy ? '10' : 1,
153
+ 'elk.layered.priority.direction': isHappy ? '10' : 1,
154
+ },
155
+ });
156
+ }
157
+ });
158
+ for (const a of structure.associations || []) {
159
+ edges.push({ id: a.id, sources: [a.sourceRef], targets: [a.targetRef] });
160
+ }
161
+
162
+ const graph = { id: 'root', layoutOptions: ROOT_OPTS, children, edges };
163
+ const res = await elk.layout(graph);
164
+
165
+ // ----- node absolute positions (accumulate parent-relative coords) -----
166
+ const pos = {};
167
+ const accumulate = (node, ox, oy) => {
168
+ for (const c of node.children || []) {
169
+ const ax = ox + (c.x || 0);
170
+ const ay = oy + (c.y || 0);
171
+ pos[c.id] = { x: ax, y: ay, width: c.width, height: c.height };
172
+ if (c.children) accumulate(c, ax, ay);
173
+ }
174
+ };
175
+ accumulate(res, 0, 0);
176
+
177
+ // ----- port positions (boundaryEvent ports on their attachedToRef node) -----
178
+ const portPos = {};
179
+ const accumulatePorts = (node, ox, oy) => {
180
+ for (const p of node.ports || []) {
181
+ portPos[p.id] = {
182
+ x: ox + (p.x || 0),
183
+ y: oy + (p.y || 0),
184
+ width: p.width || 0,
185
+ height: p.height || 0,
186
+ };
187
+ }
188
+ for (const c of node.children || []) {
189
+ accumulatePorts(c, ox + (c.x || 0), oy + (c.y || 0));
190
+ }
191
+ };
192
+ accumulatePorts(res, 0, 0);
193
+
194
+ // ----- container path per node (for edge LCA offset) -----
195
+ const pathOf = {};
196
+ const walkPaths = (nodes, stack) => {
197
+ for (const n of nodes || []) {
198
+ pathOf[n.id] = stack;
199
+ if (n.nodes) walkPaths(n.nodes, [...stack, n.id]);
200
+ }
201
+ };
202
+ walkPaths(structure.nodes, []);
203
+ for (const a of structure.annotations || []) pathOf[a.id] = pathOf[a.id] || [];
204
+
205
+ const lcaOffset = (a, b) => {
206
+ const pa = pathOf[a] || [];
207
+ const pb = pathOf[b] || [];
208
+ let i = 0;
209
+ while (i < pa.length && i < pb.length && pa[i] === pb[i]) i++;
210
+ const lca = i > 0 ? pa[i - 1] : null;
211
+ return lca && pos[lca] ? pos[lca] : { x: 0, y: 0 };
212
+ };
213
+
214
+ // ----- collect returned elk edges by id (coords are LCA-relative) -----
215
+ const elkEdges = {};
216
+ const collectEdges = (node) => {
217
+ for (const e of node.edges || []) elkEdges[e.id] = e;
218
+ for (const c of node.children || []) collectEdges(c);
219
+ };
220
+ collectEdges(res);
221
+
222
+ const waypointsFor = (edgeId, off) => {
223
+ const sec = elkEdges[edgeId]?.sections?.[0];
224
+ if (!sec) return null;
225
+ const pts = [sec.startPoint, ...(sec.bendPoints || []), sec.endPoint];
226
+ return pts.map((p) => [round(p.x + off.x), round(p.y + off.y)]);
227
+ };
228
+
229
+ // ----- write geometry back into the structure -----
230
+ eachNode(structure.nodes, null, (n) => {
231
+ const p = pos[n.id];
232
+ if (p)
233
+ n.layout = {
234
+ x: round(p.x),
235
+ y: round(p.y),
236
+ width: round(p.width),
237
+ height: round(p.height),
238
+ };
239
+ for (const e of n.edges || []) {
240
+ const wp = waypointsFor(e.id, lcaOffset(n.id, e.target));
241
+ if (wp) e.waypoints = wp;
242
+ }
243
+ });
244
+
245
+ // boundaryEvents: positioned from the first waypoint of their outgoing edge.
246
+ // With free port constraints, elkjs reports the port rect on one border but
247
+ // routes the edge from another, so the reported port position and the actual
248
+ // edge origin disagree (the glyph would detach from its own arrow). The edge
249
+ // section is the source of truth — its startPoint sits exactly on the
250
+ // attachedToRef border where the flow leaves — so we center the 36×36 glyph
251
+ // there. Fall back to the port rect only for a boundary event with no edge.
252
+ eachNode(structure.nodes, null, (n) => {
253
+ if (n.type !== 'boundaryEvent' || !n.attachedToRef) return;
254
+ const wp0 = n.edges?.find((e) => e.waypoints?.length)?.waypoints[0];
255
+ const center = wp0 ? { x: wp0[0], y: wp0[1] } : portPos[`__port_${n.id}`];
256
+ if (center)
257
+ n.layout = {
258
+ x: round(center.x - 18),
259
+ y: round(center.y - 18),
260
+ width: 36,
261
+ height: 36,
262
+ };
263
+ });
264
+
265
+ for (const a of structure.annotations || []) {
266
+ const p = pos[a.id];
267
+ if (p)
268
+ a.layout = {
269
+ x: round(p.x),
270
+ y: round(p.y),
271
+ width: round(p.width),
272
+ height: round(p.height),
273
+ };
274
+ }
275
+ for (const a of structure.associations || []) {
276
+ const wp = waypointsFor(a.id, lcaOffset(a.sourceRef, a.targetRef));
277
+ if (wp) a.waypoints = wp;
278
+ }
279
+
280
+ return structure;
281
+ }
282
+
283
+ export { autoLayout };