@uipath/maestro-builder-sdk 6.19.0 → 6.20.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/dist/api-index.md +107 -107
- package/dist/api-members.md +358 -356
- package/dist/check.js +181 -45
- package/dist/cli/commands/flow.js +3 -0
- package/dist/compile-cli.js +51 -0
- package/dist/core/actions.d.ts +13 -5
- package/dist/core/actions.js +1 -1
- package/dist/core/binding-identity.d.ts +22 -0
- package/dist/core/binding-identity.js +28 -0
- package/dist/core/context-index.d.ts +34 -0
- package/dist/core/context-index.js +38 -0
- package/dist/core/definitionless-node-types.d.ts +16 -0
- package/dist/core/definitionless-node-types.js +16 -0
- package/dist/core/form-field-type.d.ts +33 -0
- package/dist/core/form-field-type.js +30 -0
- package/dist/core/hitl-routing.d.ts +8 -0
- package/dist/core/hitl-routing.js +17 -1
- package/dist/core/process-resource-sort-order.d.ts +40 -0
- package/dist/core/process-resource-sort-order.js +44 -0
- package/dist/core/wire-expr.d.ts +27 -0
- package/dist/core/wire-expr.js +34 -0
- package/dist/decompile-cli.js +11 -1
- package/dist/decompile.d.ts +8 -3
- package/dist/decompile.js +680 -104
- package/dist/flow-expr-check.js +1 -1
- package/dist/flow-sdk.d.ts +33 -1
- package/dist/flow-sdk.js +21 -2
- package/dist/flow-step-tree.js +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/merge-cli.js +25 -8
- package/dist/merge-roundtrip.d.ts +81 -0
- package/dist/merge-roundtrip.js +404 -0
- package/dist/merge.d.ts +35 -6
- package/dist/merge.js +124 -24
- package/dist/node-placement.d.ts +93 -0
- package/dist/node-placement.js +163 -0
- package/dist/nodes/transform/flow/node-type.js +2 -1
- package/dist/registry/uip/pipeline-front-door.js +5 -3
- package/dist/serialize.js +112 -61
- package/package.json +2 -2
package/dist/merge.js
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
* preserved from the original; `subflows` and edges (with their ids) come from
|
|
29
29
|
* `edited`; `definitions` and `bindings` are unioned
|
|
30
30
|
* (original wins on conflict, since the original's are the tenant-resolved
|
|
31
|
-
* authority). Node ids are stable by construction (decompile names every step by
|
|
31
|
+
* authority). A binding row's identity is what it binds, not its row id. Node ids are stable by construction (decompile names every step by
|
|
32
32
|
* its node id), which is what lets every re-attach key on id.
|
|
33
33
|
*
|
|
34
34
|
* ## Definitions are keyed by `nodeType@version`, not by `nodeType`
|
|
@@ -63,8 +63,23 @@
|
|
|
63
63
|
* A node whose pair is in NEITHER file cannot be given a definition here, and a
|
|
64
64
|
* merge that stayed silent about it wrote a file it already knew would fail
|
|
65
65
|
* `validate`. Those are reported through {@link MergeOptions.warn}.
|
|
66
|
+
*
|
|
67
|
+
* ## The baseline is also compared with the original
|
|
68
|
+
*
|
|
69
|
+
* `baseline` is the no-edit round trip, so wherever it differs from the original
|
|
70
|
+
* in wire identity (a node, an edge, a switch case id, a human-task outcome id,
|
|
71
|
+
* a binding row), the merged file inherits that change unless the author's edit
|
|
72
|
+
* undid it. Each difference the merged file still carries is reported through
|
|
73
|
+
* `warn`, and `strict` refuses the merge over it; the merged file is also
|
|
74
|
+
* checked for an edge that leaves a handle its kept source node does not
|
|
75
|
+
* declare. See merge-roundtrip.ts.
|
|
66
76
|
*/
|
|
77
|
+
import { bindingIdentity } from './core/binding-identity.js';
|
|
67
78
|
import { jsonClone } from './core/json-file.js';
|
|
79
|
+
import { DEFINITIONLESS_NODE_TYPES } from './core/definitionless-node-types.js';
|
|
80
|
+
import { MergeRefusedError, mergedFileFindings, roundTripFindings } from './merge-roundtrip.js';
|
|
81
|
+
import { placeAddedNodes } from './node-placement.js';
|
|
82
|
+
export { MergeRefusedError } from './merge-roundtrip.js';
|
|
68
83
|
function byId(nodes) {
|
|
69
84
|
return new Map((nodes ?? []).map((n) => [n.id, n]));
|
|
70
85
|
}
|
|
@@ -82,11 +97,31 @@ function nodeUnchanged(a, b) {
|
|
|
82
97
|
return false;
|
|
83
98
|
return JSON.stringify(a.inputs ?? {}) === JSON.stringify(b.inputs ?? {});
|
|
84
99
|
}
|
|
85
|
-
/**
|
|
86
|
-
|
|
87
|
-
|
|
100
|
+
/**
|
|
101
|
+
* Give an edited node the original's position, by id.
|
|
102
|
+
*
|
|
103
|
+
* The original stores it in one of two places: `node.ui` (an SDK-compiled file)
|
|
104
|
+
* or `layout.nodes[id]` (every canvas-saved file — the designer's save moves
|
|
105
|
+
* `ui` into `layout.nodes`). From `node.ui` it is grafted onto the node. With a
|
|
106
|
+
* `layout.nodes` entry, the merged file keeps that entry (it is carried over
|
|
107
|
+
* with the original's `layout`) and the edited node's compile placeholder `ui`
|
|
108
|
+
* is dropped, so the node has one position, not two.
|
|
109
|
+
*/
|
|
110
|
+
function withOriginalLayout(edited, orig, originalLayout) {
|
|
111
|
+
if (!orig)
|
|
88
112
|
return edited;
|
|
89
|
-
|
|
113
|
+
if (orig.ui !== undefined)
|
|
114
|
+
return { ...edited, ui: jsonClone(orig.ui) };
|
|
115
|
+
if (originalLayout?.[orig.id] !== undefined) {
|
|
116
|
+
const { ui: _placeholder, ...rest } = edited;
|
|
117
|
+
return rest;
|
|
118
|
+
}
|
|
119
|
+
return edited;
|
|
120
|
+
}
|
|
121
|
+
/** The original's `layout.nodes` map, when it has one. */
|
|
122
|
+
function layoutNodesOf(f) {
|
|
123
|
+
const nodes = f.layout?.nodes;
|
|
124
|
+
return nodes && typeof nodes === 'object' && !Array.isArray(nodes) ? nodes : undefined;
|
|
90
125
|
}
|
|
91
126
|
export function mergeFlow(original, edited, options = {}) {
|
|
92
127
|
const result = jsonClone(original);
|
|
@@ -104,7 +139,11 @@ export function mergeFlow(original, edited, options = {}) {
|
|
|
104
139
|
}
|
|
105
140
|
else {
|
|
106
141
|
// Added or edited → take the edited node, re-attaching original layout.
|
|
107
|
-
finalNodes.push(withOriginalLayout(jsonClone(e), orig));
|
|
142
|
+
finalNodes.push(withOriginalLayout(jsonClone(e), orig, layoutNodesOf(original)));
|
|
143
|
+
if (orig && orig.type === e.type && orig.typeVersion !== e.typeVersion) {
|
|
144
|
+
options.warn?.(`node ${JSON.stringify(e.id)} moved ${e.type}@${String(orig.typeVersion)} -> ${String(e.typeVersion)} `
|
|
145
|
+
+ `(the edited node takes the SDK's version; flow-builder-sdk#1167).`);
|
|
146
|
+
}
|
|
108
147
|
}
|
|
109
148
|
}
|
|
110
149
|
result.nodes = finalNodes;
|
|
@@ -124,15 +163,25 @@ export function mergeFlow(original, edited, options = {}) {
|
|
|
124
163
|
// one already taken elsewhere in the file, gets a fresh id, from ids unused
|
|
125
164
|
// anywhere in the file. ──
|
|
126
165
|
result.edges = withFileUniqueEdgeIds(edited.edges ?? [], subflowScopes);
|
|
166
|
+
// ── Placement: a node the edit ADDED has only compile's placeholder, an
|
|
167
|
+
// emission-index slot that can land on an original node. Put it next to
|
|
168
|
+
// its predecessor, collision-free, in the store the
|
|
169
|
+
// original uses — see node-placement.ts. Root scope only: a subflow is
|
|
170
|
+
// taken from `edited` whole, layout included. ──
|
|
171
|
+
placeAddedNodes(finalNodes, result.edges, layoutNodesOf(result), (id) => !origNodes.has(id));
|
|
127
172
|
// ── Definitions: the (type, version) pairs used by EVERY node in the file —
|
|
128
173
|
// root and subflows — original winning on conflict (its defs are the
|
|
129
174
|
// tenant-resolved authority). Selecting by root nodes only dropped the
|
|
130
175
|
// definitions a subflow-only node needs, and validate refused the result
|
|
131
176
|
// ("has no matching definition"). ──
|
|
132
177
|
const allNodes = [...finalNodes, ...subflowScopes.flatMap((sf) => sf?.nodes ?? [])];
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
178
|
+
const originalNodes = [
|
|
179
|
+
...(original.nodes ?? []),
|
|
180
|
+
...Object.values(original.subflows ?? {}).flatMap((sf) => sf?.nodes ?? []),
|
|
181
|
+
];
|
|
182
|
+
result.definitions = mergeDefinitions(original.definitions, edited.definitions, allNodes, originalNodes, options.warn);
|
|
183
|
+
// ── Bindings: union by what each row binds (original wins), so resources for
|
|
184
|
+
// newly-added nodes come from `edited` while the original's stay intact. ──
|
|
136
185
|
result.bindings = mergeBindings(original.bindings, edited.bindings);
|
|
137
186
|
// metadata / solutionId / projectId / layout / runtime were carried by the
|
|
138
187
|
// initial clone of `original` and are intentionally left untouched.
|
|
@@ -147,6 +196,19 @@ export function mergeFlow(original, edited, options = {}) {
|
|
|
147
196
|
if (options.baseline && edited.version !== undefined && edited.version !== options.baseline.version) {
|
|
148
197
|
result.version = edited.version;
|
|
149
198
|
}
|
|
199
|
+
// ── What the no-edit round trip changed, and what mixing sources broke. ──
|
|
200
|
+
// Under strict, the findings that do not refuse are still warned, so a
|
|
201
|
+
// refused run shows them too.
|
|
202
|
+
const findings = [
|
|
203
|
+
...(options.baseline ? roundTripFindings(original, options.baseline, result) : []),
|
|
204
|
+
...mergedFileFindings(result, original),
|
|
205
|
+
];
|
|
206
|
+
const refusing = options.strict ? findings.filter((f) => f.refuses) : [];
|
|
207
|
+
for (const f of findings)
|
|
208
|
+
if (!refusing.includes(f))
|
|
209
|
+
options.warn?.(f.message);
|
|
210
|
+
if (refusing.length > 0)
|
|
211
|
+
throw new MergeRefusedError(refusing);
|
|
150
212
|
return result;
|
|
151
213
|
}
|
|
152
214
|
/**
|
|
@@ -186,8 +248,9 @@ function indexDefinitions(defs) {
|
|
|
186
248
|
}
|
|
187
249
|
return ix;
|
|
188
250
|
}
|
|
189
|
-
function mergeDefinitions(orig, edited, nodes, warn) {
|
|
251
|
+
function mergeDefinitions(orig, edited, nodes, originalNodes, warn) {
|
|
190
252
|
const origIx = indexDefinitions(orig);
|
|
253
|
+
const originalPairs = new Set(originalNodes.filter((n) => n.type !== undefined && n.typeVersion !== undefined).map((n) => defKey(n.type, n.typeVersion)));
|
|
191
254
|
const editedIx = indexDefinitions(edited);
|
|
192
255
|
const out = new Map();
|
|
193
256
|
const missing = [];
|
|
@@ -207,25 +270,62 @@ function mergeDefinitions(orig, edited, nodes, warn) {
|
|
|
207
270
|
const def = origIx.get(key) ?? editedIx.get(key);
|
|
208
271
|
if (def)
|
|
209
272
|
out.set(key, def);
|
|
210
|
-
|
|
273
|
+
// A definitionless type is not missing anything.
|
|
274
|
+
else if (!DEFINITIONLESS_NODE_TYPES.has(n.type) && !missing.includes(key))
|
|
211
275
|
missing.push(key);
|
|
212
276
|
}
|
|
213
277
|
for (const key of missing) {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
278
|
+
// A pair the original already pinned with no definition still fails
|
|
279
|
+
// `validate`; the warning says the merge inherited it rather than caused it.
|
|
280
|
+
warn?.(originalPairs.has(key)
|
|
281
|
+
? `node type ${key} is pinned by a merged node and already had no definition in the original, `
|
|
282
|
+
+ `and the edited flow carries none either, so the merged file has none; `
|
|
283
|
+
+ `\`validate\` will report "has no matching definition", as it does for the original. `
|
|
284
|
+
+ `Recompile the edited source against an SDK that bundles ${key} to add one.`
|
|
285
|
+
: `node type ${key} is pinned by a merged node but neither the original nor the edited flow `
|
|
286
|
+
+ `carries a definition for that exact pair, so the merged file has none; `
|
|
287
|
+
+ `\`validate\` will report "has no matching definition". `
|
|
288
|
+
+ `Recompile the edited source against an SDK that bundles ${key}, or align the node's typeVersion `
|
|
289
|
+
+ `with a definition one of the inputs does carry.`);
|
|
219
290
|
}
|
|
220
291
|
return [...out.values()].map((d) => jsonClone(d));
|
|
221
292
|
}
|
|
293
|
+
/**
|
|
294
|
+
* The original's rows, then each edited row that binds something the original
|
|
295
|
+
* does not (a resource of a node the user added, or one an edit re-pointed).
|
|
296
|
+
*
|
|
297
|
+
* Original wins on the same identity: its rows are the tenant-resolved
|
|
298
|
+
* authority, and a canvas row can carry values (a `default`, a folder) the
|
|
299
|
+
* compile does not. An added row keeps its id unless an original row already
|
|
300
|
+
* holds it, in which case it gets a fresh one — ids are unreferenced labels, but
|
|
301
|
+
* two rows with one id are not a valid file.
|
|
302
|
+
*
|
|
303
|
+
* Rows no surviving node uses are NOT pruned. The file has no uniform
|
|
304
|
+
* node-to-row reference to decide "unused" by: an in-solution Flow is named by
|
|
305
|
+
* the node TYPE's suffix, a connection by an id in the node's inputs, a published
|
|
306
|
+
* process by a folder-qualified key that no node field spells. A wrong prune
|
|
307
|
+
* drops a row a kept node needs, and the run fails to resolve its resource; an
|
|
308
|
+
* orphan row is accepted by `validate` and ignored at run time.
|
|
309
|
+
*/
|
|
222
310
|
function mergeBindings(orig, edited) {
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
if (
|
|
229
|
-
|
|
230
|
-
|
|
311
|
+
const out = (orig ?? []).map((b) => jsonClone(b));
|
|
312
|
+
const identities = new Set(out.map(bindingIdentity));
|
|
313
|
+
const ids = new Set(out.map((b) => b.id).filter((id) => typeof id === 'string'));
|
|
314
|
+
for (const b of edited ?? []) {
|
|
315
|
+
const identity = bindingIdentity(b);
|
|
316
|
+
if (identities.has(identity))
|
|
317
|
+
continue;
|
|
318
|
+
identities.add(identity);
|
|
319
|
+
const row = jsonClone(b);
|
|
320
|
+
if (typeof row.id === 'string' && ids.has(row.id)) {
|
|
321
|
+
let n = 2;
|
|
322
|
+
while (ids.has(`${row.id}${n}`))
|
|
323
|
+
n++;
|
|
324
|
+
row.id = `${row.id}${n}`;
|
|
325
|
+
}
|
|
326
|
+
if (typeof row.id === 'string')
|
|
327
|
+
ids.add(row.id);
|
|
328
|
+
out.push(row);
|
|
329
|
+
}
|
|
330
|
+
return out;
|
|
231
331
|
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* node-placement — where compile puts a node it has no position for, and where
|
|
3
|
+
* merge moves one the edit added.
|
|
4
|
+
*
|
|
5
|
+
* Compile gives every node a placeholder from its emission index,
|
|
6
|
+
* `x = 256 + 288 × index, y = 144` (`+160` inside a container). Merge replaces
|
|
7
|
+
* that placeholder on each node the edit added:
|
|
8
|
+
*
|
|
9
|
+
* - next to a positioned predecessor in the same container: one slot right;
|
|
10
|
+
* - else, when the incoming edge comes from the node's own container (the first
|
|
11
|
+
* node of a loop body): at the body origin, inside the container's padding;
|
|
12
|
+
* - else, one slot left of a positioned successor in the same container;
|
|
13
|
+
* - then the nearest free spot around that candidate.
|
|
14
|
+
*
|
|
15
|
+
* Example: the edit inserts `auditLog` between `extractInvoice (448,144)` and
|
|
16
|
+
* `routeInvoice (640,144)`. The candidate `(640,144)` is taken, so `auditLog`
|
|
17
|
+
* goes to the first free spot, `(640,264)`.
|
|
18
|
+
*
|
|
19
|
+
* A child's position is relative to its container, as on the canvas.
|
|
20
|
+
*
|
|
21
|
+
* The position goes where the original stores positions: `layout.nodes[id]`
|
|
22
|
+
* when it has a `layout.nodes` map, otherwise `node.ui`. Positions are read the
|
|
23
|
+
* way the designer reads them (flow-workbench `flow-schema`
|
|
24
|
+
* `workflow-format-conversion.ts` `mergeLayout`): a `layout.nodes[id]` entry
|
|
25
|
+
* wins over `node.ui`, field by field.
|
|
26
|
+
*
|
|
27
|
+
* `findFreePosition` is a copy of flow-workbench `flow-core`
|
|
28
|
+
* `layout/collision-detection.ts` (same 24 px padding, 40 px step, 50 rings,
|
|
29
|
+
* offset order, 96 px default size, sticky notes excluded), because the SDK may
|
|
30
|
+
* not import `@uipath/flow-core` (lint FB015).
|
|
31
|
+
*/
|
|
32
|
+
/** A position on the canvas. */
|
|
33
|
+
export interface Point {
|
|
34
|
+
x: number;
|
|
35
|
+
y: number;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Compile's placeholder for the `index`-th node it emits.
|
|
39
|
+
*
|
|
40
|
+
* @param index - The node's emission index in the file.
|
|
41
|
+
* @param nested - Whether the node sits inside a container.
|
|
42
|
+
* @returns The placeholder position.
|
|
43
|
+
*/
|
|
44
|
+
export declare function placeholderPosition(index: number, nested: boolean): Point;
|
|
45
|
+
/**
|
|
46
|
+
* Whether `p` is a compile placeholder rather than a position someone chose.
|
|
47
|
+
* An author `.layout()` entry that happens to hit the same grid point reads as
|
|
48
|
+
* a placeholder; that is the one ambiguity.
|
|
49
|
+
*/
|
|
50
|
+
export declare function isPlaceholderPosition(p: Point | undefined): boolean;
|
|
51
|
+
/** A node's `ui` (or its `layout.nodes` entry): the fields placement reads. */
|
|
52
|
+
interface Ui {
|
|
53
|
+
position?: Point;
|
|
54
|
+
size?: {
|
|
55
|
+
width?: number;
|
|
56
|
+
height?: number;
|
|
57
|
+
};
|
|
58
|
+
[k: string]: unknown;
|
|
59
|
+
}
|
|
60
|
+
/** A node as merge holds it; `ui` is untyped in the file model. */
|
|
61
|
+
interface PlacedNode {
|
|
62
|
+
id: string;
|
|
63
|
+
type?: string;
|
|
64
|
+
parentId?: string;
|
|
65
|
+
ui?: unknown;
|
|
66
|
+
[k: string]: unknown;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Give each node the edit added a position next to its predecessor, in place.
|
|
70
|
+
*
|
|
71
|
+
* A node is placed when it is not in the original and its position is a
|
|
72
|
+
* compile placeholder (an author `.layout()` position is kept). Its candidate,
|
|
73
|
+
* in order: one slot right of the first predecessor in the same container that
|
|
74
|
+
* already has a position (an original node, or an added node placed before it);
|
|
75
|
+
* the body origin, when an edge enters it from its own container; one slot left
|
|
76
|
+
* of a positioned successor in the same container. With none, the node keeps
|
|
77
|
+
* its placeholder, and placed nodes avoid it.
|
|
78
|
+
*
|
|
79
|
+
* Where the result is written follows the file: when the original stores
|
|
80
|
+
* positions in `layout.nodes` (every canvas-saved file does), the position goes
|
|
81
|
+
* there and the placeholder `node.ui` is dropped, so the file has one store;
|
|
82
|
+
* otherwise `node.ui.position` is updated.
|
|
83
|
+
*
|
|
84
|
+
* @param nodes - The merged root nodes (mutated).
|
|
85
|
+
* @param edges - The merged root edges.
|
|
86
|
+
* @param layoutNodes - The merged file's `layout.nodes`, or undefined when the original has none (mutated).
|
|
87
|
+
* @param isAdded - Whether a node id is new in this merge.
|
|
88
|
+
*/
|
|
89
|
+
export declare function placeAddedNodes(nodes: PlacedNode[], edges: ReadonlyArray<{
|
|
90
|
+
sourceNodeId: string;
|
|
91
|
+
targetNodeId: string;
|
|
92
|
+
}>, layoutNodes: Record<string, Ui> | undefined, isAdded: (id: string) => boolean): void;
|
|
93
|
+
export {};
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* node-placement — where compile puts a node it has no position for, and where
|
|
3
|
+
* merge moves one the edit added.
|
|
4
|
+
*
|
|
5
|
+
* Compile gives every node a placeholder from its emission index,
|
|
6
|
+
* `x = 256 + 288 × index, y = 144` (`+160` inside a container). Merge replaces
|
|
7
|
+
* that placeholder on each node the edit added:
|
|
8
|
+
*
|
|
9
|
+
* - next to a positioned predecessor in the same container: one slot right;
|
|
10
|
+
* - else, when the incoming edge comes from the node's own container (the first
|
|
11
|
+
* node of a loop body): at the body origin, inside the container's padding;
|
|
12
|
+
* - else, one slot left of a positioned successor in the same container;
|
|
13
|
+
* - then the nearest free spot around that candidate.
|
|
14
|
+
*
|
|
15
|
+
* Example: the edit inserts `auditLog` between `extractInvoice (448,144)` and
|
|
16
|
+
* `routeInvoice (640,144)`. The candidate `(640,144)` is taken, so `auditLog`
|
|
17
|
+
* goes to the first free spot, `(640,264)`.
|
|
18
|
+
*
|
|
19
|
+
* A child's position is relative to its container, as on the canvas.
|
|
20
|
+
*
|
|
21
|
+
* The position goes where the original stores positions: `layout.nodes[id]`
|
|
22
|
+
* when it has a `layout.nodes` map, otherwise `node.ui`. Positions are read the
|
|
23
|
+
* way the designer reads them (flow-workbench `flow-schema`
|
|
24
|
+
* `workflow-format-conversion.ts` `mergeLayout`): a `layout.nodes[id]` entry
|
|
25
|
+
* wins over `node.ui`, field by field.
|
|
26
|
+
*
|
|
27
|
+
* `findFreePosition` is a copy of flow-workbench `flow-core`
|
|
28
|
+
* `layout/collision-detection.ts` (same 24 px padding, 40 px step, 50 rings,
|
|
29
|
+
* offset order, 96 px default size, sticky notes excluded), because the SDK may
|
|
30
|
+
* not import `@uipath/flow-core` (lint FB015).
|
|
31
|
+
*/
|
|
32
|
+
// Sticky notes may overlap anything (flow-core skips them in collisions).
|
|
33
|
+
import { STICKY_NOTE_TYPE } from './core/definitionless-node-types.js';
|
|
34
|
+
/** Where compile's placeholder row starts, and its spacing. */
|
|
35
|
+
const PLACEHOLDER = { x0: 256, dx: 288, y0: 144, nestedDy: 160 };
|
|
36
|
+
/** The default node box the canvas assumes (flow-core `resolveWidth`/`resolveHeight`). */
|
|
37
|
+
const DEFAULT_SIZE = 96;
|
|
38
|
+
/** One slot to the right of a node: its width plus an equal gap, the spacing of a canvas row. */
|
|
39
|
+
const SLOT_DX = 192;
|
|
40
|
+
/** flow-core `findFreePosition` defaults. */
|
|
41
|
+
const PADDING = 24;
|
|
42
|
+
const STEP = 40;
|
|
43
|
+
const MAX_RINGS = 50;
|
|
44
|
+
/**
|
|
45
|
+
* Where a container's first body node goes, relative to the container: inside
|
|
46
|
+
* its left and top padding (flow-core `defaultContainerFit`: 144 left, 96 top).
|
|
47
|
+
*/
|
|
48
|
+
const BODY_ORIGIN = { x: 144, y: 96 };
|
|
49
|
+
/**
|
|
50
|
+
* Compile's placeholder for the `index`-th node it emits.
|
|
51
|
+
*
|
|
52
|
+
* @param index - The node's emission index in the file.
|
|
53
|
+
* @param nested - Whether the node sits inside a container.
|
|
54
|
+
* @returns The placeholder position.
|
|
55
|
+
*/
|
|
56
|
+
export function placeholderPosition(index, nested) {
|
|
57
|
+
return { x: PLACEHOLDER.x0 + index * PLACEHOLDER.dx, y: PLACEHOLDER.y0 + (nested ? PLACEHOLDER.nestedDy : 0) };
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Whether `p` is a compile placeholder rather than a position someone chose.
|
|
61
|
+
* An author `.layout()` entry that happens to hit the same grid point reads as
|
|
62
|
+
* a placeholder; that is the one ambiguity.
|
|
63
|
+
*/
|
|
64
|
+
export function isPlaceholderPosition(p) {
|
|
65
|
+
if (!p)
|
|
66
|
+
return false;
|
|
67
|
+
const k = (p.x - PLACEHOLDER.x0) / PLACEHOLDER.dx;
|
|
68
|
+
return Number.isInteger(k) && k >= 0 && (p.y === PLACEHOLDER.y0 || p.y === PLACEHOLDER.y0 + PLACEHOLDER.nestedDy);
|
|
69
|
+
}
|
|
70
|
+
const overlaps = (a, b) => !(a.x + a.width + PADDING <= b.x || b.x + b.width + PADDING <= a.x
|
|
71
|
+
|| a.y + a.height + PADDING <= b.y || b.y + b.height + PADDING <= a.y);
|
|
72
|
+
/** flow-core `findFreePosition`: the candidate, else the first free spot on rings around it. */
|
|
73
|
+
function findFreePosition(candidate, size, others) {
|
|
74
|
+
const free = (p) => !others.some((o) => overlaps({ ...p, ...size }, o));
|
|
75
|
+
if (free(candidate))
|
|
76
|
+
return candidate;
|
|
77
|
+
for (let d = STEP; d <= STEP * MAX_RINGS; d += STEP) {
|
|
78
|
+
for (const [dx, dy] of [[d, 0], [0, d], [d, d], [-d, 0], [0, -d], [-d, -d]]) {
|
|
79
|
+
const p = { x: candidate.x + dx, y: candidate.y + dy };
|
|
80
|
+
if (free(p))
|
|
81
|
+
return p;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return candidate;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Give each node the edit added a position next to its predecessor, in place.
|
|
88
|
+
*
|
|
89
|
+
* A node is placed when it is not in the original and its position is a
|
|
90
|
+
* compile placeholder (an author `.layout()` position is kept). Its candidate,
|
|
91
|
+
* in order: one slot right of the first predecessor in the same container that
|
|
92
|
+
* already has a position (an original node, or an added node placed before it);
|
|
93
|
+
* the body origin, when an edge enters it from its own container; one slot left
|
|
94
|
+
* of a positioned successor in the same container. With none, the node keeps
|
|
95
|
+
* its placeholder, and placed nodes avoid it.
|
|
96
|
+
*
|
|
97
|
+
* Where the result is written follows the file: when the original stores
|
|
98
|
+
* positions in `layout.nodes` (every canvas-saved file does), the position goes
|
|
99
|
+
* there and the placeholder `node.ui` is dropped, so the file has one store;
|
|
100
|
+
* otherwise `node.ui.position` is updated.
|
|
101
|
+
*
|
|
102
|
+
* @param nodes - The merged root nodes (mutated).
|
|
103
|
+
* @param edges - The merged root edges.
|
|
104
|
+
* @param layoutNodes - The merged file's `layout.nodes`, or undefined when the original has none (mutated).
|
|
105
|
+
* @param isAdded - Whether a node id is new in this merge.
|
|
106
|
+
*/
|
|
107
|
+
export function placeAddedNodes(nodes, edges, layoutNodes, isAdded) {
|
|
108
|
+
const byId = new Map(nodes.map((n) => [n.id, n]));
|
|
109
|
+
const uiOf = (n) => {
|
|
110
|
+
const entry = layoutNodes?.[n.id];
|
|
111
|
+
const own = n.ui;
|
|
112
|
+
return entry ? { ...own, ...entry } : own;
|
|
113
|
+
};
|
|
114
|
+
const sizeOf = (n) => ({
|
|
115
|
+
width: uiOf(n)?.size?.width ?? DEFAULT_SIZE,
|
|
116
|
+
height: uiOf(n)?.size?.height ?? DEFAULT_SIZE,
|
|
117
|
+
});
|
|
118
|
+
const pending = new Set(nodes.filter((n) => isAdded(n.id) && n.type !== STICKY_NOTE_TYPE && isPlaceholderPosition(uiOf(n)?.position)).map((n) => n.id));
|
|
119
|
+
const positioned = (id) => {
|
|
120
|
+
const n = byId.get(id);
|
|
121
|
+
return n !== undefined && !pending.has(id) && uiOf(n)?.position !== undefined;
|
|
122
|
+
};
|
|
123
|
+
// Place in waves, so a run of added nodes chains off the first one placed.
|
|
124
|
+
for (let progress = true; progress && pending.size > 0;) {
|
|
125
|
+
progress = false;
|
|
126
|
+
for (const id of [...pending]) {
|
|
127
|
+
const n = byId.get(id);
|
|
128
|
+
const sameScope = (other) => byId.get(other)?.parentId === n.parentId;
|
|
129
|
+
const pred = edges.find((e) => e.targetNodeId === id && sameScope(e.sourceNodeId) && positioned(e.sourceNodeId));
|
|
130
|
+
const entersBody = !pred && n.parentId !== undefined && byId.has(n.parentId)
|
|
131
|
+
&& edges.some((e) => e.targetNodeId === id && e.sourceNodeId === n.parentId);
|
|
132
|
+
const succ = pred || entersBody ? undefined
|
|
133
|
+
: edges.find((e) => e.sourceNodeId === id && sameScope(e.targetNodeId) && positioned(e.targetNodeId));
|
|
134
|
+
let candidate;
|
|
135
|
+
if (pred || succ) {
|
|
136
|
+
const anchor = uiOf(byId.get(pred?.sourceNodeId ?? succ.targetNodeId)).position;
|
|
137
|
+
candidate = { x: anchor.x + (pred ? SLOT_DX : -SLOT_DX), y: anchor.y };
|
|
138
|
+
}
|
|
139
|
+
else if (entersBody) {
|
|
140
|
+
candidate = { ...BODY_ORIGIN };
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
// Still-pending nodes count at their placeholder: one that never finds an
|
|
146
|
+
// anchor stays there, and nothing may land on it.
|
|
147
|
+
const others = nodes
|
|
148
|
+
.filter((o) => o.id !== id && o.parentId === n.parentId && o.type !== STICKY_NOTE_TYPE && uiOf(o)?.position !== undefined)
|
|
149
|
+
.map((o) => ({ ...uiOf(o).position, ...sizeOf(o) }));
|
|
150
|
+
const position = findFreePosition(candidate, sizeOf(n), others);
|
|
151
|
+
const ui = { ...uiOf(n), position };
|
|
152
|
+
if (layoutNodes) {
|
|
153
|
+
layoutNodes[id] = ui;
|
|
154
|
+
delete n.ui;
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
n.ui = ui;
|
|
158
|
+
}
|
|
159
|
+
pending.delete(id);
|
|
160
|
+
progress = true;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* codes and messages unchanged.
|
|
8
8
|
*/
|
|
9
9
|
import { Expr } from '../../../core/expr.js';
|
|
10
|
+
import { wireExprBody } from '../../../core/wire-expr.js';
|
|
10
11
|
import { jsonClone } from '../../../core/json-file.js';
|
|
11
12
|
import { VARIANT_OPERATION } from './spec.js';
|
|
12
13
|
import { checkTransform } from './check.js';
|
|
@@ -139,7 +140,7 @@ function liftTransform(node, variant, ctx) {
|
|
|
139
140
|
return {
|
|
140
141
|
kind: 'transform',
|
|
141
142
|
inputs: {
|
|
142
|
-
collection: (raw !== undefined ? new Expr(
|
|
143
|
+
collection: (raw !== undefined ? new Expr(wireExprBody(raw)) : undefined),
|
|
143
144
|
...(variant ? { variant } : {}),
|
|
144
145
|
operations: operations,
|
|
145
146
|
},
|
|
@@ -20,7 +20,9 @@
|
|
|
20
20
|
* folder and every folder above it, then `PATH`.
|
|
21
21
|
*
|
|
22
22
|
* The block brings its own imports, `existsSync` among them, and defines
|
|
23
|
-
* `
|
|
23
|
+
* `frontDoor` (`{ via: 'uip' | 'sdk', file, prefix }`) and `run(verb, args)`;
|
|
24
|
+
* the pipeline body uses them. `via` says which door won, because on Windows
|
|
25
|
+
* `file` is node.exe or a full path, never the bare name. Nothing in it names a path on
|
|
24
26
|
* the machine that generated it, so a pipeline stays relocatable.
|
|
25
27
|
*
|
|
26
28
|
* Example, for `kind = 'flow'` on a machine whose `uip` has the commands:
|
|
@@ -61,7 +63,7 @@ function uipFrontDoor() {
|
|
|
61
63
|
return undefined; // a uip shim that cannot be read: use the SDK instead
|
|
62
64
|
}
|
|
63
65
|
const probe = spawnSync(uip.file, [...uip.args, 'maestro', KIND, 'compile', '--help'], { stdio: 'ignore' });
|
|
64
|
-
return !probe.error && probe.status === 0 ? { file: uip.file, prefix: [...uip.args, 'maestro', KIND] } : undefined;
|
|
66
|
+
return !probe.error && probe.status === 0 ? { via: 'uip', file: uip.file, prefix: [...uip.args, 'maestro', KIND] } : undefined;
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
// 2. The SDK's flow-sdk bin, where \`npx --no-install flow-sdk\` looked for it.
|
|
@@ -78,7 +80,7 @@ function sdkFrontDoor() {
|
|
|
78
80
|
const local = bins.map((bin) => join(bin, 'flow-sdk')).find((p) => existsSync(p));
|
|
79
81
|
sdk = { file: local ?? 'flow-sdk', args: [] };
|
|
80
82
|
}
|
|
81
|
-
return { file: sdk.file, prefix: [...sdk.args, KIND] };
|
|
83
|
+
return { via: 'sdk', file: sdk.file, prefix: [...sdk.args, KIND] };
|
|
82
84
|
}
|
|
83
85
|
|
|
84
86
|
const frontDoor = uipFrontDoor() ?? sdkFrontDoor();
|