@revenexx/studio-integrations 2.8.1 → 2.9.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.
- package/CHANGELOG.md +22 -0
- package/dist/module.json +1 -1
- package/dist/runtime/components/Integrations/ExpressionInput.vue +31 -0
- package/dist/runtime/components/Integrations/NodeConfigField.vue +5 -1
- package/dist/runtime/components/Integrations/NodePalette.vue +72 -64
- package/dist/runtime/components/Integrations/RunDetailPanel.vue +11 -5
- package/dist/runtime/components/Integrations/TemplateInstallStep.vue +2 -2
- package/dist/runtime/components/Integrations/WorkflowEdgeView.d.vue.ts +6 -0
- package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +4 -2
- package/dist/runtime/components/Integrations/WorkflowEdgeView.vue.d.ts +6 -0
- package/dist/runtime/components/Integrations/WorkflowEditor.vue +147 -27
- package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +36 -12
- package/dist/runtime/components/Integrations/WorkflowNodeView.d.vue.ts +14 -0
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue +162 -13
- package/dist/runtime/components/Integrations/WorkflowNodeView.vue.d.ts +14 -0
- package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +34 -12
- package/dist/runtime/components/Integrations/WorkflowTriggerView.d.vue.ts +8 -1
- package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +3 -1
- package/dist/runtime/components/Integrations/WorkflowTriggerView.vue.d.ts +8 -1
- package/dist/runtime/components/Integrations/canvasSides.d.ts +27 -0
- package/dist/runtime/components/Integrations/canvasSides.js +22 -0
- package/dist/runtime/composables/useIntegrationsCredentials.d.ts +4 -1
- package/dist/runtime/composables/useIntegrationsCredentials.js +8 -3
- package/dist/runtime/data/canvasShapes.d.ts +139 -0
- package/dist/runtime/data/canvasShapes.js +130 -0
- package/dist/runtime/data/connectionRules.d.ts +110 -0
- package/dist/runtime/data/connectionRules.js +110 -0
- package/dist/runtime/data/graphShapes.d.ts +175 -0
- package/dist/runtime/data/graphShapes.js +359 -0
- package/dist/runtime/data/handleId.d.ts +12 -0
- package/dist/runtime/data/handleId.js +3 -0
- package/dist/runtime/data/refusalMessage.d.ts +32 -0
- package/dist/runtime/data/refusalMessage.js +42 -0
- package/dist/runtime/data/saveRefusal.d.ts +31 -0
- package/dist/runtime/data/saveRefusal.js +25 -0
- package/dist/runtime/i18n/messages/de.json +36 -1
- package/dist/runtime/i18n/messages/en.json +36 -1
- package/dist/runtime/pages/integrations/schedules.vue +1 -1
- package/dist/runtime/pages/integrations/workflows/[id].vue +38 -3
- package/dist/runtime/types/integrations.d.ts +13 -4
- package/dist/runtime/utils/skippedStep.d.ts +39 -0
- package/dist/runtime/utils/skippedStep.js +22 -0
- package/package.json +1 -1
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
const PRECEDENCE = ["splitter-bypassed", "merger-across-splitters", "undecomposable-shape"];
|
|
2
|
+
const ROOT = "\0root";
|
|
3
|
+
function compare(a, b) {
|
|
4
|
+
if (a === b) {
|
|
5
|
+
return 0;
|
|
6
|
+
}
|
|
7
|
+
const left = Array.from(a);
|
|
8
|
+
const right = Array.from(b);
|
|
9
|
+
for (let i = 0; i < left.length && i < right.length; i++) {
|
|
10
|
+
const difference = left[i].codePointAt(0) - right[i].codePointAt(0);
|
|
11
|
+
if (difference !== 0) {
|
|
12
|
+
return difference < 0 ? -1 : 1;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return left.length < right.length ? -1 : 1;
|
|
16
|
+
}
|
|
17
|
+
function compareLists(a, b) {
|
|
18
|
+
for (let i = 0; i < a.length && i < b.length; i++) {
|
|
19
|
+
const difference = compare(a[i], b[i]);
|
|
20
|
+
if (difference !== 0) {
|
|
21
|
+
return difference;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return a.length - b.length;
|
|
25
|
+
}
|
|
26
|
+
function comparePorts(a, b) {
|
|
27
|
+
return compare(a.nodeId, b.nodeId) || compare(a.port, b.port);
|
|
28
|
+
}
|
|
29
|
+
function compareOptionalPorts(a, b) {
|
|
30
|
+
return a && b ? comparePorts(a, b) : a ? -1 : b ? 1 : 0;
|
|
31
|
+
}
|
|
32
|
+
function sorted(values) {
|
|
33
|
+
return [...values].sort(compare);
|
|
34
|
+
}
|
|
35
|
+
const inputKey = (nodeId, input) => `${nodeId}\0${input}`;
|
|
36
|
+
const crossingKey = (c) => c ? [c.from, c.port, c.to, c.input].join("\0") : "";
|
|
37
|
+
export function analyzeGraphShapes(blob, opts) {
|
|
38
|
+
const nodeIds = new Set(blob.nodes.map((n) => n.id));
|
|
39
|
+
const iterators = new Set(opts.iterators.filter((id) => nodeIds.has(id)));
|
|
40
|
+
const terminals = new Set(opts.terminals ?? []);
|
|
41
|
+
const connections = [];
|
|
42
|
+
blob.edges.forEach((edge, index) => {
|
|
43
|
+
if (nodeIds.has(edge.to.nodeId)) {
|
|
44
|
+
connections.push({
|
|
45
|
+
index,
|
|
46
|
+
from: edge.from.nodeId,
|
|
47
|
+
port: edge.from.output,
|
|
48
|
+
to: edge.to.nodeId,
|
|
49
|
+
input: edge.to.input ?? "in"
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
const handles = sorted(new Set(connections.filter((c) => !nodeIds.has(c.from)).map((c) => c.from)));
|
|
54
|
+
const fed = new Set(connections.map((c) => c.to));
|
|
55
|
+
const origins = [...handles, ...sorted([...nodeIds].filter((id) => !fed.has(id)))];
|
|
56
|
+
for (const origin of origins) {
|
|
57
|
+
connections.push({ index: -1, from: ROOT, port: origin, to: origin, input: "in" });
|
|
58
|
+
}
|
|
59
|
+
const vertices = [ROOT, ...handles, ...sorted(nodeIds)];
|
|
60
|
+
const outgoing = new Map(vertices.map((v) => [v, []]));
|
|
61
|
+
const incoming = new Map(vertices.map((v) => [v, []]));
|
|
62
|
+
for (const c of connections) {
|
|
63
|
+
outgoing.get(c.from).push(c);
|
|
64
|
+
incoming.get(c.to).push(c);
|
|
65
|
+
}
|
|
66
|
+
const descendants = /* @__PURE__ */ new Map();
|
|
67
|
+
for (const v of vertices) {
|
|
68
|
+
const seen = /* @__PURE__ */ new Set();
|
|
69
|
+
const queue = outgoing.get(v).map((c) => c.to);
|
|
70
|
+
while (queue.length > 0) {
|
|
71
|
+
const next = queue.shift();
|
|
72
|
+
if (!seen.has(next)) {
|
|
73
|
+
seen.add(next);
|
|
74
|
+
queue.push(...outgoing.get(next).map((c) => c.to));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
descendants.set(v, seen);
|
|
78
|
+
}
|
|
79
|
+
const reaches = (c, v) => c.to === v || descendants.get(c.to).has(v);
|
|
80
|
+
const onCycle = sorted([...nodeIds].filter((id) => descendants.get(id).has(id)));
|
|
81
|
+
if (onCycle.length > 0) {
|
|
82
|
+
return withHelpers(
|
|
83
|
+
{
|
|
84
|
+
splitters: [],
|
|
85
|
+
mergers: [],
|
|
86
|
+
converging: [],
|
|
87
|
+
virtualMergers: [],
|
|
88
|
+
loopBodies: {},
|
|
89
|
+
violations: onCycle.map((nodeId) => ({ rule: "cycle", nodeId }))
|
|
90
|
+
},
|
|
91
|
+
() => null,
|
|
92
|
+
() => false
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
const order = [];
|
|
96
|
+
const inDegree = new Map(vertices.map((v) => [v, incoming.get(v).length]));
|
|
97
|
+
const ready = vertices.filter((v) => inDegree.get(v) === 0);
|
|
98
|
+
while (ready.length > 0) {
|
|
99
|
+
const v = ready.shift();
|
|
100
|
+
order.push(v);
|
|
101
|
+
for (const c of outgoing.get(v)) {
|
|
102
|
+
inDegree.set(c.to, inDegree.get(c.to) - 1);
|
|
103
|
+
if (inDegree.get(c.to) === 0) {
|
|
104
|
+
ready.push(c.to);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const dominators = /* @__PURE__ */ new Map();
|
|
109
|
+
for (const v of order) {
|
|
110
|
+
const [first, ...rest] = incoming.get(v).map((c) => dominators.get(c.from));
|
|
111
|
+
const common = [...first ?? []].filter((d) => rest.every((ofPred) => ofPred.has(d)));
|
|
112
|
+
dominators.set(v, /* @__PURE__ */ new Set([...common, v]));
|
|
113
|
+
}
|
|
114
|
+
const violations = [];
|
|
115
|
+
const splitters = [];
|
|
116
|
+
for (const v of vertices.slice(1)) {
|
|
117
|
+
const perPort = /* @__PURE__ */ new Map();
|
|
118
|
+
for (const c of outgoing.get(v)) {
|
|
119
|
+
perPort.set(c.port, (perPort.get(c.port) ?? 0) + 1);
|
|
120
|
+
}
|
|
121
|
+
for (const [port, count] of perPort) {
|
|
122
|
+
if (count > 1) {
|
|
123
|
+
splitters.push({ nodeId: v, port });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
splitters.sort(comparePorts);
|
|
128
|
+
const bodies = /* @__PURE__ */ new Map();
|
|
129
|
+
for (const it of sorted(iterators)) {
|
|
130
|
+
const body = /* @__PURE__ */ new Set();
|
|
131
|
+
for (const c of outgoing.get(it)) {
|
|
132
|
+
if (c.port === "item") {
|
|
133
|
+
body.add(c.to);
|
|
134
|
+
descendants.get(c.to).forEach((d) => body.add(d));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
bodies.set(it, body);
|
|
138
|
+
}
|
|
139
|
+
const crossed = /* @__PURE__ */ new Set();
|
|
140
|
+
const loopBodies = {};
|
|
141
|
+
for (const [it, body] of bodies) {
|
|
142
|
+
for (const c of connections) {
|
|
143
|
+
if (body.has(c.to) && !body.has(c.from) && !(c.from === it && c.port === "item")) {
|
|
144
|
+
violations.push({ rule: "loop-boundary", nodeId: c.to, edgeIndex: c.index });
|
|
145
|
+
crossed.add(inputKey(c.to, c.input));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const nested = /* @__PURE__ */ new Set();
|
|
149
|
+
for (const inner of body) {
|
|
150
|
+
bodies.get(inner)?.forEach((n) => nested.add(n));
|
|
151
|
+
}
|
|
152
|
+
const ownLevel = [...body].filter((n) => !nested.has(n) && !terminals.has(n));
|
|
153
|
+
const ends = sorted(
|
|
154
|
+
ownLevel.filter(
|
|
155
|
+
(n) => !outgoing.get(n).some((c) => ownLevel.includes(c.to) && !(iterators.has(n) && c.port === "item"))
|
|
156
|
+
)
|
|
157
|
+
);
|
|
158
|
+
if (body.size > 0 && ends.length !== 1) {
|
|
159
|
+
violations.push({ rule: "loop-end-not-unique", nodeId: it });
|
|
160
|
+
}
|
|
161
|
+
loopBodies[it] = ends.length === 1 ? { members: sorted(body), endNode: ends[0] } : { members: sorted(body) };
|
|
162
|
+
}
|
|
163
|
+
const nearestCommonDominator = (sources) => {
|
|
164
|
+
let common = [...dominators.get(sources[0])];
|
|
165
|
+
for (const s of sources.slice(1)) {
|
|
166
|
+
const ofSource = dominators.get(s);
|
|
167
|
+
common = common.filter((d) => ofSource.has(d));
|
|
168
|
+
}
|
|
169
|
+
return common.reduce((best, d) => dominators.get(d).size > dominators.get(best).size ? d : best);
|
|
170
|
+
};
|
|
171
|
+
const bypass = (branches) => {
|
|
172
|
+
const candidates = splitters.filter(({ nodeId, port }) => {
|
|
173
|
+
const onPort = outgoing.get(nodeId).filter((c) => c.port === port);
|
|
174
|
+
return branches.every((b) => onPort.some((c) => c === b || reaches(c, b.from)));
|
|
175
|
+
}).sort((a, b) => dominators.get(b.nodeId).size - dominators.get(a.nodeId).size || comparePorts(a, b));
|
|
176
|
+
for (const splitter of candidates) {
|
|
177
|
+
const around = sorted(branches.filter((b) => !dominators.get(b.from).has(splitter.nodeId)).map((b) => b.from));
|
|
178
|
+
if (around.length > 0) {
|
|
179
|
+
return { rule: "splitter-bypassed", splitter, branch: around[0] };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return { rule: "splitter-bypassed" };
|
|
183
|
+
};
|
|
184
|
+
const crossing = (n, port, target) => {
|
|
185
|
+
const reached = outgoing.get(n).filter((c) => c.port === port).map((c) => /* @__PURE__ */ new Set([c.to, ...descendants.get(c.to)]));
|
|
186
|
+
return connections.filter(
|
|
187
|
+
(c) => c.index >= 0 && c.to !== target && descendants.get(c.to).has(target) && reached.some((from, i) => from.has(c.from) && reached.some((into, j) => j !== i && into.has(c.to) && !into.has(c.from)))
|
|
188
|
+
).sort((a, b) => compare(a.from, b.from) || compare(a.port, b.port) || compare(a.to, b.to) || compare(a.input, b.input))[0];
|
|
189
|
+
};
|
|
190
|
+
const decompose = (branches) => {
|
|
191
|
+
if (branches.length === 1) {
|
|
192
|
+
return { level: { kind: "leaf", connection: branches[0] } };
|
|
193
|
+
}
|
|
194
|
+
const n = nearestCommonDominator(branches.map((b) => b.from));
|
|
195
|
+
const via = branches.map((b) => outgoing.get(n).filter((c) => c === b || reaches(c, b.from)));
|
|
196
|
+
const portsOf = (members) => new Set(members.flatMap((i) => via[i].map((c) => c.port)));
|
|
197
|
+
const parent = branches.map((_, i) => i);
|
|
198
|
+
const find = (i) => parent[i] === i ? i : parent[i] = find(parent[i]);
|
|
199
|
+
for (let i = 0; i < branches.length; i++) {
|
|
200
|
+
for (let j = i + 1; j < branches.length; j++) {
|
|
201
|
+
if (via[i].some((c) => via[j].includes(c))) {
|
|
202
|
+
parent[find(j)] = find(i);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const groups = /* @__PURE__ */ new Map();
|
|
207
|
+
branches.forEach((_, i) => {
|
|
208
|
+
const root = find(i);
|
|
209
|
+
groups.set(root, [...groups.get(root) ?? [], i]);
|
|
210
|
+
});
|
|
211
|
+
if (groups.size === 1) {
|
|
212
|
+
const ports = portsOf(branches.map((_, i) => i));
|
|
213
|
+
if (ports.size > 1) {
|
|
214
|
+
return { problems: [bypass(branches)] };
|
|
215
|
+
}
|
|
216
|
+
const [port] = [...ports];
|
|
217
|
+
const crossed2 = crossing(n, port, branches[0].to);
|
|
218
|
+
return { problems: [{ rule: "merger-across-splitters", ...crossed2 ? { crossing: crossed2 } : {} }] };
|
|
219
|
+
}
|
|
220
|
+
const problems = [];
|
|
221
|
+
const byPort = /* @__PURE__ */ new Map();
|
|
222
|
+
for (const members of groups.values()) {
|
|
223
|
+
const ports = portsOf(members);
|
|
224
|
+
if (ports.size > 1) {
|
|
225
|
+
problems.push({ rule: "undecomposable-shape" });
|
|
226
|
+
}
|
|
227
|
+
const part = decompose(members.map((i) => branches[i]));
|
|
228
|
+
if ("problems" in part) {
|
|
229
|
+
problems.push(...part.problems);
|
|
230
|
+
} else if (ports.size === 1) {
|
|
231
|
+
const [port] = ports;
|
|
232
|
+
byPort.set(port, [...byPort.get(port) ?? [], part.level]);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (problems.length > 0) {
|
|
236
|
+
return { problems };
|
|
237
|
+
}
|
|
238
|
+
if (byPort.size === 1) {
|
|
239
|
+
const [[port, parts2]] = byPort;
|
|
240
|
+
return { level: { kind: "and", splitter: { nodeId: n, port }, parts: parts2 } };
|
|
241
|
+
}
|
|
242
|
+
const parts = sorted(byPort.keys()).map((port) => {
|
|
243
|
+
const onPort = byPort.get(port);
|
|
244
|
+
return onPort.length === 1 ? onPort[0] : { kind: "and", splitter: { nodeId: n, port }, parts: onPort };
|
|
245
|
+
});
|
|
246
|
+
return { level: { kind: "or", parts } };
|
|
247
|
+
};
|
|
248
|
+
const leaves = (level) => level.kind === "leaf" ? [level.connection.from] : level.parts.flatMap(leaves);
|
|
249
|
+
const mergers = [];
|
|
250
|
+
const mergerByInput = /* @__PURE__ */ new Map();
|
|
251
|
+
const converging = /* @__PURE__ */ new Set();
|
|
252
|
+
const virtualMergers = [];
|
|
253
|
+
const inputsOf = (nodeId) => sorted(new Set((incoming.get(nodeId) ?? []).filter((c) => c.index >= 0).map((c) => c.input)));
|
|
254
|
+
for (const nodeId of sorted(nodeIds)) {
|
|
255
|
+
const inputs = inputsOf(nodeId);
|
|
256
|
+
const named = (input) => inputs.length > 1 ? { inputName: input } : {};
|
|
257
|
+
for (const input of inputs) {
|
|
258
|
+
const branches = incoming.get(nodeId).filter((c) => c.index >= 0 && c.input === input);
|
|
259
|
+
if (branches.length < 2 || crossed.has(inputKey(nodeId, input))) {
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
const top = decompose(branches);
|
|
263
|
+
if ("problems" in top) {
|
|
264
|
+
const [chosen] = [...top.problems].sort(
|
|
265
|
+
(a, b) => PRECEDENCE.indexOf(a.rule) - PRECEDENCE.indexOf(b.rule) || compareOptionalPorts(a.splitter, b.splitter) || compare(a.branch ?? "", b.branch ?? "") || compare(crossingKey(a.crossing), crossingKey(b.crossing))
|
|
266
|
+
);
|
|
267
|
+
violations.push({
|
|
268
|
+
rule: chosen.rule,
|
|
269
|
+
nodeId,
|
|
270
|
+
...named(input),
|
|
271
|
+
...chosen.splitter ? { splitter: chosen.splitter } : {},
|
|
272
|
+
...chosen.branch !== void 0 ? { branch: chosen.branch } : {},
|
|
273
|
+
...chosen.crossing ? { edgeIndex: chosen.crossing.index } : {}
|
|
274
|
+
});
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
const level = top.level;
|
|
278
|
+
if (level.kind === "leaf") {
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
const mergerBranches2 = [];
|
|
282
|
+
const walk = (part, depth, splitter) => {
|
|
283
|
+
if (part.kind === "leaf") {
|
|
284
|
+
if (splitter !== null) {
|
|
285
|
+
mergerBranches2.push({ splitter, lastNodeId: part.connection.from });
|
|
286
|
+
}
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (depth > 0) {
|
|
290
|
+
virtualMergers.push({
|
|
291
|
+
input: nodeId,
|
|
292
|
+
...named(input),
|
|
293
|
+
level: depth,
|
|
294
|
+
kind: part.kind === "and" ? "merger" : "converging",
|
|
295
|
+
...part.kind === "and" ? { splitter: part.splitter } : {},
|
|
296
|
+
members: sorted(leaves(part))
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
const inner = part.kind === "and" ? part.splitter : splitter;
|
|
300
|
+
part.parts.forEach((p) => walk(p, depth + 1, inner));
|
|
301
|
+
};
|
|
302
|
+
walk(level, 0, null);
|
|
303
|
+
if (level.kind === "and") {
|
|
304
|
+
mergerBranches2.sort((a, b) => compare(a.lastNodeId, b.lastNodeId) || comparePorts(a.splitter, b.splitter));
|
|
305
|
+
mergers.push({ nodeId, ...named(input), branches: mergerBranches2 });
|
|
306
|
+
mergerByInput.set(inputKey(nodeId, input), mergerBranches2);
|
|
307
|
+
} else {
|
|
308
|
+
converging.add(nodeId);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
virtualMergers.sort(
|
|
313
|
+
(a, b) => compare(a.input, b.input) || compare(a.inputName ?? "", b.inputName ?? "") || a.level - b.level || compareLists(a.members, b.members)
|
|
314
|
+
);
|
|
315
|
+
const isSiblingReference = (fromNodeId, targetNodeId) => {
|
|
316
|
+
if (!nodeIds.has(fromNodeId) || !nodeIds.has(targetNodeId) || fromNodeId === targetNodeId) {
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
if (descendants.get(targetNodeId).has(fromNodeId) || descendants.get(fromNodeId).has(targetNodeId)) {
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
return splitters.some(({ nodeId, port }) => {
|
|
323
|
+
const onPort = outgoing.get(nodeId).filter((c) => c.port === port);
|
|
324
|
+
return onPort.some((c1) => reaches(c1, fromNodeId) && onPort.some((c2) => c2 !== c1 && reaches(c2, targetNodeId)));
|
|
325
|
+
});
|
|
326
|
+
};
|
|
327
|
+
for (const ref of opts.references ?? []) {
|
|
328
|
+
if (ref.root === "nodes" && isSiblingReference(ref.fromNodeId, ref.target)) {
|
|
329
|
+
violations.push({ rule: "sibling-reference", nodeId: ref.fromNodeId, target: ref.target });
|
|
330
|
+
}
|
|
331
|
+
if (ref.root === "in" && nodeIds.has(ref.fromNodeId)) {
|
|
332
|
+
const inputs = inputsOf(ref.fromNodeId);
|
|
333
|
+
const branches = inputs.length === 1 ? mergerByInput.get(inputKey(ref.fromNodeId, inputs[0])) : void 0;
|
|
334
|
+
if (branches && !branches.some((b) => b.lastNodeId === ref.target)) {
|
|
335
|
+
violations.push({ rule: "merger-in-reference", nodeId: ref.fromNodeId, target: ref.target });
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
violations.sort(
|
|
340
|
+
(a, b) => compare(a.rule, b.rule) || compare(a.nodeId, b.nodeId) || compare(a.inputName ?? "", b.inputName ?? "") || (a.edgeIndex ?? -1) - (b.edgeIndex ?? -1) || compare(a.target ?? "", b.target ?? "") || compare(a.branch ?? "", b.branch ?? "")
|
|
341
|
+
);
|
|
342
|
+
const mergerBranches = (nodeId, inputName) => {
|
|
343
|
+
const input = inputName ?? inputsOf(nodeId).find((name) => mergerByInput.has(inputKey(nodeId, name)));
|
|
344
|
+
return input === void 0 ? null : mergerByInput.get(inputKey(nodeId, input)) ?? null;
|
|
345
|
+
};
|
|
346
|
+
return withHelpers(
|
|
347
|
+
{ splitters, mergers, converging: sorted(converging), virtualMergers, loopBodies, violations },
|
|
348
|
+
mergerBranches,
|
|
349
|
+
isSiblingReference
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
function withHelpers(shapes, mergerBranches, isSiblingReference) {
|
|
353
|
+
return {
|
|
354
|
+
...shapes,
|
|
355
|
+
mergerBranches,
|
|
356
|
+
loopEndNode: (iteratorId) => shapes.loopBodies[iteratorId]?.endNode ?? null,
|
|
357
|
+
isSiblingReference
|
|
358
|
+
};
|
|
359
|
+
}
|
|
@@ -81,3 +81,15 @@ export declare function outputHandleId(port: string, side: Side): string;
|
|
|
81
81
|
* name to carry. A group frame's input is this on the top side.
|
|
82
82
|
*/
|
|
83
83
|
export declare function inputHandleId(side: Side): string;
|
|
84
|
+
/**
|
|
85
|
+
* The id of a virtual merger's dot: one level of a merge drawn in front of a node's
|
|
86
|
+
* input (PO-750). `n` counts the virtual mergers in front of that one input, from 1;
|
|
87
|
+
* two can stand on the same level, so the level alone would not tell them apart.
|
|
88
|
+
*
|
|
89
|
+
* It arrives at the same node as the input dot does, so it carries no port name
|
|
90
|
+
* either, and the separator stays at the front: `portFromHandleId` reads
|
|
91
|
+
* `__merge1__in__top` as "no port", exactly like `__in__top`. Nothing stores this
|
|
92
|
+
* id — the blob names only the node an edge arrives at — so the canvas may route a
|
|
93
|
+
* connection through it without the workflow ever knowing it exists.
|
|
94
|
+
*/
|
|
95
|
+
export declare function virtualMergerHandleId(n: number, side: Side): string;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Refusal } from './connectionRules.js';
|
|
2
|
+
import type { ShapeRefusal } from './canvasShapes.js';
|
|
3
|
+
/**
|
|
4
|
+
* What a toast says when the editor refuses a connection (PO-750).
|
|
5
|
+
*
|
|
6
|
+
* A toast is a small record, not a sentence — the design system's shape: a title
|
|
7
|
+
* saying what happened, and a description with the detail (`@revenexx/studio`,
|
|
8
|
+
* its Sonner examples). The detail names the steps involved, so the author does not
|
|
9
|
+
* have to work out which of the dots they were near was the one refused. No action:
|
|
10
|
+
* there is no next step the editor could take for them — the connection is either
|
|
11
|
+
* drawn elsewhere or not at all.
|
|
12
|
+
*
|
|
13
|
+
* Pure, and it returns message keys rather than text, so the choice of words is
|
|
14
|
+
* testable without the translator and the translator stays the editor's.
|
|
15
|
+
*/
|
|
16
|
+
export interface ToastText {
|
|
17
|
+
title: string;
|
|
18
|
+
description: string;
|
|
19
|
+
params: Record<string, string | number>;
|
|
20
|
+
}
|
|
21
|
+
/** The record for one refused connection; `label` names a step or a trigger by id. */
|
|
22
|
+
export declare function refusalText(refusal: Refusal, label: (id: string) => string): ToastText;
|
|
23
|
+
/** The record for copies that came without some of their connections. */
|
|
24
|
+
export declare function copiedRefusalText(count: number): ToastText;
|
|
25
|
+
/**
|
|
26
|
+
* The record for an input the save will refuse because of how branches arrive at
|
|
27
|
+
* it (decided 19 Oct 2026) — said as it is drawn, not refused: the shape may still
|
|
28
|
+
* be finished into a valid one, so the connection stays. Where the branches of a
|
|
29
|
+
* split cross, it names that connection, which is the one to change; `step` is the
|
|
30
|
+
* step the save will refuse, as its card names it.
|
|
31
|
+
*/
|
|
32
|
+
export declare function joinRefusalText(refusal: ShapeRefusal, step: string): ToastText;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const TITLE = "integration.editor.connection.refused.title";
|
|
2
|
+
export function refusalText(refusal, label) {
|
|
3
|
+
if (refusal.reason === "cycle") {
|
|
4
|
+
return refusal.from && refusal.to ? { title: TITLE, description: "integration.editor.connection.refused.cycle", params: { source: label(refusal.from), target: label(refusal.to) } } : { title: TITLE, description: "integration.editor.connection.refused.cycleUnnamed", params: {} };
|
|
5
|
+
}
|
|
6
|
+
if (refusal.direction === "item") {
|
|
7
|
+
return {
|
|
8
|
+
title: TITLE,
|
|
9
|
+
description: "integration.editor.connection.refused.loopBoundaryItem",
|
|
10
|
+
params: { outside: label(refusal.outside), loop: label(refusal.iterator) }
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
return refusal.direction === "out" ? {
|
|
14
|
+
title: TITLE,
|
|
15
|
+
description: "integration.editor.connection.refused.loopBoundaryOut",
|
|
16
|
+
params: { inside: label(refusal.inside), outside: label(refusal.outside), loop: label(refusal.iterator) }
|
|
17
|
+
} : {
|
|
18
|
+
title: TITLE,
|
|
19
|
+
description: "integration.editor.connection.refused.loopBoundary",
|
|
20
|
+
params: { inside: label(refusal.inside), loop: label(refusal.iterator) }
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export function copiedRefusalText(count) {
|
|
24
|
+
return {
|
|
25
|
+
title: "integration.editor.connection.refused.copiedTitle",
|
|
26
|
+
description: "integration.editor.connection.refused.copied",
|
|
27
|
+
params: { n: count }
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const JOIN_DESCRIPTION = {
|
|
31
|
+
"merger-across-splitters": "integration.editor.connection.joinRule.mergerAcrossSplitters",
|
|
32
|
+
"splitter-bypassed": "integration.editor.connection.joinRule.splitterBypassed",
|
|
33
|
+
"undecomposable-shape": "integration.editor.connection.joinRule.undecomposable"
|
|
34
|
+
};
|
|
35
|
+
export function joinRefusalText(refusal, step) {
|
|
36
|
+
const title = "integration.editor.connection.joinRule.title";
|
|
37
|
+
if (refusal.crossing) {
|
|
38
|
+
const { from, output, to } = refusal.crossing;
|
|
39
|
+
return { title, description: "integration.editor.connection.joinRule.crossing", params: { from, output, to, step } };
|
|
40
|
+
}
|
|
41
|
+
return { title, description: JOIN_DESCRIPTION[refusal.rule], params: { step } };
|
|
42
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { WorkflowEdge, WorkflowNode, WorkflowTrigger } from '../types/integrations.js';
|
|
2
|
+
/**
|
|
3
|
+
* What the save dialog says when the service refuses a workflow for its shape
|
|
4
|
+
* (PO-746).
|
|
5
|
+
*
|
|
6
|
+
* The service checks the shape of the graph on every save, active or paused, and
|
|
7
|
+
* reports it at the node or the connection itself — `blob.nodes.5`, `blob.edges.3`
|
|
8
|
+
* — where a node setting is reported under `blob.nodes.5.config.…`. Pausing the
|
|
9
|
+
* workflow does not get such a save through, so the dialog must not offer it.
|
|
10
|
+
*
|
|
11
|
+
* The service says it in English and by step id. The canvas runs the same
|
|
12
|
+
* analysis, so a refusal it recognises is said in the words the canvas warns
|
|
13
|
+
* with — the studio's language, the name each card carries, which connection or
|
|
14
|
+
* node, and what to do. Only a refusal the canvas does not know is passed on as
|
|
15
|
+
* the service wrote it.
|
|
16
|
+
*/
|
|
17
|
+
export interface SaveIssue {
|
|
18
|
+
path: string;
|
|
19
|
+
message: string;
|
|
20
|
+
}
|
|
21
|
+
/** One line of the dialog: a message key with its params, or the service's own text. */
|
|
22
|
+
export type SaveRefusalLine = {
|
|
23
|
+
key: string;
|
|
24
|
+
params: Record<string, string | number>;
|
|
25
|
+
} | {
|
|
26
|
+
where: string | null;
|
|
27
|
+
message: string;
|
|
28
|
+
};
|
|
29
|
+
/** Whether the service refused the graph itself, which no `active` flag changes. */
|
|
30
|
+
export declare function isShapeIssue(path: string): boolean;
|
|
31
|
+
export declare function saveRefusalLines(issues: readonly SaveIssue[], nodes: readonly WorkflowNode[], edges: readonly WorkflowEdge[], triggers?: readonly WorkflowTrigger[]): SaveRefusalLine[];
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { nodeDisplayLabel } from "../utils/validationFindings.js";
|
|
2
|
+
import { canvasShapes } from "./canvasShapes.js";
|
|
3
|
+
import { joinRefusalText } from "./refusalMessage.js";
|
|
4
|
+
export function isShapeIssue(path) {
|
|
5
|
+
return /^blob\.(nodes|edges)(\.\d+)?$/.test(path);
|
|
6
|
+
}
|
|
7
|
+
export function saveRefusalLines(issues, nodes, edges, triggers = []) {
|
|
8
|
+
const refusals = canvasShapes(nodes, edges, triggers).refusals;
|
|
9
|
+
const lines = [];
|
|
10
|
+
const said = /* @__PURE__ */ new Set();
|
|
11
|
+
for (const issue of issues) {
|
|
12
|
+
const index = /^blob\.nodes\.(\d+)\b/.exec(issue.path);
|
|
13
|
+
const node = index ? nodes[Number(index[1])] : void 0;
|
|
14
|
+
const refusal = node && isShapeIssue(issue.path) ? refusals.find((r) => r.nodeId === node.id) : void 0;
|
|
15
|
+
if (refusal) {
|
|
16
|
+
if (said.has(refusal.nodeId)) continue;
|
|
17
|
+
said.add(refusal.nodeId);
|
|
18
|
+
const text = joinRefusalText(refusal, nodeDisplayLabel(node));
|
|
19
|
+
lines.push({ key: text.description, params: text.params });
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
lines.push({ where: node ? nodeDisplayLabel(node) : null, message: issue.message });
|
|
23
|
+
}
|
|
24
|
+
return lines;
|
|
25
|
+
}
|
|
@@ -77,7 +77,13 @@
|
|
|
77
77
|
"integration.canvas.node.badge.loopBody": "Schleife — am Ausgang „Element“ hängt noch nichts, es wiederholt sich also nichts | Schleife — wiederholt den einen Schritt am Ausgang „Element“, einmal pro Element | Schleife — wiederholt die {n} Schritte am Ausgang „Element“, einmal pro Element",
|
|
78
78
|
"integration.canvas.node.badge.noBuiltCode": "Version {version} ist nicht einsatzbereit — dieser Schritt lässt sich nicht testen",
|
|
79
79
|
"integration.canvas.node.badge.repeats": "Wiederholt sich — läuft einmal pro Element von {iterators} | Wiederholt sich — läuft einmal pro Element jeder der {n} Schleifen, in denen er liegt: {iterators}",
|
|
80
|
+
"integration.canvas.node.handle.converging": "Zusammenführung: Der Zweig, der ankommt, wird als Eingabe übergeben",
|
|
81
|
+
"integration.canvas.node.handle.convergingKeyed": "Die Zweige eines Splitters kommen zusammen an, jeder unter dem Schritt, in dem er endet",
|
|
80
82
|
"integration.canvas.node.handle.input": "Eingang",
|
|
83
|
+
"integration.canvas.node.handle.merger": "Merger: wartet auf alle Zweige; jeder wird unter dem Schritt übergeben, in dem er endet",
|
|
84
|
+
"integration.canvas.node.handle.splitter": "Splitter: alle Zweige laufen",
|
|
85
|
+
"integration.canvas.node.handle.virtualConverging": "Zusammenführung, nur gezeichnet: der Zweig, der ankommt",
|
|
86
|
+
"integration.canvas.node.handle.virtualMerger": "Merger, nur gezeichnet: wartet auf alle Zweige",
|
|
81
87
|
"integration.canvas.node.meta.versionTitle": "Version {version}",
|
|
82
88
|
"integration.canvas.node.note.heading": "Notiz",
|
|
83
89
|
"integration.canvas.node.port.outputFallback": "Ausgang",
|
|
@@ -385,6 +391,14 @@
|
|
|
385
391
|
"integration.editor.clipboard.error.read": "Zwischenablage konnte nicht gelesen werden.",
|
|
386
392
|
"integration.editor.clipboard.error.write": "In die Zwischenablage konnte nicht geschrieben werden.",
|
|
387
393
|
"integration.editor.clipboard.toast.copied": "Node kopiert | {n} Nodes kopiert",
|
|
394
|
+
"integration.editor.connection.refused.copied": "{n} Verbindung zwischen den Kopien wurde weggelassen, weil sie die Grenze einer Schleife überqueren würde. | {n} Verbindungen zwischen den Kopien wurden weggelassen, weil sie die Grenze einer Schleife überqueren würden.",
|
|
395
|
+
"integration.editor.connection.refused.copiedTitle": "Ohne einige Verbindungen kopiert",
|
|
396
|
+
"integration.editor.connection.refused.cycle": "{source} läuft bereits nach {target} — diese Verbindung würde einen Kreis schließen.",
|
|
397
|
+
"integration.editor.connection.refused.cycleUnnamed": "Damit entstünde ein Kreis: Ein Schritt liefe nach sich selbst.",
|
|
398
|
+
"integration.editor.connection.refused.loopBoundary": "{inside} liegt in der Schleife {loop}; nur deren Ausgang „Element“ darf in ihre Schritte führen.",
|
|
399
|
+
"integration.editor.connection.refused.loopBoundaryItem": "{outside} wird auch von außerhalb der Schleife {loop} erreicht, deshalb darf deren Ausgang „Element“ nicht dorthin führen.",
|
|
400
|
+
"integration.editor.connection.refused.loopBoundaryOut": "{inside} liegt in der Schleife {loop}, und {outside} wird auch von außerhalb erreicht; nur deren Ausgang „Fertig“ darf aus der Schleife herausführen.",
|
|
401
|
+
"integration.editor.connection.refused.title": "Verbindung nicht möglich",
|
|
388
402
|
"integration.editor.deactivate.dialog.cancel": "Abbrechen",
|
|
389
403
|
"integration.editor.deactivate.dialog.confirm": "Als pausiert speichern",
|
|
390
404
|
"integration.editor.deactivate.dialog.description.paused": "pausiert",
|
|
@@ -581,14 +595,23 @@
|
|
|
581
595
|
"integration.nodeConfig.expression.picker.filterPlaceholder": "Filtern…",
|
|
582
596
|
"integration.nodeConfig.expression.pill.inputPath": "Input · {rest}",
|
|
583
597
|
"integration.nodeConfig.expression.pill.inputPayload": "Eingabe-Payload",
|
|
598
|
+
"integration.nodeConfig.expression.pill.runId": "Run-ID",
|
|
599
|
+
"integration.nodeConfig.expression.pill.runUrl": "Run-Link",
|
|
584
600
|
"integration.nodeConfig.expression.pill.triggerPath": "Trigger · {rest}",
|
|
585
601
|
"integration.nodeConfig.expression.pill.triggerPayload": "Trigger-Payload",
|
|
586
602
|
"integration.nodeConfig.expression.pill.variable": "Variable · {path}",
|
|
603
|
+
"integration.nodeConfig.expression.pill.workflowId": "Workflow-ID",
|
|
604
|
+
"integration.nodeConfig.expression.pill.workflowName": "Workflow-Name",
|
|
587
605
|
"integration.nodeConfig.expression.preview.missTitle": "Teile dieses Ausdrucks haben noch keine Beispieldaten",
|
|
588
606
|
"integration.nodeConfig.expression.suggestion.heading.input": "EINGABE",
|
|
589
607
|
"integration.nodeConfig.expression.suggestion.heading.nodeOutputs": "NODE-AUSGABEN",
|
|
608
|
+
"integration.nodeConfig.expression.suggestion.heading.run": "RUN",
|
|
590
609
|
"integration.nodeConfig.expression.suggestion.heading.variables": "VARIABLEN",
|
|
591
610
|
"integration.nodeConfig.expression.suggestion.inputPayload": "Eingabe-Payload",
|
|
611
|
+
"integration.nodeConfig.expression.suggestion.runId": "Run-ID",
|
|
612
|
+
"integration.nodeConfig.expression.suggestion.runUrl": "Run-Link",
|
|
613
|
+
"integration.nodeConfig.expression.suggestion.workflowId": "Workflow-ID",
|
|
614
|
+
"integration.nodeConfig.expression.suggestion.workflowName": "Workflow-Name",
|
|
592
615
|
"integration.nodeConfig.expression.toggle.off": "Zurück zum normalen Feld (löscht den Ausdruck)",
|
|
593
616
|
"integration.nodeConfig.expression.toggle.on": "Diesen Wert stattdessen aus einem vorherigen Schritt zuordnen",
|
|
594
617
|
"integration.nodeConfig.expression.useFit": "Übernehmen:",
|
|
@@ -990,10 +1013,14 @@
|
|
|
990
1013
|
"integration.runs.response.title.starting": "Run wird gestartet",
|
|
991
1014
|
"integration.runs.response.title.sync": "Run-Antwort",
|
|
992
1015
|
"integration.runs.response.title.unconfirmed": "Start nicht bestätigt",
|
|
1016
|
+
"integration.runs.skip.deadEnd": "Übersprungen: Dead End",
|
|
1017
|
+
"integration.runs.skip.deadEndBranch": "Übersprungen: Dead End in Zweig {branch}",
|
|
1018
|
+
"integration.runs.skip.plain": "Übersprungen",
|
|
993
1019
|
"integration.runs.status.cancelled": "abgebrochen",
|
|
994
1020
|
"integration.runs.status.failed": "fehlgeschlagen",
|
|
995
1021
|
"integration.runs.status.queued": "wartet",
|
|
996
1022
|
"integration.runs.status.running": "läuft",
|
|
1023
|
+
"integration.runs.status.skipped": "übersprungen",
|
|
997
1024
|
"integration.runs.status.succeeded": "erfolgreich",
|
|
998
1025
|
"integration.runs.table.column.duration": "Dauer",
|
|
999
1026
|
"integration.runs.table.column.id": "ID",
|
|
@@ -1393,5 +1420,13 @@
|
|
|
1393
1420
|
"integration.nodeVersions.badge.offered": "Angeboten",
|
|
1394
1421
|
"integration.nodeVersions.badge.notBuilt": "Nicht gebaut",
|
|
1395
1422
|
"integration.nodeVersions.action.retire": "Ausmustern",
|
|
1396
|
-
"integration.nodeVersions.action.restore": "Wiederherstellen"
|
|
1423
|
+
"integration.nodeVersions.action.restore": "Wiederherstellen",
|
|
1424
|
+
"integration.editor.connection.joinRule.title": "So nicht speicherbar",
|
|
1425
|
+
"integration.editor.connection.joinRule.crossing": "Die Verbindung vom Node „{from}“ (Ausgang {output}) zum Node „{to}“ verbindet zwei Wege, die getrennt laufen müssen. Löschen Sie diese Verbindung oder führen Sie sie zu einem Node im selben Weg.",
|
|
1426
|
+
"integration.editor.connection.joinRule.mergerAcrossSplitters": "Im Node „{step}“ kommen Wege zusammen, die sich an verschiedenen Stellen geteilt haben. Führen Sie zuerst die Wege jeder Teilung in einem eigenen Node zusammen.",
|
|
1427
|
+
"integration.editor.connection.joinRule.splitterBypassed": "Zum Node „{step}“ führen Wege, die sich vorher geteilt haben – und eine Verbindung führt zusätzlich an dieser Teilung vorbei. Löschen Sie diese zusätzliche Verbindung.",
|
|
1428
|
+
"integration.editor.connection.joinRule.undecomposable": "Im Node „{step}“ kommen Wege zusammen, die immer laufen, und Wege, die nur bei einer bestimmten Bedingung laufen. Führen Sie diese Wege in getrennten Nodes zusammen.",
|
|
1429
|
+
"integration.editor.unsavable.dialog.back": "Zurück zum Editor",
|
|
1430
|
+
"integration.editor.unsavable.dialog.description": "Dieser Workflow ist aktuell ungültig. Er kann so nicht gespeichert werden. Bitte beheben Sie zuerst diese Fehler:",
|
|
1431
|
+
"integration.editor.unsavable.dialog.title": "Workflow kann nicht gespeichert werden"
|
|
1397
1432
|
}
|