@octaviaflow/core 3.0.18-beta.8 → 3.0.18-beta.9
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/chunk-CEUP4NK2.js +2850 -0
- package/dist/chunk-CEUP4NK2.js.map +1 -0
- package/dist/chunk-EERNYLFL.js +2860 -0
- package/dist/chunk-EERNYLFL.js.map +1 -0
- package/dist/chunk-EKFDJX4G.js +2872 -0
- package/dist/chunk-EKFDJX4G.js.map +1 -0
- package/dist/chunk-GJA3GJUZ.js +2844 -0
- package/dist/chunk-GJA3GJUZ.js.map +1 -0
- package/dist/chunk-J7YASALS.js +2859 -0
- package/dist/chunk-J7YASALS.js.map +1 -0
- package/dist/chunk-JIEUYBQT.js +2658 -0
- package/dist/chunk-JIEUYBQT.js.map +1 -0
- package/dist/chunk-S2SSBMWJ.js +2658 -0
- package/dist/chunk-S2SSBMWJ.js.map +1 -0
- package/dist/chunk-WEPTBLWX.js +2847 -0
- package/dist/chunk-WEPTBLWX.js.map +1 -0
- package/dist/chunk-WG4ZQMPS.js +2844 -0
- package/dist/chunk-WG4ZQMPS.js.map +1 -0
- package/dist/chunk-XEPEBHAW.js +2808 -0
- package/dist/chunk-XEPEBHAW.js.map +1 -0
- package/dist/chunk-XG2OYFX6.js +2925 -0
- package/dist/chunk-XG2OYFX6.js.map +1 -0
- package/dist/components/WorkflowHeader/WorkflowHeader.d.ts.map +1 -1
- package/dist/index.cjs +736 -441
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +6 -5
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/workflow/components/FlowCanvas/FlowCanvas.d.ts +27 -0
- package/dist/workflow/components/FlowCanvas/FlowCanvas.d.ts.map +1 -1
- package/dist/workflow/components/kinds/index.d.ts +4 -0
- package/dist/workflow/components/kinds/index.d.ts.map +1 -1
- package/dist/workflow.cjs +433 -312
- package/dist/workflow.cjs.map +1 -1
- package/dist/workflow.js +5 -149
- package/dist/workflow.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,2844 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cn
|
|
3
|
+
} from "./chunk-ZAUUGK2Y.js";
|
|
4
|
+
|
|
5
|
+
// src/workflow/utils/paths.ts
|
|
6
|
+
function buildEdgePath(routing, start, startSide, end, endSide, options = {}) {
|
|
7
|
+
switch (routing) {
|
|
8
|
+
case "step":
|
|
9
|
+
return stepPath(start, startSide, end, endSide);
|
|
10
|
+
case "smoothstep":
|
|
11
|
+
return smoothStepPath(start, startSide, end, endSide, options.borderRadius ?? 8);
|
|
12
|
+
case "straight":
|
|
13
|
+
return straightPath(start, end);
|
|
14
|
+
default:
|
|
15
|
+
return bezierPath(start, startSide, end, endSide, options.curvature ?? 0.25);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function bezierPath(start, startSide, end, endSide, curvature = 0.25) {
|
|
19
|
+
const sourceOffset = calcControlOffset(start, startSide, end, curvature);
|
|
20
|
+
const targetOffset = calcControlOffset(end, endSide, start, curvature);
|
|
21
|
+
const c1 = offsetAlongSide(start, startSide, sourceOffset);
|
|
22
|
+
const c2 = offsetAlongSide(end, endSide, targetOffset);
|
|
23
|
+
const d = `M ${start.x},${start.y} C ${c1.x},${c1.y} ${c2.x},${c2.y} ${end.x},${end.y}`;
|
|
24
|
+
const midX = 0.125 * start.x + 0.375 * c1.x + 0.375 * c2.x + 0.125 * end.x;
|
|
25
|
+
const midY = 0.125 * start.y + 0.375 * c1.y + 0.375 * c2.y + 0.125 * end.y;
|
|
26
|
+
const tx = 3 * ((1 - 0.5) ** 2 * (c1.x - start.x) + 2 * (1 - 0.5) * 0.5 * (c2.x - c1.x) + 0.5 ** 2 * (end.x - c2.x));
|
|
27
|
+
const ty = 3 * ((1 - 0.5) ** 2 * (c1.y - start.y) + 2 * (1 - 0.5) * 0.5 * (c2.y - c1.y) + 0.5 ** 2 * (end.y - c2.y));
|
|
28
|
+
const midAngle = Math.atan2(ty, tx);
|
|
29
|
+
return { d, midX, midY, midAngle };
|
|
30
|
+
}
|
|
31
|
+
function axisDistance(from, side, to) {
|
|
32
|
+
switch (side) {
|
|
33
|
+
case "top":
|
|
34
|
+
return from.y - to.y;
|
|
35
|
+
case "bottom":
|
|
36
|
+
return to.y - from.y;
|
|
37
|
+
case "left":
|
|
38
|
+
return from.x - to.x;
|
|
39
|
+
case "right":
|
|
40
|
+
return to.x - from.x;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function calcControlOffset(from, side, to, curvature) {
|
|
44
|
+
const distance2 = axisDistance(from, side, to);
|
|
45
|
+
if (distance2 >= 0) {
|
|
46
|
+
return distance2 * 0.5 * (1 + curvature);
|
|
47
|
+
}
|
|
48
|
+
return curvature * 25 * Math.sqrt(-distance2);
|
|
49
|
+
}
|
|
50
|
+
function offsetAlongSide(p, side, magnitude) {
|
|
51
|
+
switch (side) {
|
|
52
|
+
case "top":
|
|
53
|
+
return { x: p.x, y: p.y - magnitude };
|
|
54
|
+
case "bottom":
|
|
55
|
+
return { x: p.x, y: p.y + magnitude };
|
|
56
|
+
case "left":
|
|
57
|
+
return { x: p.x - magnitude, y: p.y };
|
|
58
|
+
case "right":
|
|
59
|
+
return { x: p.x + magnitude, y: p.y };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function stepPath(start, startSide, end, endSide) {
|
|
63
|
+
const corners = computeStepCorners(start, startSide, end, endSide);
|
|
64
|
+
const pts = [start, ...corners, end];
|
|
65
|
+
const d = pts.map((p, i) => `${i === 0 ? "M" : "L"} ${p.x},${p.y}`).join(" ");
|
|
66
|
+
const { midX, midY, midAngle } = midpointOnPolyline(pts);
|
|
67
|
+
return { d, midX, midY, midAngle };
|
|
68
|
+
}
|
|
69
|
+
function smoothStepPath(start, startSide, end, endSide, borderRadius = 8) {
|
|
70
|
+
const corners = computeStepCorners(start, startSide, end, endSide);
|
|
71
|
+
const pts = [start, ...corners, end];
|
|
72
|
+
if (corners.length === 0) {
|
|
73
|
+
return straightPath(start, end);
|
|
74
|
+
}
|
|
75
|
+
const segments = [`M ${pts[0].x},${pts[0].y}`];
|
|
76
|
+
for (let i = 1; i < pts.length - 1; i++) {
|
|
77
|
+
const prev = pts[i - 1];
|
|
78
|
+
const here = pts[i];
|
|
79
|
+
const next = pts[i + 1];
|
|
80
|
+
const r = Math.min(borderRadius, distance(prev, here) / 2, distance(here, next) / 2);
|
|
81
|
+
const enter = pointTowards(here, prev, r);
|
|
82
|
+
const exit = pointTowards(here, next, r);
|
|
83
|
+
segments.push(`L ${enter.x},${enter.y}`);
|
|
84
|
+
segments.push(`Q ${here.x},${here.y} ${exit.x},${exit.y}`);
|
|
85
|
+
}
|
|
86
|
+
const last = pts[pts.length - 1];
|
|
87
|
+
segments.push(`L ${last.x},${last.y}`);
|
|
88
|
+
const d = segments.join(" ");
|
|
89
|
+
const { midX, midY, midAngle } = midpointOnPolyline(pts);
|
|
90
|
+
return { d, midX, midY, midAngle };
|
|
91
|
+
}
|
|
92
|
+
function straightPath(start, end) {
|
|
93
|
+
const d = `M ${start.x},${start.y} L ${end.x},${end.y}`;
|
|
94
|
+
return {
|
|
95
|
+
d,
|
|
96
|
+
midX: (start.x + end.x) / 2,
|
|
97
|
+
midY: (start.y + end.y) / 2,
|
|
98
|
+
midAngle: Math.atan2(end.y - start.y, end.x - start.x)
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function computeStepCorners(start, startSide, end, endSide) {
|
|
102
|
+
const sourceVertical = startSide === "top" || startSide === "bottom";
|
|
103
|
+
const targetVertical = endSide === "top" || endSide === "bottom";
|
|
104
|
+
if (sourceVertical && targetVertical) {
|
|
105
|
+
const midY = (start.y + end.y) / 2;
|
|
106
|
+
return [
|
|
107
|
+
{ x: start.x, y: midY },
|
|
108
|
+
{ x: end.x, y: midY }
|
|
109
|
+
];
|
|
110
|
+
}
|
|
111
|
+
if (!sourceVertical && !targetVertical) {
|
|
112
|
+
const midX = (start.x + end.x) / 2;
|
|
113
|
+
return [
|
|
114
|
+
{ x: midX, y: start.y },
|
|
115
|
+
{ x: midX, y: end.y }
|
|
116
|
+
];
|
|
117
|
+
}
|
|
118
|
+
if (sourceVertical) {
|
|
119
|
+
return [{ x: end.x, y: start.y }];
|
|
120
|
+
}
|
|
121
|
+
return [{ x: start.x, y: end.y }];
|
|
122
|
+
}
|
|
123
|
+
function midpointOnPolyline(pts) {
|
|
124
|
+
if (pts.length === 2) {
|
|
125
|
+
return {
|
|
126
|
+
midX: (pts[0].x + pts[1].x) / 2,
|
|
127
|
+
midY: (pts[0].y + pts[1].y) / 2,
|
|
128
|
+
midAngle: Math.atan2(pts[1].y - pts[0].y, pts[1].x - pts[0].x)
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
let totalLen = 0;
|
|
132
|
+
const lens = [];
|
|
133
|
+
for (let i = 0; i < pts.length - 1; i++) {
|
|
134
|
+
const l = distance(pts[i], pts[i + 1]);
|
|
135
|
+
lens.push(l);
|
|
136
|
+
totalLen += l;
|
|
137
|
+
}
|
|
138
|
+
const half = totalLen / 2;
|
|
139
|
+
let acc = 0;
|
|
140
|
+
for (let i = 0; i < lens.length; i++) {
|
|
141
|
+
if (acc + lens[i] >= half) {
|
|
142
|
+
const seg = lens[i];
|
|
143
|
+
const remaining = half - acc;
|
|
144
|
+
const t = seg === 0 ? 0 : remaining / seg;
|
|
145
|
+
const a = pts[i];
|
|
146
|
+
const b = pts[i + 1];
|
|
147
|
+
return {
|
|
148
|
+
midX: a.x + (b.x - a.x) * t,
|
|
149
|
+
midY: a.y + (b.y - a.y) * t,
|
|
150
|
+
midAngle: Math.atan2(b.y - a.y, b.x - a.x)
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
acc += lens[i];
|
|
154
|
+
}
|
|
155
|
+
return { midX: pts[0].x, midY: pts[0].y, midAngle: 0 };
|
|
156
|
+
}
|
|
157
|
+
function distance(a, b) {
|
|
158
|
+
return Math.hypot(b.x - a.x, b.y - a.y);
|
|
159
|
+
}
|
|
160
|
+
function pointTowards(from, to, dist) {
|
|
161
|
+
const d = distance(from, to);
|
|
162
|
+
if (d === 0) return from;
|
|
163
|
+
const t = dist / d;
|
|
164
|
+
return { x: from.x + (to.x - from.x) * t, y: from.y + (to.y - from.y) * t };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// src/workflow/hooks/useFlow.ts
|
|
168
|
+
import { createContext, useContext } from "react";
|
|
169
|
+
var FlowInstanceContext = createContext(null);
|
|
170
|
+
function useFlow() {
|
|
171
|
+
const instance = useContext(FlowInstanceContext);
|
|
172
|
+
if (!instance) {
|
|
173
|
+
throw new Error("[@octaviaflow/core/workflow] useFlow() must be called inside <FlowCanvas>.");
|
|
174
|
+
}
|
|
175
|
+
return instance;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// src/workflow/store/selectors.ts
|
|
179
|
+
import { useContext as useContext3, useMemo, useSyncExternalStore } from "react";
|
|
180
|
+
|
|
181
|
+
// src/workflow/store/context.ts
|
|
182
|
+
import { createContext as createContext2, useContext as useContext2 } from "react";
|
|
183
|
+
var FlowStoreContext = createContext2(null);
|
|
184
|
+
function useFlowStore() {
|
|
185
|
+
const store = useContext2(FlowStoreContext);
|
|
186
|
+
if (!store) {
|
|
187
|
+
throw new Error(
|
|
188
|
+
"[@octaviaflow/core/workflow] useFlowStore must be called inside a <FlowCanvas>."
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
return store;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// src/workflow/store/selectors.ts
|
|
195
|
+
function useFlowSelector(selector, isEqual = Object.is) {
|
|
196
|
+
void isEqual;
|
|
197
|
+
const store = useFlowStore();
|
|
198
|
+
return useSyncExternalStore(
|
|
199
|
+
store.subscribe,
|
|
200
|
+
() => selector(store.getSnapshot()),
|
|
201
|
+
() => selector(store.getSnapshot())
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
function useNodes() {
|
|
205
|
+
return useFlowSelector((s) => s.nodes);
|
|
206
|
+
}
|
|
207
|
+
function useEdges() {
|
|
208
|
+
return useFlowSelector((s) => s.edges);
|
|
209
|
+
}
|
|
210
|
+
function useViewport() {
|
|
211
|
+
return useFlowSelector((s) => s.viewport);
|
|
212
|
+
}
|
|
213
|
+
var VIEWPORT_OR_NULL_NO_STORE_SUBSCRIBE = (_cb) => () => {
|
|
214
|
+
};
|
|
215
|
+
var VIEWPORT_OR_NULL_NO_STORE_SNAPSHOT = () => null;
|
|
216
|
+
function useViewportOrNull() {
|
|
217
|
+
const store = useContext3(FlowStoreContext);
|
|
218
|
+
const { sub, snap } = useMemo(
|
|
219
|
+
() => store ? {
|
|
220
|
+
sub: store.subscribe,
|
|
221
|
+
snap: () => store.getSnapshot().viewport
|
|
222
|
+
} : {
|
|
223
|
+
sub: VIEWPORT_OR_NULL_NO_STORE_SUBSCRIBE,
|
|
224
|
+
snap: VIEWPORT_OR_NULL_NO_STORE_SNAPSHOT
|
|
225
|
+
},
|
|
226
|
+
[store]
|
|
227
|
+
);
|
|
228
|
+
return useSyncExternalStore(sub, snap, snap);
|
|
229
|
+
}
|
|
230
|
+
function useNodeById(id) {
|
|
231
|
+
return useFlowSelector((s) => s.nodes.find((n) => n.id === id));
|
|
232
|
+
}
|
|
233
|
+
function useNodeData(id) {
|
|
234
|
+
return useFlowSelector(
|
|
235
|
+
(s) => s.nodes.find((n) => n.id === id)?.data ?? void 0
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
function useEdgeById(id) {
|
|
239
|
+
return useFlowSelector((s) => s.edges.find((e) => e.id === id));
|
|
240
|
+
}
|
|
241
|
+
function useIsNodeSelected(id) {
|
|
242
|
+
return useFlowSelector((s) => s.selectedNodeIds.has(id));
|
|
243
|
+
}
|
|
244
|
+
function useIsEdgeSelected(id) {
|
|
245
|
+
return useFlowSelector((s) => s.selectedEdgeIds.has(id));
|
|
246
|
+
}
|
|
247
|
+
function useConnection() {
|
|
248
|
+
return useFlowSelector((s) => s.connection);
|
|
249
|
+
}
|
|
250
|
+
function useSelection() {
|
|
251
|
+
const nodes = useNodes();
|
|
252
|
+
const edges = useEdges();
|
|
253
|
+
const selectedNodeIds = useFlowSelector((s) => s.selectedNodeIds);
|
|
254
|
+
const selectedEdgeIds = useFlowSelector((s) => s.selectedEdgeIds);
|
|
255
|
+
return useMemo(
|
|
256
|
+
() => ({
|
|
257
|
+
nodes: nodes.filter((n) => selectedNodeIds.has(n.id)),
|
|
258
|
+
edges: edges.filter((e) => selectedEdgeIds.has(e.id))
|
|
259
|
+
}),
|
|
260
|
+
[nodes, edges, selectedNodeIds, selectedEdgeIds]
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// src/workflow/store/changes.ts
|
|
265
|
+
function applyNodeChanges(nodes, changes) {
|
|
266
|
+
if (changes.length === 0) return nodes;
|
|
267
|
+
const byId = /* @__PURE__ */ new Map();
|
|
268
|
+
const adds = [];
|
|
269
|
+
const removes = /* @__PURE__ */ new Set();
|
|
270
|
+
let touched = false;
|
|
271
|
+
for (const change2 of changes) {
|
|
272
|
+
switch (change2.type) {
|
|
273
|
+
case "add":
|
|
274
|
+
adds.push(change2.item);
|
|
275
|
+
touched = true;
|
|
276
|
+
break;
|
|
277
|
+
case "remove":
|
|
278
|
+
removes.add(change2.id);
|
|
279
|
+
touched = true;
|
|
280
|
+
break;
|
|
281
|
+
default: {
|
|
282
|
+
const id = change2.id;
|
|
283
|
+
const arr = byId.get(id);
|
|
284
|
+
if (arr) arr.push(change2);
|
|
285
|
+
else byId.set(id, [change2]);
|
|
286
|
+
touched = true;
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (!touched) return nodes;
|
|
292
|
+
const next = [];
|
|
293
|
+
for (const node of nodes) {
|
|
294
|
+
if (removes.has(node.id)) continue;
|
|
295
|
+
const patches = byId.get(node.id);
|
|
296
|
+
if (!patches) {
|
|
297
|
+
next.push(node);
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
let n = node;
|
|
301
|
+
for (const patch of patches) {
|
|
302
|
+
switch (patch.type) {
|
|
303
|
+
case "position":
|
|
304
|
+
n = {
|
|
305
|
+
...n,
|
|
306
|
+
position: patch.position,
|
|
307
|
+
dragging: patch.dragging
|
|
308
|
+
};
|
|
309
|
+
break;
|
|
310
|
+
case "dimensions":
|
|
311
|
+
n = {
|
|
312
|
+
...n,
|
|
313
|
+
width: patch.dimensions.width,
|
|
314
|
+
height: patch.dimensions.height
|
|
315
|
+
};
|
|
316
|
+
break;
|
|
317
|
+
case "select":
|
|
318
|
+
if (n.selected === patch.selected) break;
|
|
319
|
+
n = { ...n, selected: patch.selected };
|
|
320
|
+
break;
|
|
321
|
+
case "replace":
|
|
322
|
+
n = patch.item;
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
next.push(n);
|
|
327
|
+
}
|
|
328
|
+
for (const add of adds) next.push(add);
|
|
329
|
+
return next;
|
|
330
|
+
}
|
|
331
|
+
function applyEdgeChanges(edges, changes) {
|
|
332
|
+
if (changes.length === 0) return edges;
|
|
333
|
+
const byId = /* @__PURE__ */ new Map();
|
|
334
|
+
const adds = [];
|
|
335
|
+
const removes = /* @__PURE__ */ new Set();
|
|
336
|
+
let touched = false;
|
|
337
|
+
for (const change2 of changes) {
|
|
338
|
+
switch (change2.type) {
|
|
339
|
+
case "add":
|
|
340
|
+
adds.push(change2.item);
|
|
341
|
+
touched = true;
|
|
342
|
+
break;
|
|
343
|
+
case "remove":
|
|
344
|
+
removes.add(change2.id);
|
|
345
|
+
touched = true;
|
|
346
|
+
break;
|
|
347
|
+
default: {
|
|
348
|
+
const arr = byId.get(change2.id);
|
|
349
|
+
if (arr) arr.push(change2);
|
|
350
|
+
else byId.set(change2.id, [change2]);
|
|
351
|
+
touched = true;
|
|
352
|
+
break;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (!touched) return edges;
|
|
357
|
+
const next = [];
|
|
358
|
+
for (const edge of edges) {
|
|
359
|
+
if (removes.has(edge.id)) continue;
|
|
360
|
+
const patches = byId.get(edge.id);
|
|
361
|
+
if (!patches) {
|
|
362
|
+
next.push(edge);
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
let e = edge;
|
|
366
|
+
for (const patch of patches) {
|
|
367
|
+
switch (patch.type) {
|
|
368
|
+
case "select":
|
|
369
|
+
if (e.selected === patch.selected) break;
|
|
370
|
+
e = { ...e, selected: patch.selected };
|
|
371
|
+
break;
|
|
372
|
+
case "replace":
|
|
373
|
+
e = patch.item;
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
next.push(e);
|
|
378
|
+
}
|
|
379
|
+
for (const add of adds) next.push(add);
|
|
380
|
+
return next;
|
|
381
|
+
}
|
|
382
|
+
var change = {
|
|
383
|
+
node: {
|
|
384
|
+
add(item) {
|
|
385
|
+
return { type: "add", item };
|
|
386
|
+
},
|
|
387
|
+
remove(id) {
|
|
388
|
+
return { type: "remove", id };
|
|
389
|
+
},
|
|
390
|
+
position(id, position, dragging) {
|
|
391
|
+
return { type: "position", id, position, dragging };
|
|
392
|
+
},
|
|
393
|
+
dimensions(id, dimensions) {
|
|
394
|
+
return { type: "dimensions", id, dimensions };
|
|
395
|
+
},
|
|
396
|
+
select(id, selected) {
|
|
397
|
+
return { type: "select", id, selected };
|
|
398
|
+
},
|
|
399
|
+
replace(id, item) {
|
|
400
|
+
return { type: "replace", id, item };
|
|
401
|
+
}
|
|
402
|
+
},
|
|
403
|
+
edge: {
|
|
404
|
+
add(item) {
|
|
405
|
+
return { type: "add", item };
|
|
406
|
+
},
|
|
407
|
+
remove(id) {
|
|
408
|
+
return { type: "remove", id };
|
|
409
|
+
},
|
|
410
|
+
select(id, selected) {
|
|
411
|
+
return { type: "select", id, selected };
|
|
412
|
+
},
|
|
413
|
+
replace(id, item) {
|
|
414
|
+
return { type: "replace", id, item };
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
// src/workflow/utils/geometry.ts
|
|
420
|
+
var DEFAULT_NODE_WIDTH = 368;
|
|
421
|
+
var DEFAULT_NODE_HEIGHT = 96;
|
|
422
|
+
var COLLAPSED_GROUP_HEIGHT = 36;
|
|
423
|
+
var COLLAPSED_FOREACH_HEIGHT = 40;
|
|
424
|
+
function effectiveHeight(node) {
|
|
425
|
+
const data = node.data;
|
|
426
|
+
if (data?.collapsed) {
|
|
427
|
+
if (node.type === "group") return COLLAPSED_GROUP_HEIGHT;
|
|
428
|
+
if (node.type === "forEach") return COLLAPSED_FOREACH_HEIGHT;
|
|
429
|
+
}
|
|
430
|
+
return node.height ?? DEFAULT_NODE_HEIGHT;
|
|
431
|
+
}
|
|
432
|
+
function handleCentre(node, side, index, total) {
|
|
433
|
+
const w = node.width ?? DEFAULT_NODE_WIDTH;
|
|
434
|
+
const h = effectiveHeight(node);
|
|
435
|
+
const x0 = node.position.x;
|
|
436
|
+
const y0 = node.position.y;
|
|
437
|
+
const denom = total + 1;
|
|
438
|
+
const ratio = (index + 1) / denom;
|
|
439
|
+
switch (side) {
|
|
440
|
+
case "top":
|
|
441
|
+
return { x: x0 + w * ratio, y: y0 };
|
|
442
|
+
case "bottom":
|
|
443
|
+
return { x: x0 + w * ratio, y: y0 + h };
|
|
444
|
+
case "left":
|
|
445
|
+
return { x: x0, y: y0 + h * ratio };
|
|
446
|
+
case "right":
|
|
447
|
+
return { x: x0 + w, y: y0 + h * ratio };
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
function bezierPath2(start, startSide, end, endSide) {
|
|
451
|
+
const distance2 = Math.hypot(end.x - start.x, end.y - start.y);
|
|
452
|
+
const magnitude = Math.max(40, Math.min(160, distance2 * 0.45));
|
|
453
|
+
const c1 = offsetBySide(start, startSide, magnitude);
|
|
454
|
+
const c2 = offsetBySide(end, endSide, magnitude);
|
|
455
|
+
const d = `M ${start.x},${start.y} C ${c1.x},${c1.y} ${c2.x},${c2.y} ${end.x},${end.y}`;
|
|
456
|
+
const midX = 0.125 * start.x + 0.375 * c1.x + 0.375 * c2.x + 0.125 * end.x;
|
|
457
|
+
const midY = 0.125 * start.y + 0.375 * c1.y + 0.375 * c2.y + 0.125 * end.y;
|
|
458
|
+
return { d, midX, midY };
|
|
459
|
+
}
|
|
460
|
+
function offsetBySide(p, side, m) {
|
|
461
|
+
switch (side) {
|
|
462
|
+
case "top":
|
|
463
|
+
return { x: p.x, y: p.y - m };
|
|
464
|
+
case "bottom":
|
|
465
|
+
return { x: p.x, y: p.y + m };
|
|
466
|
+
case "left":
|
|
467
|
+
return { x: p.x - m, y: p.y };
|
|
468
|
+
case "right":
|
|
469
|
+
return { x: p.x + m, y: p.y };
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
function screenToFlow(p, vp) {
|
|
473
|
+
return {
|
|
474
|
+
x: (p.x - vp.x) / vp.zoom,
|
|
475
|
+
y: (p.y - vp.y) / vp.zoom
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
function flowToScreen(p, vp) {
|
|
479
|
+
return {
|
|
480
|
+
x: p.x * vp.zoom + vp.x,
|
|
481
|
+
y: p.y * vp.zoom + vp.y
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// src/workflow/utils/parenting.ts
|
|
486
|
+
function descendantsOf(nodeId, nodes) {
|
|
487
|
+
const childrenByParent = /* @__PURE__ */ new Map();
|
|
488
|
+
for (const n of nodes) {
|
|
489
|
+
if (!n.parentId) continue;
|
|
490
|
+
const arr = childrenByParent.get(n.parentId);
|
|
491
|
+
if (arr) arr.push(n);
|
|
492
|
+
else childrenByParent.set(n.parentId, [n]);
|
|
493
|
+
}
|
|
494
|
+
const out = [];
|
|
495
|
+
const stack = [nodeId];
|
|
496
|
+
const visited = /* @__PURE__ */ new Set([nodeId]);
|
|
497
|
+
while (stack.length) {
|
|
498
|
+
const cur = stack.pop();
|
|
499
|
+
if (cur === void 0) break;
|
|
500
|
+
const list = childrenByParent.get(cur);
|
|
501
|
+
if (!list) continue;
|
|
502
|
+
for (const c of list) {
|
|
503
|
+
if (visited.has(c.id)) continue;
|
|
504
|
+
visited.add(c.id);
|
|
505
|
+
out.push(c);
|
|
506
|
+
stack.push(c.id);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return out;
|
|
510
|
+
}
|
|
511
|
+
function findAncestor(node, nodes, predicate) {
|
|
512
|
+
let cursor = node;
|
|
513
|
+
const seen = /* @__PURE__ */ new Set();
|
|
514
|
+
while (cursor?.parentId) {
|
|
515
|
+
if (seen.has(cursor.parentId)) return void 0;
|
|
516
|
+
seen.add(cursor.parentId);
|
|
517
|
+
const parent = nodes.find((n) => n.id === cursor.parentId);
|
|
518
|
+
if (!parent) return void 0;
|
|
519
|
+
if (predicate(parent)) return parent;
|
|
520
|
+
cursor = parent;
|
|
521
|
+
}
|
|
522
|
+
return void 0;
|
|
523
|
+
}
|
|
524
|
+
function clampToParentExtent(node, proposed, nodes) {
|
|
525
|
+
if (node.extent !== "parent" || !node.parentId) return proposed;
|
|
526
|
+
const parent = nodes.find((n) => n.id === node.parentId);
|
|
527
|
+
if (!parent) return proposed;
|
|
528
|
+
const pw = parent.width ?? DEFAULT_NODE_WIDTH;
|
|
529
|
+
const ph = effectiveHeight(parent);
|
|
530
|
+
const w = node.width ?? DEFAULT_NODE_WIDTH;
|
|
531
|
+
const h = effectiveHeight(node);
|
|
532
|
+
const minX = parent.position.x;
|
|
533
|
+
const minY = parent.position.y;
|
|
534
|
+
const maxX = parent.position.x + pw - w;
|
|
535
|
+
const maxY = parent.position.y + ph - h;
|
|
536
|
+
return {
|
|
537
|
+
x: Math.max(minX, Math.min(maxX, proposed.x)),
|
|
538
|
+
y: Math.max(minY, Math.min(maxY, proposed.y))
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
function findContainingGroup(point, nodes, exclude = []) {
|
|
542
|
+
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
543
|
+
const n = nodes[i];
|
|
544
|
+
if (exclude.includes(n.id)) continue;
|
|
545
|
+
if (n.type !== "group") continue;
|
|
546
|
+
if (n.data && typeof n.data === "object" && n.data.collapsed) {
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
const w = n.width ?? DEFAULT_NODE_WIDTH;
|
|
550
|
+
const h = effectiveHeight(n);
|
|
551
|
+
if (point.x >= n.position.x && point.y >= n.position.y && point.x <= n.position.x + w && point.y <= n.position.y + h) {
|
|
552
|
+
return n;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
return void 0;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// src/workflow/components/FlowEdge/FlowEdge.tsx
|
|
559
|
+
import {
|
|
560
|
+
memo,
|
|
561
|
+
useEffect,
|
|
562
|
+
useId,
|
|
563
|
+
useRef,
|
|
564
|
+
useState
|
|
565
|
+
} from "react";
|
|
566
|
+
|
|
567
|
+
// src/workflow/components/Handle/handleRegistry.ts
|
|
568
|
+
import { createContext as createContext3, useContext as useContext4 } from "react";
|
|
569
|
+
var HandleRegistryContext = createContext3(null);
|
|
570
|
+
function useHandleRegistry() {
|
|
571
|
+
const r = useContext4(HandleRegistryContext);
|
|
572
|
+
if (!r) {
|
|
573
|
+
throw new Error("[@octaviaflow/core/workflow] Handle must be used inside <FlowCanvas>.");
|
|
574
|
+
}
|
|
575
|
+
return r;
|
|
576
|
+
}
|
|
577
|
+
function createHandleRegistry() {
|
|
578
|
+
const map = /* @__PURE__ */ new Map();
|
|
579
|
+
const key = (n, t, h) => `${n}::${t}::${h}`;
|
|
580
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
581
|
+
const notify = () => {
|
|
582
|
+
for (const l of listeners) l();
|
|
583
|
+
};
|
|
584
|
+
return {
|
|
585
|
+
register(d) {
|
|
586
|
+
map.set(key(d.nodeId, d.type, d.handleId), d);
|
|
587
|
+
notify();
|
|
588
|
+
return () => {
|
|
589
|
+
const k = key(d.nodeId, d.type, d.handleId);
|
|
590
|
+
if (map.get(k) === d) {
|
|
591
|
+
map.delete(k);
|
|
592
|
+
notify();
|
|
593
|
+
}
|
|
594
|
+
};
|
|
595
|
+
},
|
|
596
|
+
resolve(nodeId, type, handleId) {
|
|
597
|
+
return map.get(key(nodeId, type, handleId));
|
|
598
|
+
},
|
|
599
|
+
subscribe(listener) {
|
|
600
|
+
listeners.add(listener);
|
|
601
|
+
return () => {
|
|
602
|
+
listeners.delete(listener);
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// src/workflow/components/FlowEdge/FlowEdge.tsx
|
|
609
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
610
|
+
function FlowEdgeImpl({
|
|
611
|
+
edge,
|
|
612
|
+
nodes,
|
|
613
|
+
onSelect,
|
|
614
|
+
onDelete,
|
|
615
|
+
onLabelChange,
|
|
616
|
+
showDelete = true,
|
|
617
|
+
curvature = 0.25,
|
|
618
|
+
borderRadius = 8
|
|
619
|
+
}) {
|
|
620
|
+
const uid = useId();
|
|
621
|
+
const markerId = `ods-flow-edge-arrow-${uid.replace(/:/g, "")}`;
|
|
622
|
+
const registry = useHandleRegistry();
|
|
623
|
+
const [hovered, setHovered] = useState(false);
|
|
624
|
+
const [editing, setEditing] = useState(false);
|
|
625
|
+
const [draftLabel, setDraftLabel] = useState(edge.label ?? "");
|
|
626
|
+
const inputRef = useRef(null);
|
|
627
|
+
useEffect(() => {
|
|
628
|
+
if (!editing) setDraftLabel(edge.label ?? "");
|
|
629
|
+
}, [edge.label, editing]);
|
|
630
|
+
const sourceNode = nodes.find((n) => n.id === edge.source);
|
|
631
|
+
const targetNode = nodes.find((n) => n.id === edge.target);
|
|
632
|
+
if (!sourceNode || !targetNode) return null;
|
|
633
|
+
const sourceHandleId = edge.sourceHandle ?? "default";
|
|
634
|
+
const targetHandleId = edge.targetHandle ?? "default";
|
|
635
|
+
const sourceDesc = registry.resolve(sourceNode.id, "source", sourceHandleId);
|
|
636
|
+
const targetDesc = registry.resolve(targetNode.id, "target", targetHandleId);
|
|
637
|
+
const sourceSide = sourceDesc?.side ?? sourceNode.sourcePosition ?? "bottom";
|
|
638
|
+
const targetSide = targetDesc?.side ?? targetNode.targetPosition ?? "top";
|
|
639
|
+
const sourceIndex = sourceDesc?.index ?? 0;
|
|
640
|
+
const sourceTotal = sourceDesc?.total ?? 1;
|
|
641
|
+
const targetIndex = targetDesc?.index ?? 0;
|
|
642
|
+
const targetTotal = targetDesc?.total ?? 1;
|
|
643
|
+
const rawStart = handleCentre(sourceNode, sourceSide, sourceIndex, sourceTotal);
|
|
644
|
+
const rawEnd = handleCentre(targetNode, targetSide, targetIndex, targetTotal);
|
|
645
|
+
const HANDLE_GAP = 8;
|
|
646
|
+
const start = offsetAlongSide2(rawStart, sourceSide, HANDLE_GAP);
|
|
647
|
+
const end = offsetAlongSide2(rawEnd, targetSide, HANDLE_GAP);
|
|
648
|
+
const routing = edge.routing ?? "bezier";
|
|
649
|
+
const { d, midX, midY } = buildEdgePath(routing, start, sourceSide, end, targetSide, {
|
|
650
|
+
curvature,
|
|
651
|
+
borderRadius
|
|
652
|
+
});
|
|
653
|
+
const { d: hitD } = buildEdgePath(routing, rawStart, sourceSide, rawEnd, targetSide, {
|
|
654
|
+
curvature,
|
|
655
|
+
borderRadius
|
|
656
|
+
});
|
|
657
|
+
const type = edge.type ?? "default";
|
|
658
|
+
const isHot = hovered || edge.selected;
|
|
659
|
+
const stroke = type === "error" ? "var(--ods-status-failed, #dc2626)" : isHot ? "var(--ods-accent, #4f46e5)" : "var(--ods-border-strong, #6b7280)";
|
|
660
|
+
const typeDash = type === "conditional" ? "6 4" : type === "loop" ? "4 4" : void 0;
|
|
661
|
+
const dash = edge.style?.strokeDasharray ?? typeDash;
|
|
662
|
+
const strokeWidth = edge.style?.strokeWidth ?? (isHot ? 2 : 1.4);
|
|
663
|
+
const animateDash = edge.animated === true;
|
|
664
|
+
const label = edge.label;
|
|
665
|
+
const hasDelete = showDelete && (isHot || edge.selected) && !!onDelete;
|
|
666
|
+
const showChrome = !!label || editing || hasDelete;
|
|
667
|
+
const commitLabel = (next) => {
|
|
668
|
+
setEditing(false);
|
|
669
|
+
if (next !== edge.label) onLabelChange?.(edge.id, next);
|
|
670
|
+
};
|
|
671
|
+
const onLabelKey = (e) => {
|
|
672
|
+
if (e.key === "Enter") {
|
|
673
|
+
e.preventDefault();
|
|
674
|
+
commitLabel(draftLabel);
|
|
675
|
+
} else if (e.key === "Escape") {
|
|
676
|
+
e.preventDefault();
|
|
677
|
+
setEditing(false);
|
|
678
|
+
setDraftLabel(edge.label ?? "");
|
|
679
|
+
}
|
|
680
|
+
};
|
|
681
|
+
return /* @__PURE__ */ jsxs(
|
|
682
|
+
"g",
|
|
683
|
+
{
|
|
684
|
+
className: cn(
|
|
685
|
+
"ods-flow-edge-v2",
|
|
686
|
+
`ods-flow-edge-v2--${type}`,
|
|
687
|
+
`ods-flow-edge-v2--routing-${routing}`,
|
|
688
|
+
edge.selected && "ods-flow-edge-v2--selected",
|
|
689
|
+
hovered && "ods-flow-edge-v2--hovered",
|
|
690
|
+
edge.className
|
|
691
|
+
),
|
|
692
|
+
"data-edge-id": edge.id,
|
|
693
|
+
"data-edge-routing": routing,
|
|
694
|
+
onMouseEnter: () => setHovered(true),
|
|
695
|
+
onMouseLeave: () => setHovered(false),
|
|
696
|
+
onClick: (e) => {
|
|
697
|
+
e.stopPropagation();
|
|
698
|
+
onSelect?.(edge.id);
|
|
699
|
+
},
|
|
700
|
+
children: [
|
|
701
|
+
/* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsx(
|
|
702
|
+
"marker",
|
|
703
|
+
{
|
|
704
|
+
id: markerId,
|
|
705
|
+
viewBox: "0 0 10 10",
|
|
706
|
+
refX: "9",
|
|
707
|
+
refY: "5",
|
|
708
|
+
markerWidth: "7",
|
|
709
|
+
markerHeight: "7",
|
|
710
|
+
orient: "auto-start-reverse",
|
|
711
|
+
children: /* @__PURE__ */ jsx("path", { d: "M0 0L10 5L0 10z", fill: stroke })
|
|
712
|
+
}
|
|
713
|
+
) }),
|
|
714
|
+
/* @__PURE__ */ jsx("path", { d: hitD, stroke: "transparent", strokeWidth: 20, fill: "none" }),
|
|
715
|
+
/* @__PURE__ */ jsx(
|
|
716
|
+
"path",
|
|
717
|
+
{
|
|
718
|
+
d,
|
|
719
|
+
stroke,
|
|
720
|
+
strokeWidth,
|
|
721
|
+
strokeDasharray: dash,
|
|
722
|
+
fill: "none",
|
|
723
|
+
markerEnd: `url(#${markerId})`,
|
|
724
|
+
style: animateDash ? {
|
|
725
|
+
strokeDasharray: dash ?? "6 4",
|
|
726
|
+
animation: "ods-flow-edge-flow 0.6s linear infinite"
|
|
727
|
+
} : void 0
|
|
728
|
+
}
|
|
729
|
+
),
|
|
730
|
+
showChrome && /* @__PURE__ */ jsx(
|
|
731
|
+
"foreignObject",
|
|
732
|
+
{
|
|
733
|
+
x: midX - 140,
|
|
734
|
+
y: midY - 16,
|
|
735
|
+
width: 280,
|
|
736
|
+
height: 32,
|
|
737
|
+
style: { overflow: "visible", pointerEvents: "none" },
|
|
738
|
+
children: /* @__PURE__ */ jsxs("div", { className: "ods-flow-edge-v2__chrome", children: [
|
|
739
|
+
(label || editing) && (editing ? /* @__PURE__ */ jsx(
|
|
740
|
+
"input",
|
|
741
|
+
{
|
|
742
|
+
ref: inputRef,
|
|
743
|
+
className: "ods-flow-edge-v2__label-input",
|
|
744
|
+
value: draftLabel,
|
|
745
|
+
autoFocus: true,
|
|
746
|
+
onChange: (e) => setDraftLabel(e.target.value),
|
|
747
|
+
onBlur: () => commitLabel(draftLabel),
|
|
748
|
+
onKeyDown: onLabelKey,
|
|
749
|
+
onClick: (e) => e.stopPropagation(),
|
|
750
|
+
onMouseDown: (e) => e.stopPropagation(),
|
|
751
|
+
"aria-label": "Edit edge label"
|
|
752
|
+
}
|
|
753
|
+
) : /* @__PURE__ */ jsx(
|
|
754
|
+
"button",
|
|
755
|
+
{
|
|
756
|
+
type: "button",
|
|
757
|
+
className: "ods-flow-edge-v2__label-chip",
|
|
758
|
+
onDoubleClick: (e) => {
|
|
759
|
+
if (!onLabelChange) return;
|
|
760
|
+
e.stopPropagation();
|
|
761
|
+
setDraftLabel(edge.label ?? "");
|
|
762
|
+
setEditing(true);
|
|
763
|
+
},
|
|
764
|
+
onClick: (e) => {
|
|
765
|
+
e.stopPropagation();
|
|
766
|
+
onSelect?.(edge.id);
|
|
767
|
+
},
|
|
768
|
+
title: onLabelChange ? "Double-click to edit" : void 0,
|
|
769
|
+
tabIndex: onLabelChange ? 0 : -1,
|
|
770
|
+
children: /* @__PURE__ */ jsx("span", { children: label })
|
|
771
|
+
}
|
|
772
|
+
)),
|
|
773
|
+
hasDelete && /* @__PURE__ */ jsx(
|
|
774
|
+
"button",
|
|
775
|
+
{
|
|
776
|
+
type: "button",
|
|
777
|
+
className: "ods-flow-edge-v2__delete-btn",
|
|
778
|
+
onClick: (e) => {
|
|
779
|
+
e.stopPropagation();
|
|
780
|
+
onDelete?.(edge.id);
|
|
781
|
+
},
|
|
782
|
+
"aria-label": "Delete edge",
|
|
783
|
+
children: /* @__PURE__ */ jsx("svg", { width: "10", height: "10", viewBox: "0 0 10 10", "aria-hidden": "true", children: /* @__PURE__ */ jsx(
|
|
784
|
+
"path",
|
|
785
|
+
{
|
|
786
|
+
d: "M2 2l6 6M8 2l-6 6",
|
|
787
|
+
stroke: "currentColor",
|
|
788
|
+
strokeWidth: 1.5,
|
|
789
|
+
strokeLinecap: "round"
|
|
790
|
+
}
|
|
791
|
+
) })
|
|
792
|
+
}
|
|
793
|
+
)
|
|
794
|
+
] })
|
|
795
|
+
}
|
|
796
|
+
)
|
|
797
|
+
]
|
|
798
|
+
}
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
function offsetAlongSide2(p, side, gap) {
|
|
802
|
+
switch (side) {
|
|
803
|
+
case "top":
|
|
804
|
+
return { x: p.x, y: p.y - gap };
|
|
805
|
+
case "bottom":
|
|
806
|
+
return { x: p.x, y: p.y + gap };
|
|
807
|
+
case "left":
|
|
808
|
+
return { x: p.x - gap, y: p.y };
|
|
809
|
+
case "right":
|
|
810
|
+
return { x: p.x + gap, y: p.y };
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
var FlowEdge = memo(FlowEdgeImpl, (prev, next) => {
|
|
814
|
+
if (prev.edge !== next.edge) return false;
|
|
815
|
+
if (prev.onSelect !== next.onSelect || prev.onDelete !== next.onDelete) return false;
|
|
816
|
+
if (prev.onLabelChange !== next.onLabelChange) return false;
|
|
817
|
+
if (prev.showDelete !== next.showDelete) return false;
|
|
818
|
+
if (prev.curvature !== next.curvature || prev.borderRadius !== next.borderRadius) return false;
|
|
819
|
+
if (prev.handleVersion !== next.handleVersion) return false;
|
|
820
|
+
const ps = prev.nodes.find((n) => n.id === prev.edge.source);
|
|
821
|
+
const pt = prev.nodes.find((n) => n.id === prev.edge.target);
|
|
822
|
+
const ns = next.nodes.find((n) => n.id === next.edge.source);
|
|
823
|
+
const nt = next.nodes.find((n) => n.id === next.edge.target);
|
|
824
|
+
if (ps !== ns || pt !== nt) return false;
|
|
825
|
+
return true;
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
// src/workflow/components/FlowNode/FlowNodeContext.tsx
|
|
829
|
+
import { createContext as createContext4, useContext as useContext5 } from "react";
|
|
830
|
+
var FlowNodeContext = createContext4(null);
|
|
831
|
+
function useFlowNodeContext() {
|
|
832
|
+
const v = useContext5(FlowNodeContext);
|
|
833
|
+
if (!v) {
|
|
834
|
+
throw new Error(
|
|
835
|
+
"[@octaviaflow/core/workflow] Handle / NodeToolbar must be inside a node renderer."
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
return v;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// src/workflow/components/FlowNode/FlowNode.tsx
|
|
842
|
+
import { useEffect as useEffect2, useMemo as useMemo2, useRef as useRef2 } from "react";
|
|
843
|
+
|
|
844
|
+
// src/workflow/components/FlowCanvas/FlowCanvasContext.tsx
|
|
845
|
+
import { createContext as createContext5, useContext as useContext6 } from "react";
|
|
846
|
+
var FlowDispatchContext = createContext5(null);
|
|
847
|
+
function useFlowDispatch() {
|
|
848
|
+
const dispatch = useContext6(FlowDispatchContext);
|
|
849
|
+
if (!dispatch) {
|
|
850
|
+
throw new Error(
|
|
851
|
+
"[@octaviaflow/core/workflow] useFlowDispatch must be called inside <FlowCanvas>."
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
return dispatch;
|
|
855
|
+
}
|
|
856
|
+
var FlowNodeBridgeContext = createContext5(null);
|
|
857
|
+
function useFlowNodeBridge() {
|
|
858
|
+
const b = useContext6(FlowNodeBridgeContext);
|
|
859
|
+
if (!b) {
|
|
860
|
+
throw new Error("[@octaviaflow/core/workflow] FlowNode must be a child of <FlowCanvas>.");
|
|
861
|
+
}
|
|
862
|
+
return b;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// src/workflow/components/FlowNode/FlowNode.tsx
|
|
866
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
867
|
+
function FlowNode({
|
|
868
|
+
node,
|
|
869
|
+
selected,
|
|
870
|
+
dragging,
|
|
871
|
+
isConnecting,
|
|
872
|
+
Kind,
|
|
873
|
+
// Match the BaseNode body's fixed 368 px width so the wrapper bbox and
|
|
874
|
+
// the rendered card line up on the first paint — before the
|
|
875
|
+
// ResizeObserver has had a chance to write back the measured size.
|
|
876
|
+
// Container kinds (group / forEach) opt out by setting `node.width`.
|
|
877
|
+
defaultWidth = 368
|
|
878
|
+
}) {
|
|
879
|
+
const bridge = useFlowNodeBridge();
|
|
880
|
+
const wrapperRef = useRef2(null);
|
|
881
|
+
useEffect2(() => {
|
|
882
|
+
const el = wrapperRef.current;
|
|
883
|
+
if (!el || typeof ResizeObserver === "undefined") return;
|
|
884
|
+
if (node.type === "group" || node.type === "forEach") return;
|
|
885
|
+
const ro = new ResizeObserver(() => {
|
|
886
|
+
bridge.reportDimensions(node.id, el.offsetWidth, el.offsetHeight);
|
|
887
|
+
});
|
|
888
|
+
ro.observe(el);
|
|
889
|
+
return () => ro.disconnect();
|
|
890
|
+
}, [bridge, node.id, node.type]);
|
|
891
|
+
const ctx = useMemo2(
|
|
892
|
+
() => ({ id: node.id, node, selected }),
|
|
893
|
+
[node, selected]
|
|
894
|
+
);
|
|
895
|
+
const downPosRef = useRef2(null);
|
|
896
|
+
const draggedRef = useRef2(false);
|
|
897
|
+
const CLICK_VS_DRAG_PX = 5;
|
|
898
|
+
const handlePointerDown = (e) => {
|
|
899
|
+
if (e.target.closest("[data-handle-id]")) return;
|
|
900
|
+
if (e.target.closest("[data-flow-no-drag='true']")) return;
|
|
901
|
+
e.stopPropagation();
|
|
902
|
+
downPosRef.current = { x: e.clientX, y: e.clientY };
|
|
903
|
+
draggedRef.current = false;
|
|
904
|
+
bridge.beginNodeDrag(node.id, e.pointerId, e.clientX, e.clientY, e.altKey);
|
|
905
|
+
};
|
|
906
|
+
const handlePointerMove = (e) => {
|
|
907
|
+
const start = downPosRef.current;
|
|
908
|
+
if (!start || draggedRef.current) return;
|
|
909
|
+
if (Math.abs(e.clientX - start.x) > CLICK_VS_DRAG_PX || Math.abs(e.clientY - start.y) > CLICK_VS_DRAG_PX) {
|
|
910
|
+
draggedRef.current = true;
|
|
911
|
+
}
|
|
912
|
+
};
|
|
913
|
+
const handleClick = (e) => {
|
|
914
|
+
if (e.target.closest("[data-handle-id]")) return;
|
|
915
|
+
e.stopPropagation();
|
|
916
|
+
if (draggedRef.current) {
|
|
917
|
+
draggedRef.current = false;
|
|
918
|
+
downPosRef.current = null;
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
downPosRef.current = null;
|
|
922
|
+
bridge.selectNode(node.id, e.metaKey || e.ctrlKey || e.shiftKey);
|
|
923
|
+
bridge.notifyNodeClick(node.id);
|
|
924
|
+
};
|
|
925
|
+
return /* @__PURE__ */ jsx2(
|
|
926
|
+
"div",
|
|
927
|
+
{
|
|
928
|
+
ref: wrapperRef,
|
|
929
|
+
"data-node-id": node.id,
|
|
930
|
+
"data-node-type": node.type,
|
|
931
|
+
"data-node-selected": selected ? "true" : "false",
|
|
932
|
+
className: cn(
|
|
933
|
+
"ods-flow-node-v2",
|
|
934
|
+
`ods-flow-node-v2--kind-${node.type}`,
|
|
935
|
+
selected && "ods-flow-node-v2--selected",
|
|
936
|
+
dragging && "ods-flow-node-v2--dragging",
|
|
937
|
+
isConnecting && "ods-flow-node-v2--connecting",
|
|
938
|
+
node.className
|
|
939
|
+
),
|
|
940
|
+
style: {
|
|
941
|
+
position: "absolute",
|
|
942
|
+
left: node.position.x,
|
|
943
|
+
top: node.position.y,
|
|
944
|
+
width: node.width ?? defaultWidth,
|
|
945
|
+
// Z-stack (per architecture doc): groups (20) sit below normal
|
|
946
|
+
// nodes (30) so children render visually on top of their parent
|
|
947
|
+
// frame. Selection raises by 10. Consumer override wins.
|
|
948
|
+
zIndex: node.zIndex ?? (selected ? node.type === "group" ? 35 : 40 : node.type === "group" ? 20 : 30),
|
|
949
|
+
...node.style
|
|
950
|
+
},
|
|
951
|
+
onPointerDown: handlePointerDown,
|
|
952
|
+
onPointerMove: handlePointerMove,
|
|
953
|
+
onClick: handleClick,
|
|
954
|
+
children: /* @__PURE__ */ jsx2(FlowNodeContext.Provider, { value: ctx, children: /* @__PURE__ */ jsx2(Kind, { node, selected, dragging, isConnecting }) })
|
|
955
|
+
}
|
|
956
|
+
);
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// src/workflow/components/FlowNode/nodeKinds.ts
|
|
960
|
+
function buildNodeKindRegistry(defaults, overrides) {
|
|
961
|
+
return Object.freeze({ ...defaults, ...overrides ?? {} });
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// src/workflow/components/Handle/Handle.tsx
|
|
965
|
+
import { useEffect as useEffect3, useRef as useRef3 } from "react";
|
|
966
|
+
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
967
|
+
var DEFAULT_HANDLE_ID = "default";
|
|
968
|
+
function Handle({
|
|
969
|
+
type,
|
|
970
|
+
position,
|
|
971
|
+
id = DEFAULT_HANDLE_ID,
|
|
972
|
+
isConnectable = true,
|
|
973
|
+
isConnectableStart,
|
|
974
|
+
isConnectableEnd,
|
|
975
|
+
index = 0,
|
|
976
|
+
total = 1,
|
|
977
|
+
label,
|
|
978
|
+
className,
|
|
979
|
+
style
|
|
980
|
+
}) {
|
|
981
|
+
const registry = useHandleRegistry();
|
|
982
|
+
const node = useFlowNodeContext();
|
|
983
|
+
const dispatch = useFlowDispatch();
|
|
984
|
+
const ref = useRef3(null);
|
|
985
|
+
const canStart = isConnectableStart ?? isConnectable;
|
|
986
|
+
const canEnd = isConnectableEnd ?? isConnectable;
|
|
987
|
+
useEffect3(() => {
|
|
988
|
+
const dispose = registry.register({
|
|
989
|
+
nodeId: node.id,
|
|
990
|
+
handleId: id,
|
|
991
|
+
type,
|
|
992
|
+
side: position,
|
|
993
|
+
index,
|
|
994
|
+
total
|
|
995
|
+
});
|
|
996
|
+
return dispose;
|
|
997
|
+
}, [registry, node.id, id, type, position, index, total]);
|
|
998
|
+
const handlePointerDown = (e) => {
|
|
999
|
+
if (!canStart) return;
|
|
1000
|
+
e.stopPropagation();
|
|
1001
|
+
e.preventDefault();
|
|
1002
|
+
dispatch({
|
|
1003
|
+
type: "connection/start",
|
|
1004
|
+
nodeId: node.id,
|
|
1005
|
+
handleId: id,
|
|
1006
|
+
handleType: type,
|
|
1007
|
+
pointerId: e.pointerId,
|
|
1008
|
+
clientX: e.clientX,
|
|
1009
|
+
clientY: e.clientY
|
|
1010
|
+
});
|
|
1011
|
+
};
|
|
1012
|
+
return /* @__PURE__ */ jsxs2(
|
|
1013
|
+
"div",
|
|
1014
|
+
{
|
|
1015
|
+
ref,
|
|
1016
|
+
"data-handle-id": id,
|
|
1017
|
+
"data-handle-node-id": node.id,
|
|
1018
|
+
"data-handle-type": type,
|
|
1019
|
+
"data-handle-side": position,
|
|
1020
|
+
"data-handle-connectable-end": canEnd ? "true" : "false",
|
|
1021
|
+
className: cn(
|
|
1022
|
+
"ods-flow-handle",
|
|
1023
|
+
`ods-flow-handle--${type}`,
|
|
1024
|
+
`ods-flow-handle--side-${position}`,
|
|
1025
|
+
!isConnectable && "ods-flow-handle--disabled",
|
|
1026
|
+
className
|
|
1027
|
+
),
|
|
1028
|
+
style: {
|
|
1029
|
+
...handleSideStyle(position, index, total),
|
|
1030
|
+
...style
|
|
1031
|
+
},
|
|
1032
|
+
onPointerDown: handlePointerDown,
|
|
1033
|
+
children: [
|
|
1034
|
+
/* @__PURE__ */ jsx3("div", { className: "ods-flow-handle__dot" }),
|
|
1035
|
+
label && /* @__PURE__ */ jsx3("span", { className: "ods-flow-handle__label", children: label })
|
|
1036
|
+
]
|
|
1037
|
+
}
|
|
1038
|
+
);
|
|
1039
|
+
}
|
|
1040
|
+
function handleSideStyle(side, index, total) {
|
|
1041
|
+
const ratio = (index + 1) / (total + 1) * 100;
|
|
1042
|
+
switch (side) {
|
|
1043
|
+
case "top":
|
|
1044
|
+
return { position: "absolute", top: -6, left: `${ratio}%`, transform: "translateX(-50%)" };
|
|
1045
|
+
case "bottom":
|
|
1046
|
+
return { position: "absolute", bottom: -6, left: `${ratio}%`, transform: "translateX(-50%)" };
|
|
1047
|
+
case "left":
|
|
1048
|
+
return { position: "absolute", left: -6, top: `${ratio}%`, transform: "translateY(-50%)" };
|
|
1049
|
+
case "right":
|
|
1050
|
+
return { position: "absolute", right: -6, top: `${ratio}%`, transform: "translateY(-50%)" };
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// src/workflow/components/NodeResizer/NodeResizer.tsx
|
|
1055
|
+
import { useRef as useRef4 } from "react";
|
|
1056
|
+
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
1057
|
+
function NodeResizer({
|
|
1058
|
+
isVisible,
|
|
1059
|
+
minWidth = 80,
|
|
1060
|
+
minHeight = 60,
|
|
1061
|
+
maxWidth,
|
|
1062
|
+
maxHeight,
|
|
1063
|
+
keepAspectRatio = false,
|
|
1064
|
+
onResize,
|
|
1065
|
+
onResizeEnd,
|
|
1066
|
+
color
|
|
1067
|
+
}) {
|
|
1068
|
+
const { node, selected } = useFlowNodeContext();
|
|
1069
|
+
const viewport = useViewport();
|
|
1070
|
+
const flow = useFlow();
|
|
1071
|
+
const dragRef = useRef4(null);
|
|
1072
|
+
const show = isVisible ?? selected;
|
|
1073
|
+
if (!show) return null;
|
|
1074
|
+
const beginResize = (e, corner) => {
|
|
1075
|
+
e.preventDefault();
|
|
1076
|
+
e.stopPropagation();
|
|
1077
|
+
e.target.setPointerCapture(e.pointerId);
|
|
1078
|
+
const w = node.width ?? DEFAULT_NODE_WIDTH;
|
|
1079
|
+
const h = node.height ?? DEFAULT_NODE_HEIGHT;
|
|
1080
|
+
dragRef.current = {
|
|
1081
|
+
pointerId: e.pointerId,
|
|
1082
|
+
corner,
|
|
1083
|
+
startClientX: e.clientX,
|
|
1084
|
+
startClientY: e.clientY,
|
|
1085
|
+
startWidth: w,
|
|
1086
|
+
startHeight: h,
|
|
1087
|
+
startX: node.position.x,
|
|
1088
|
+
startY: node.position.y,
|
|
1089
|
+
aspect: w / Math.max(1, h)
|
|
1090
|
+
};
|
|
1091
|
+
};
|
|
1092
|
+
const onMove = (e) => {
|
|
1093
|
+
const drag = dragRef.current;
|
|
1094
|
+
if (!drag || drag.pointerId !== e.pointerId) return;
|
|
1095
|
+
const dx = (e.clientX - drag.startClientX) / viewport.zoom;
|
|
1096
|
+
const dy = (e.clientY - drag.startClientY) / viewport.zoom;
|
|
1097
|
+
let nextW = drag.startWidth;
|
|
1098
|
+
let nextH = drag.startHeight;
|
|
1099
|
+
let nextX = drag.startX;
|
|
1100
|
+
let nextY = drag.startY;
|
|
1101
|
+
switch (drag.corner) {
|
|
1102
|
+
case "se":
|
|
1103
|
+
nextW = drag.startWidth + dx;
|
|
1104
|
+
nextH = drag.startHeight + dy;
|
|
1105
|
+
break;
|
|
1106
|
+
case "sw":
|
|
1107
|
+
nextW = drag.startWidth - dx;
|
|
1108
|
+
nextH = drag.startHeight + dy;
|
|
1109
|
+
nextX = drag.startX + dx;
|
|
1110
|
+
break;
|
|
1111
|
+
case "ne":
|
|
1112
|
+
nextW = drag.startWidth + dx;
|
|
1113
|
+
nextH = drag.startHeight - dy;
|
|
1114
|
+
nextY = drag.startY + dy;
|
|
1115
|
+
break;
|
|
1116
|
+
case "nw":
|
|
1117
|
+
nextW = drag.startWidth - dx;
|
|
1118
|
+
nextH = drag.startHeight - dy;
|
|
1119
|
+
nextX = drag.startX + dx;
|
|
1120
|
+
nextY = drag.startY + dy;
|
|
1121
|
+
break;
|
|
1122
|
+
}
|
|
1123
|
+
if (keepAspectRatio) {
|
|
1124
|
+
nextH = nextW / drag.aspect;
|
|
1125
|
+
if (drag.corner === "nw" || drag.corner === "ne") {
|
|
1126
|
+
nextY = drag.startY + (drag.startHeight - nextH);
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
nextW = Math.max(minWidth, maxWidth ? Math.min(maxWidth, nextW) : nextW);
|
|
1130
|
+
nextH = Math.max(minHeight, maxHeight ? Math.min(maxHeight, nextH) : nextH);
|
|
1131
|
+
flow.updateNode(node.id, {
|
|
1132
|
+
width: nextW,
|
|
1133
|
+
height: nextH,
|
|
1134
|
+
position: { x: nextX, y: nextY }
|
|
1135
|
+
});
|
|
1136
|
+
onResize?.({ width: nextW, height: nextH });
|
|
1137
|
+
};
|
|
1138
|
+
const onUp = (e) => {
|
|
1139
|
+
if (dragRef.current?.pointerId === e.pointerId) {
|
|
1140
|
+
const cur = flow.getNode(node.id);
|
|
1141
|
+
if (cur) {
|
|
1142
|
+
onResizeEnd?.({
|
|
1143
|
+
width: cur.width ?? DEFAULT_NODE_WIDTH,
|
|
1144
|
+
height: cur.height ?? DEFAULT_NODE_HEIGHT
|
|
1145
|
+
});
|
|
1146
|
+
}
|
|
1147
|
+
dragRef.current = null;
|
|
1148
|
+
}
|
|
1149
|
+
};
|
|
1150
|
+
const handleColor = color ?? "var(--ods-accent)";
|
|
1151
|
+
const handleStyle = (corner) => {
|
|
1152
|
+
const base = {
|
|
1153
|
+
position: "absolute",
|
|
1154
|
+
width: 12,
|
|
1155
|
+
height: 12,
|
|
1156
|
+
background: "var(--ods-surface-canvas)",
|
|
1157
|
+
border: `2px solid ${handleColor}`,
|
|
1158
|
+
borderRadius: 2,
|
|
1159
|
+
cursor: cursorFor(corner),
|
|
1160
|
+
touchAction: "none",
|
|
1161
|
+
// Place each handle so its CENTRE sits on the corresponding corner.
|
|
1162
|
+
transform: "translate(-50%, -50%)"
|
|
1163
|
+
};
|
|
1164
|
+
switch (corner) {
|
|
1165
|
+
case "nw":
|
|
1166
|
+
return { ...base, top: 0, left: 0 };
|
|
1167
|
+
case "ne":
|
|
1168
|
+
return { ...base, top: 0, left: "100%" };
|
|
1169
|
+
case "sw":
|
|
1170
|
+
return { ...base, top: "100%", left: 0 };
|
|
1171
|
+
case "se":
|
|
1172
|
+
return { ...base, top: "100%", left: "100%" };
|
|
1173
|
+
}
|
|
1174
|
+
};
|
|
1175
|
+
return /* @__PURE__ */ jsx4("div", { className: cn("ods-node-resizer"), "data-flow-no-drag": "true", children: ["nw", "ne", "sw", "se"].map((corner) => /* @__PURE__ */ jsx4(
|
|
1176
|
+
"div",
|
|
1177
|
+
{
|
|
1178
|
+
style: handleStyle(corner),
|
|
1179
|
+
onPointerDown: (e) => beginResize(e, corner),
|
|
1180
|
+
onPointerMove: onMove,
|
|
1181
|
+
onPointerUp: onUp,
|
|
1182
|
+
onPointerCancel: onUp,
|
|
1183
|
+
"aria-label": `Resize ${corner}`
|
|
1184
|
+
},
|
|
1185
|
+
corner
|
|
1186
|
+
)) });
|
|
1187
|
+
}
|
|
1188
|
+
function cursorFor(corner) {
|
|
1189
|
+
switch (corner) {
|
|
1190
|
+
case "nw":
|
|
1191
|
+
case "se":
|
|
1192
|
+
return "nwse-resize";
|
|
1193
|
+
case "ne":
|
|
1194
|
+
case "sw":
|
|
1195
|
+
return "nesw-resize";
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
// src/workflow/components/kinds/BaseNode.tsx
|
|
1200
|
+
import { TrashCanIcon } from "@octaviaflow/icons";
|
|
1201
|
+
import { useContext as useContext7 } from "react";
|
|
1202
|
+
import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1203
|
+
function BaseNode({
|
|
1204
|
+
kind,
|
|
1205
|
+
kindIcon,
|
|
1206
|
+
icon,
|
|
1207
|
+
title,
|
|
1208
|
+
chip,
|
|
1209
|
+
description,
|
|
1210
|
+
valueChip,
|
|
1211
|
+
accent = "green",
|
|
1212
|
+
status,
|
|
1213
|
+
onDelete,
|
|
1214
|
+
footer,
|
|
1215
|
+
className,
|
|
1216
|
+
children
|
|
1217
|
+
}) {
|
|
1218
|
+
const ctx = useContext7(FlowNodeContext);
|
|
1219
|
+
const bridge = useContext7(FlowNodeBridgeContext);
|
|
1220
|
+
const deleteHandler = onDelete === false ? void 0 : onDelete ?? (ctx && bridge ? () => bridge.deleteNode(ctx.id) : void 0);
|
|
1221
|
+
return /* @__PURE__ */ jsxs3(
|
|
1222
|
+
"div",
|
|
1223
|
+
{
|
|
1224
|
+
className: cn(
|
|
1225
|
+
"ods-flow-base-node",
|
|
1226
|
+
`ods-flow-base-node--accent-${accent}`,
|
|
1227
|
+
status && `ods-flow-base-node--status-${status}`,
|
|
1228
|
+
className
|
|
1229
|
+
),
|
|
1230
|
+
children: [
|
|
1231
|
+
/* @__PURE__ */ jsxs3("div", { className: "ods-flow-base-node__pill", children: [
|
|
1232
|
+
kindIcon && /* @__PURE__ */ jsx5("span", { className: "ods-flow-base-node__pill-icon", "aria-hidden": "true", children: kindIcon }),
|
|
1233
|
+
/* @__PURE__ */ jsx5("span", { className: "ods-flow-base-node__pill-label", children: kind })
|
|
1234
|
+
] }),
|
|
1235
|
+
status && status !== "idle" && /* @__PURE__ */ jsx5(
|
|
1236
|
+
"span",
|
|
1237
|
+
{
|
|
1238
|
+
className: cn("ods-flow-base-node__status", `ods-flow-base-node__status--${status}`),
|
|
1239
|
+
"aria-hidden": "true"
|
|
1240
|
+
}
|
|
1241
|
+
),
|
|
1242
|
+
/* @__PURE__ */ jsxs3("div", { className: "ods-flow-base-node__body", children: [
|
|
1243
|
+
/* @__PURE__ */ jsxs3("div", { className: "ods-flow-base-node__content", children: [
|
|
1244
|
+
/* @__PURE__ */ jsx5("div", { className: "ods-flow-base-node__bubble", "aria-hidden": "true", children: icon }),
|
|
1245
|
+
/* @__PURE__ */ jsxs3("div", { className: "ods-flow-base-node__content-text", children: [
|
|
1246
|
+
/* @__PURE__ */ jsx5("div", { className: "ods-flow-base-node__content-title", children: title }),
|
|
1247
|
+
(chip !== void 0 || description !== void 0 || valueChip !== void 0) && /* @__PURE__ */ jsxs3("div", { className: "ods-flow-base-node__content-info", children: [
|
|
1248
|
+
chip !== void 0 && /* @__PURE__ */ jsx5("span", { className: "ods-flow-base-node__chip", children: chip }),
|
|
1249
|
+
description !== void 0 && /* @__PURE__ */ jsx5("span", { className: "ods-flow-base-node__description", children: description }),
|
|
1250
|
+
valueChip !== void 0 && /* @__PURE__ */ jsx5("span", { className: "ods-flow-base-node__value-chip", children: valueChip })
|
|
1251
|
+
] })
|
|
1252
|
+
] })
|
|
1253
|
+
] }),
|
|
1254
|
+
footer && /* @__PURE__ */ jsx5("div", { className: "ods-flow-base-node__footer", children: footer }),
|
|
1255
|
+
deleteHandler && /* @__PURE__ */ jsx5(
|
|
1256
|
+
"button",
|
|
1257
|
+
{
|
|
1258
|
+
type: "button",
|
|
1259
|
+
className: "ods-flow-base-node__delete",
|
|
1260
|
+
onClick: (e) => {
|
|
1261
|
+
e.stopPropagation();
|
|
1262
|
+
deleteHandler();
|
|
1263
|
+
},
|
|
1264
|
+
"aria-label": "Delete node",
|
|
1265
|
+
"data-flow-no-drag": "true",
|
|
1266
|
+
title: "Delete node",
|
|
1267
|
+
children: /* @__PURE__ */ jsx5(TrashCanIcon, { size: 16, "aria-hidden": true })
|
|
1268
|
+
}
|
|
1269
|
+
)
|
|
1270
|
+
] }),
|
|
1271
|
+
children
|
|
1272
|
+
]
|
|
1273
|
+
}
|
|
1274
|
+
);
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
// src/workflow/components/kinds/index.tsx
|
|
1278
|
+
import { Fragment, jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1279
|
+
var ActionNode = ({
|
|
1280
|
+
node
|
|
1281
|
+
}) => {
|
|
1282
|
+
const d = node.data ?? {};
|
|
1283
|
+
return /* @__PURE__ */ jsxs4(
|
|
1284
|
+
BaseNode,
|
|
1285
|
+
{
|
|
1286
|
+
kind: d.kind ?? "ACTION",
|
|
1287
|
+
icon: d.icon,
|
|
1288
|
+
title: d.title ?? "Action",
|
|
1289
|
+
chip: d.chip ?? d.badge,
|
|
1290
|
+
description: d.description ?? d.subtitle,
|
|
1291
|
+
valueChip: d.valueChip,
|
|
1292
|
+
status: d.status,
|
|
1293
|
+
accent: "green",
|
|
1294
|
+
children: [
|
|
1295
|
+
/* @__PURE__ */ jsx6(Handle, { type: "target", position: "top" }),
|
|
1296
|
+
/* @__PURE__ */ jsx6(Handle, { type: "source", position: "bottom" })
|
|
1297
|
+
]
|
|
1298
|
+
}
|
|
1299
|
+
);
|
|
1300
|
+
};
|
|
1301
|
+
var TriggerNode = ({
|
|
1302
|
+
node
|
|
1303
|
+
}) => {
|
|
1304
|
+
const d = node.data ?? {};
|
|
1305
|
+
return /* @__PURE__ */ jsx6(
|
|
1306
|
+
BaseNode,
|
|
1307
|
+
{
|
|
1308
|
+
kind: d.kind ?? "TRIGGER",
|
|
1309
|
+
icon: d.icon,
|
|
1310
|
+
title: d.title ?? "Trigger",
|
|
1311
|
+
chip: d.chip,
|
|
1312
|
+
description: d.description ?? "Manually triggered",
|
|
1313
|
+
valueChip: d.valueChip,
|
|
1314
|
+
status: d.status,
|
|
1315
|
+
accent: "green",
|
|
1316
|
+
children: /* @__PURE__ */ jsx6(Handle, { type: "source", position: "bottom" })
|
|
1317
|
+
}
|
|
1318
|
+
);
|
|
1319
|
+
};
|
|
1320
|
+
var ConditionNode = ({
|
|
1321
|
+
node
|
|
1322
|
+
}) => {
|
|
1323
|
+
const d = node.data ?? {};
|
|
1324
|
+
const branches = d.branches ?? [
|
|
1325
|
+
{ id: "true", label: "true" },
|
|
1326
|
+
{ id: "false", label: "false" }
|
|
1327
|
+
];
|
|
1328
|
+
return /* @__PURE__ */ jsxs4(
|
|
1329
|
+
BaseNode,
|
|
1330
|
+
{
|
|
1331
|
+
kind: d.kind ?? "CONDITION",
|
|
1332
|
+
icon: d.icon,
|
|
1333
|
+
title: d.title ?? "Condition",
|
|
1334
|
+
chip: d.chip ?? d.badge,
|
|
1335
|
+
description: d.description ?? d.subtitle,
|
|
1336
|
+
valueChip: d.valueChip,
|
|
1337
|
+
status: d.status,
|
|
1338
|
+
accent: "amber",
|
|
1339
|
+
children: [
|
|
1340
|
+
/* @__PURE__ */ jsx6(Handle, { type: "target", position: "top" }),
|
|
1341
|
+
branches.map((b, i) => /* @__PURE__ */ jsx6(
|
|
1342
|
+
Handle,
|
|
1343
|
+
{
|
|
1344
|
+
type: "source",
|
|
1345
|
+
position: "bottom",
|
|
1346
|
+
id: b.id,
|
|
1347
|
+
index: i,
|
|
1348
|
+
total: branches.length,
|
|
1349
|
+
label: b.label
|
|
1350
|
+
},
|
|
1351
|
+
b.id
|
|
1352
|
+
))
|
|
1353
|
+
]
|
|
1354
|
+
}
|
|
1355
|
+
);
|
|
1356
|
+
};
|
|
1357
|
+
var GroupNode = ({
|
|
1358
|
+
node
|
|
1359
|
+
}) => {
|
|
1360
|
+
const d = node.data ?? {};
|
|
1361
|
+
const collapsed = !!d.collapsed;
|
|
1362
|
+
const disabled = !!d.disabled;
|
|
1363
|
+
const hiddenCount = d.hiddenCount;
|
|
1364
|
+
const bridge = useFlowNodeBridge();
|
|
1365
|
+
const onChevronClick = (e) => {
|
|
1366
|
+
e.stopPropagation();
|
|
1367
|
+
bridge.toggleNodeCollapse(node.id);
|
|
1368
|
+
};
|
|
1369
|
+
return /* @__PURE__ */ jsxs4(
|
|
1370
|
+
"div",
|
|
1371
|
+
{
|
|
1372
|
+
className: "ods-flow-group",
|
|
1373
|
+
"data-collapsed": collapsed ? "true" : "false",
|
|
1374
|
+
"data-disabled": disabled ? "true" : void 0,
|
|
1375
|
+
style: {
|
|
1376
|
+
width: node.width ?? 360,
|
|
1377
|
+
height: collapsed ? 36 : node.height ?? 200
|
|
1378
|
+
},
|
|
1379
|
+
children: [
|
|
1380
|
+
!collapsed && /* @__PURE__ */ jsx6(NodeResizer, { minWidth: 240, minHeight: 120 }),
|
|
1381
|
+
/* @__PURE__ */ jsxs4("div", { className: "ods-flow-group__header", "data-flow-no-drag": "false", children: [
|
|
1382
|
+
/* @__PURE__ */ jsx6(
|
|
1383
|
+
"button",
|
|
1384
|
+
{
|
|
1385
|
+
type: "button",
|
|
1386
|
+
className: "ods-flow-group__chevron",
|
|
1387
|
+
"data-flow-no-drag": "true",
|
|
1388
|
+
"aria-label": collapsed ? "Expand group" : "Collapse group",
|
|
1389
|
+
"aria-expanded": !collapsed,
|
|
1390
|
+
onClick: onChevronClick,
|
|
1391
|
+
onPointerDown: (e) => e.stopPropagation(),
|
|
1392
|
+
children: collapsed ? "\u25B8" : "\u25BE"
|
|
1393
|
+
}
|
|
1394
|
+
),
|
|
1395
|
+
/* @__PURE__ */ jsx6("span", { className: "ods-flow-group__title", children: d.title ?? "Group" }),
|
|
1396
|
+
d.subtitle && /* @__PURE__ */ jsx6("span", { className: "ods-flow-group__subtitle", children: d.subtitle }),
|
|
1397
|
+
disabled && /* @__PURE__ */ jsx6("span", { className: "ods-flow-group__disabled-badge", "aria-label": "Disabled subflow", children: "Disabled" }),
|
|
1398
|
+
collapsed && hiddenCount !== void 0 && hiddenCount > 0 && /* @__PURE__ */ jsxs4("span", { className: "ods-flow-group__count", "aria-label": `${hiddenCount} hidden steps`, children: [
|
|
1399
|
+
hiddenCount,
|
|
1400
|
+
" steps"
|
|
1401
|
+
] })
|
|
1402
|
+
] }),
|
|
1403
|
+
/* @__PURE__ */ jsx6(Handle, { type: "target", position: "top", id: "__group_in" }),
|
|
1404
|
+
/* @__PURE__ */ jsx6(Handle, { type: "source", position: "bottom", id: "__group_out" })
|
|
1405
|
+
]
|
|
1406
|
+
}
|
|
1407
|
+
);
|
|
1408
|
+
};
|
|
1409
|
+
var ForEachNode = ({
|
|
1410
|
+
node
|
|
1411
|
+
}) => {
|
|
1412
|
+
const d = node.data ?? {};
|
|
1413
|
+
const iteratorExpr = d.iterator ?? d.description ?? "items[]";
|
|
1414
|
+
const collapsed = !!d.collapsed;
|
|
1415
|
+
const disabled = !!d.disabled;
|
|
1416
|
+
const hiddenCount = d.hiddenCount;
|
|
1417
|
+
const bridge = useFlowNodeBridge();
|
|
1418
|
+
const onChevronClick = (e) => {
|
|
1419
|
+
e.stopPropagation();
|
|
1420
|
+
bridge.toggleNodeCollapse(node.id);
|
|
1421
|
+
};
|
|
1422
|
+
return /* @__PURE__ */ jsxs4(
|
|
1423
|
+
"div",
|
|
1424
|
+
{
|
|
1425
|
+
className: "ods-flow-foreach",
|
|
1426
|
+
"data-collapsed": collapsed ? "true" : "false",
|
|
1427
|
+
"data-disabled": disabled ? "true" : void 0,
|
|
1428
|
+
style: {
|
|
1429
|
+
width: node.width ?? 420,
|
|
1430
|
+
height: collapsed ? 40 : node.height ?? 260
|
|
1431
|
+
},
|
|
1432
|
+
children: [
|
|
1433
|
+
!collapsed && /* @__PURE__ */ jsx6(NodeResizer, { minWidth: 240, minHeight: 120 }),
|
|
1434
|
+
/* @__PURE__ */ jsxs4("div", { className: "ods-flow-foreach__header", children: [
|
|
1435
|
+
/* @__PURE__ */ jsx6(
|
|
1436
|
+
"button",
|
|
1437
|
+
{
|
|
1438
|
+
type: "button",
|
|
1439
|
+
className: "ods-flow-foreach__chevron",
|
|
1440
|
+
"data-flow-no-drag": "true",
|
|
1441
|
+
"aria-label": collapsed ? "Expand iterator" : "Collapse iterator",
|
|
1442
|
+
"aria-expanded": !collapsed,
|
|
1443
|
+
onClick: onChevronClick,
|
|
1444
|
+
onPointerDown: (e) => e.stopPropagation(),
|
|
1445
|
+
children: collapsed ? "\u25B8" : "\u25BE"
|
|
1446
|
+
}
|
|
1447
|
+
),
|
|
1448
|
+
/* @__PURE__ */ jsx6("span", { className: "ods-flow-foreach__icon", "aria-hidden": "true", children: "\u21BB" }),
|
|
1449
|
+
/* @__PURE__ */ jsx6("span", { className: "ods-flow-foreach__title", children: d.title ?? "For each" }),
|
|
1450
|
+
/* @__PURE__ */ jsx6("code", { className: "ods-flow-foreach__iterator", children: iteratorExpr }),
|
|
1451
|
+
disabled && /* @__PURE__ */ jsx6(
|
|
1452
|
+
"span",
|
|
1453
|
+
{
|
|
1454
|
+
className: "ods-flow-foreach__disabled-badge",
|
|
1455
|
+
"aria-label": "Disabled subflow",
|
|
1456
|
+
children: "Disabled"
|
|
1457
|
+
}
|
|
1458
|
+
),
|
|
1459
|
+
collapsed && hiddenCount !== void 0 && hiddenCount > 0 && /* @__PURE__ */ jsxs4("span", { className: "ods-flow-foreach__count", "aria-label": `${hiddenCount} hidden steps`, children: [
|
|
1460
|
+
hiddenCount,
|
|
1461
|
+
" steps"
|
|
1462
|
+
] })
|
|
1463
|
+
] }),
|
|
1464
|
+
/* @__PURE__ */ jsx6(Handle, { type: "target", position: "top", id: "__group_in", label: !collapsed ? "in" : void 0 }),
|
|
1465
|
+
collapsed ? (
|
|
1466
|
+
// Collapsed: single bottom exit so the container behaves like a
|
|
1467
|
+
// regular action in the chain.
|
|
1468
|
+
/* @__PURE__ */ jsx6(Handle, { type: "source", position: "bottom", id: "__group_out" })
|
|
1469
|
+
) : (
|
|
1470
|
+
// Expanded: two bottom outputs spread across the bottom edge.
|
|
1471
|
+
// Index/total tell the Handle to position them at 33% and 66%
|
|
1472
|
+
// along the edge (per `handleSideStyle`). `each` is left of
|
|
1473
|
+
// centre, `__group_out` (labelled "done") is right of centre.
|
|
1474
|
+
/* @__PURE__ */ jsxs4(Fragment, { children: [
|
|
1475
|
+
/* @__PURE__ */ jsx6(
|
|
1476
|
+
Handle,
|
|
1477
|
+
{
|
|
1478
|
+
type: "source",
|
|
1479
|
+
position: "bottom",
|
|
1480
|
+
id: "each",
|
|
1481
|
+
label: "each",
|
|
1482
|
+
index: 0,
|
|
1483
|
+
total: 2
|
|
1484
|
+
}
|
|
1485
|
+
),
|
|
1486
|
+
/* @__PURE__ */ jsx6(
|
|
1487
|
+
Handle,
|
|
1488
|
+
{
|
|
1489
|
+
type: "source",
|
|
1490
|
+
position: "bottom",
|
|
1491
|
+
id: "__group_out",
|
|
1492
|
+
label: "done",
|
|
1493
|
+
index: 1,
|
|
1494
|
+
total: 2
|
|
1495
|
+
}
|
|
1496
|
+
)
|
|
1497
|
+
] })
|
|
1498
|
+
)
|
|
1499
|
+
]
|
|
1500
|
+
}
|
|
1501
|
+
);
|
|
1502
|
+
};
|
|
1503
|
+
var OutputNode = ({
|
|
1504
|
+
node
|
|
1505
|
+
}) => {
|
|
1506
|
+
const d = node.data ?? {};
|
|
1507
|
+
return /* @__PURE__ */ jsx6(
|
|
1508
|
+
BaseNode,
|
|
1509
|
+
{
|
|
1510
|
+
kind: d.kind ?? "OUTPUT",
|
|
1511
|
+
icon: d.icon,
|
|
1512
|
+
title: d.title ?? "Output",
|
|
1513
|
+
chip: d.chip ?? d.badge,
|
|
1514
|
+
description: d.description ?? d.subtitle,
|
|
1515
|
+
status: d.status,
|
|
1516
|
+
accent: "green",
|
|
1517
|
+
children: /* @__PURE__ */ jsx6(Handle, { type: "target", position: "top" })
|
|
1518
|
+
}
|
|
1519
|
+
);
|
|
1520
|
+
};
|
|
1521
|
+
var ErrorNode = ({
|
|
1522
|
+
node
|
|
1523
|
+
}) => {
|
|
1524
|
+
const d = node.data ?? {};
|
|
1525
|
+
return /* @__PURE__ */ jsxs4(
|
|
1526
|
+
BaseNode,
|
|
1527
|
+
{
|
|
1528
|
+
kind: d.kind ?? "ERROR",
|
|
1529
|
+
icon: d.icon,
|
|
1530
|
+
title: d.title ?? "On error",
|
|
1531
|
+
chip: d.chip ?? d.badge,
|
|
1532
|
+
description: d.description ?? d.subtitle,
|
|
1533
|
+
status: d.status ?? "error",
|
|
1534
|
+
accent: "red",
|
|
1535
|
+
children: [
|
|
1536
|
+
/* @__PURE__ */ jsx6(Handle, { type: "target", position: "top" }),
|
|
1537
|
+
/* @__PURE__ */ jsx6(Handle, { type: "source", position: "bottom" })
|
|
1538
|
+
]
|
|
1539
|
+
}
|
|
1540
|
+
);
|
|
1541
|
+
};
|
|
1542
|
+
var WaitNode = ({
|
|
1543
|
+
node
|
|
1544
|
+
}) => {
|
|
1545
|
+
const d = node.data ?? {};
|
|
1546
|
+
const waitMs = d.waitMs;
|
|
1547
|
+
const durationChip = waitMs ? `${Math.round(waitMs / 100) / 10}s` : void 0;
|
|
1548
|
+
return /* @__PURE__ */ jsxs4(
|
|
1549
|
+
BaseNode,
|
|
1550
|
+
{
|
|
1551
|
+
kind: d.kind ?? "WAIT",
|
|
1552
|
+
icon: d.icon,
|
|
1553
|
+
title: d.title ?? "Wait",
|
|
1554
|
+
chip: d.chip ?? durationChip,
|
|
1555
|
+
description: d.description ?? (durationChip ? "Pause execution" : void 0),
|
|
1556
|
+
status: d.status,
|
|
1557
|
+
accent: "violet",
|
|
1558
|
+
children: [
|
|
1559
|
+
/* @__PURE__ */ jsx6(Handle, { type: "target", position: "top" }),
|
|
1560
|
+
/* @__PURE__ */ jsx6(Handle, { type: "source", position: "bottom" })
|
|
1561
|
+
]
|
|
1562
|
+
}
|
|
1563
|
+
);
|
|
1564
|
+
};
|
|
1565
|
+
var ParallelNode = ({
|
|
1566
|
+
node
|
|
1567
|
+
}) => {
|
|
1568
|
+
const d = node.data ?? {};
|
|
1569
|
+
const branches = d.branches ?? [
|
|
1570
|
+
{ id: "a", label: "a" },
|
|
1571
|
+
{ id: "b", label: "b" }
|
|
1572
|
+
];
|
|
1573
|
+
return /* @__PURE__ */ jsxs4(
|
|
1574
|
+
BaseNode,
|
|
1575
|
+
{
|
|
1576
|
+
kind: d.kind ?? "PARALLEL",
|
|
1577
|
+
icon: d.icon,
|
|
1578
|
+
title: d.title ?? "Parallel",
|
|
1579
|
+
chip: d.chip ?? `${branches.length}\xD7`,
|
|
1580
|
+
description: d.description ?? "Fan-out branches",
|
|
1581
|
+
status: d.status,
|
|
1582
|
+
accent: "blue",
|
|
1583
|
+
children: [
|
|
1584
|
+
/* @__PURE__ */ jsx6(Handle, { type: "target", position: "top" }),
|
|
1585
|
+
branches.map((b, i) => /* @__PURE__ */ jsx6(
|
|
1586
|
+
Handle,
|
|
1587
|
+
{
|
|
1588
|
+
type: "source",
|
|
1589
|
+
position: "bottom",
|
|
1590
|
+
id: b.id,
|
|
1591
|
+
index: i,
|
|
1592
|
+
total: branches.length,
|
|
1593
|
+
label: b.label
|
|
1594
|
+
},
|
|
1595
|
+
b.id
|
|
1596
|
+
))
|
|
1597
|
+
]
|
|
1598
|
+
}
|
|
1599
|
+
);
|
|
1600
|
+
};
|
|
1601
|
+
var StickyNode = ({
|
|
1602
|
+
node
|
|
1603
|
+
}) => {
|
|
1604
|
+
const d = node.data ?? {};
|
|
1605
|
+
return /* @__PURE__ */ jsxs4(
|
|
1606
|
+
"div",
|
|
1607
|
+
{
|
|
1608
|
+
className: "ods-flow-sticky",
|
|
1609
|
+
style: {
|
|
1610
|
+
width: node.width ?? 240,
|
|
1611
|
+
minHeight: node.height ?? 120
|
|
1612
|
+
},
|
|
1613
|
+
children: [
|
|
1614
|
+
d.title && /* @__PURE__ */ jsx6("div", { className: "ods-flow-sticky__title", children: d.title }),
|
|
1615
|
+
d.description && /* @__PURE__ */ jsx6("div", { className: "ods-flow-sticky__body", children: d.description })
|
|
1616
|
+
]
|
|
1617
|
+
}
|
|
1618
|
+
);
|
|
1619
|
+
};
|
|
1620
|
+
var WebhookNode = ({
|
|
1621
|
+
node
|
|
1622
|
+
}) => {
|
|
1623
|
+
const d = node.data ?? {};
|
|
1624
|
+
return /* @__PURE__ */ jsx6(
|
|
1625
|
+
BaseNode,
|
|
1626
|
+
{
|
|
1627
|
+
kind: d.kind ?? "WEBHOOK",
|
|
1628
|
+
icon: d.icon,
|
|
1629
|
+
title: d.title ?? "Webhook",
|
|
1630
|
+
chip: d.chip ?? d.method ?? "POST",
|
|
1631
|
+
description: d.description ?? d.path ?? "/hooks/incoming",
|
|
1632
|
+
valueChip: d.valueChip,
|
|
1633
|
+
status: d.status,
|
|
1634
|
+
accent: "blue",
|
|
1635
|
+
children: /* @__PURE__ */ jsx6(Handle, { type: "source", position: "bottom" })
|
|
1636
|
+
}
|
|
1637
|
+
);
|
|
1638
|
+
};
|
|
1639
|
+
var HttpRequestNode = ({
|
|
1640
|
+
node
|
|
1641
|
+
}) => {
|
|
1642
|
+
const d = node.data ?? {};
|
|
1643
|
+
return /* @__PURE__ */ jsxs4(
|
|
1644
|
+
BaseNode,
|
|
1645
|
+
{
|
|
1646
|
+
kind: d.kind ?? "HTTP",
|
|
1647
|
+
icon: d.icon,
|
|
1648
|
+
title: d.title ?? "HTTP Request",
|
|
1649
|
+
chip: d.chip ?? d.method ?? "GET",
|
|
1650
|
+
description: d.description ?? d.url ?? "Call an API",
|
|
1651
|
+
valueChip: d.valueChip,
|
|
1652
|
+
status: d.status,
|
|
1653
|
+
accent: "blue",
|
|
1654
|
+
children: [
|
|
1655
|
+
/* @__PURE__ */ jsx6(Handle, { type: "target", position: "top" }),
|
|
1656
|
+
/* @__PURE__ */ jsx6(Handle, { type: "source", position: "bottom" })
|
|
1657
|
+
]
|
|
1658
|
+
}
|
|
1659
|
+
);
|
|
1660
|
+
};
|
|
1661
|
+
var DEFAULT_NODE_KINDS = {
|
|
1662
|
+
action: ActionNode,
|
|
1663
|
+
trigger: TriggerNode,
|
|
1664
|
+
condition: ConditionNode,
|
|
1665
|
+
group: GroupNode,
|
|
1666
|
+
forEach: ForEachNode,
|
|
1667
|
+
output: OutputNode,
|
|
1668
|
+
error: ErrorNode,
|
|
1669
|
+
wait: WaitNode,
|
|
1670
|
+
parallel: ParallelNode,
|
|
1671
|
+
sticky: StickyNode,
|
|
1672
|
+
webhook: WebhookNode,
|
|
1673
|
+
httpRequest: HttpRequestNode
|
|
1674
|
+
};
|
|
1675
|
+
|
|
1676
|
+
// src/workflow/components/FlowCanvas/FlowCanvas.tsx
|
|
1677
|
+
import {
|
|
1678
|
+
memo as memo2,
|
|
1679
|
+
useCallback,
|
|
1680
|
+
useEffect as useEffect4,
|
|
1681
|
+
useMemo as useMemo3,
|
|
1682
|
+
useRef as useRef5,
|
|
1683
|
+
useState as useState2
|
|
1684
|
+
} from "react";
|
|
1685
|
+
|
|
1686
|
+
// src/workflow/store/createFlowStore.ts
|
|
1687
|
+
var DEFAULT_VIEWPORT = { x: 0, y: 0, zoom: 1 };
|
|
1688
|
+
function createFlowStore({
|
|
1689
|
+
initialNodes = [],
|
|
1690
|
+
initialEdges = [],
|
|
1691
|
+
initialViewport = DEFAULT_VIEWPORT
|
|
1692
|
+
} = {}) {
|
|
1693
|
+
let snapshot = {
|
|
1694
|
+
nodes: initialNodes,
|
|
1695
|
+
edges: initialEdges,
|
|
1696
|
+
viewport: initialViewport,
|
|
1697
|
+
selectedNodeIds: EMPTY_SET,
|
|
1698
|
+
selectedEdgeIds: EMPTY_SET,
|
|
1699
|
+
connection: null
|
|
1700
|
+
};
|
|
1701
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1702
|
+
const emit = () => {
|
|
1703
|
+
for (const l of listeners) l();
|
|
1704
|
+
};
|
|
1705
|
+
const subscribe = (listener) => {
|
|
1706
|
+
listeners.add(listener);
|
|
1707
|
+
return () => {
|
|
1708
|
+
listeners.delete(listener);
|
|
1709
|
+
};
|
|
1710
|
+
};
|
|
1711
|
+
const replace = (patch) => {
|
|
1712
|
+
snapshot = { ...snapshot, ...patch };
|
|
1713
|
+
emit();
|
|
1714
|
+
};
|
|
1715
|
+
return {
|
|
1716
|
+
subscribe,
|
|
1717
|
+
getSnapshot: () => snapshot,
|
|
1718
|
+
setNodes(nodes) {
|
|
1719
|
+
if (nodes === snapshot.nodes) return;
|
|
1720
|
+
replace({ nodes });
|
|
1721
|
+
},
|
|
1722
|
+
setEdges(edges) {
|
|
1723
|
+
if (edges === snapshot.edges) return;
|
|
1724
|
+
replace({ edges });
|
|
1725
|
+
},
|
|
1726
|
+
setViewport(viewport) {
|
|
1727
|
+
if (viewport.x === snapshot.viewport.x && viewport.y === snapshot.viewport.y && viewport.zoom === snapshot.viewport.zoom) {
|
|
1728
|
+
return;
|
|
1729
|
+
}
|
|
1730
|
+
replace({ viewport });
|
|
1731
|
+
},
|
|
1732
|
+
setSelection(selectedNodeIds, selectedEdgeIds) {
|
|
1733
|
+
replace({ selectedNodeIds, selectedEdgeIds });
|
|
1734
|
+
},
|
|
1735
|
+
setConnection(connection) {
|
|
1736
|
+
if (connection === snapshot.connection) return;
|
|
1737
|
+
replace({ connection });
|
|
1738
|
+
}
|
|
1739
|
+
};
|
|
1740
|
+
}
|
|
1741
|
+
var EMPTY_SET = /* @__PURE__ */ new Set();
|
|
1742
|
+
|
|
1743
|
+
// src/workflow/utils/collision.ts
|
|
1744
|
+
function resolveNodeCollisions(node, proposed, others, opts) {
|
|
1745
|
+
const { gap, maxIterations = 8, exclude, scopeToSiblings = true } = opts;
|
|
1746
|
+
if (gap < 0 || others.length === 0) return proposed;
|
|
1747
|
+
const w = node.width ?? DEFAULT_NODE_WIDTH;
|
|
1748
|
+
const h = effectiveHeight(node);
|
|
1749
|
+
const parentId = node.parentId;
|
|
1750
|
+
const candidates = others.filter((o) => {
|
|
1751
|
+
if (o.id === node.id) return false;
|
|
1752
|
+
if (o.hidden) return false;
|
|
1753
|
+
if (exclude?.has(o.id)) return false;
|
|
1754
|
+
if (scopeToSiblings && (o.parentId ?? void 0) !== (parentId ?? void 0)) {
|
|
1755
|
+
return false;
|
|
1756
|
+
}
|
|
1757
|
+
return true;
|
|
1758
|
+
});
|
|
1759
|
+
if (candidates.length === 0) return proposed;
|
|
1760
|
+
let cur = proposed;
|
|
1761
|
+
for (let iter = 0; iter < maxIterations; iter++) {
|
|
1762
|
+
let bestOverlap = Number.POSITIVE_INFINITY;
|
|
1763
|
+
let bestPushX = 0;
|
|
1764
|
+
let bestPushY = 0;
|
|
1765
|
+
let hit = false;
|
|
1766
|
+
const aLeft = cur.x - gap;
|
|
1767
|
+
const aRight = cur.x + w + gap;
|
|
1768
|
+
const aTop = cur.y - gap;
|
|
1769
|
+
const aBottom = cur.y + h + gap;
|
|
1770
|
+
for (const o of candidates) {
|
|
1771
|
+
const ow = o.width ?? DEFAULT_NODE_WIDTH;
|
|
1772
|
+
const oh = effectiveHeight(o);
|
|
1773
|
+
const bLeft = o.position.x;
|
|
1774
|
+
const bRight = o.position.x + ow;
|
|
1775
|
+
const bTop = o.position.y;
|
|
1776
|
+
const bBottom = o.position.y + oh;
|
|
1777
|
+
const overlapX = Math.min(aRight, bRight) - Math.max(aLeft, bLeft);
|
|
1778
|
+
const overlapY = Math.min(aBottom, bBottom) - Math.max(aTop, bTop);
|
|
1779
|
+
if (overlapX <= 0 || overlapY <= 0) continue;
|
|
1780
|
+
let pushX = 0;
|
|
1781
|
+
let pushY = 0;
|
|
1782
|
+
let overlap;
|
|
1783
|
+
if (overlapX < overlapY) {
|
|
1784
|
+
const aCx = cur.x + w / 2;
|
|
1785
|
+
const bCx = o.position.x + ow / 2;
|
|
1786
|
+
pushX = aCx < bCx ? -overlapX : overlapX;
|
|
1787
|
+
overlap = overlapX;
|
|
1788
|
+
} else {
|
|
1789
|
+
const aCy = cur.y + h / 2;
|
|
1790
|
+
const bCy = o.position.y + oh / 2;
|
|
1791
|
+
pushY = aCy < bCy ? -overlapY : overlapY;
|
|
1792
|
+
overlap = overlapY;
|
|
1793
|
+
}
|
|
1794
|
+
if (overlap < bestOverlap) {
|
|
1795
|
+
bestOverlap = overlap;
|
|
1796
|
+
bestPushX = pushX;
|
|
1797
|
+
bestPushY = pushY;
|
|
1798
|
+
hit = true;
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
if (!hit) break;
|
|
1802
|
+
cur = { x: cur.x + bestPushX, y: cur.y + bestPushY };
|
|
1803
|
+
}
|
|
1804
|
+
return cur;
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
// src/workflow/components/FlowCanvas/FlowCanvas.tsx
|
|
1808
|
+
import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1809
|
+
var DEFAULT_VIEWPORT2 = { x: 0, y: 0, zoom: 1 };
|
|
1810
|
+
function FlowCanvas(props) {
|
|
1811
|
+
const viewportPropProvided = props.viewport !== void 0 || props.defaultViewport !== void 0;
|
|
1812
|
+
const {
|
|
1813
|
+
nodes,
|
|
1814
|
+
edges,
|
|
1815
|
+
onNodesChange,
|
|
1816
|
+
onEdgesChange,
|
|
1817
|
+
viewport: controlledViewport,
|
|
1818
|
+
defaultViewport = DEFAULT_VIEWPORT2,
|
|
1819
|
+
onViewportChange,
|
|
1820
|
+
minZoom = 0.25,
|
|
1821
|
+
maxZoom = 2,
|
|
1822
|
+
fitViewOnInit,
|
|
1823
|
+
nodeKinds,
|
|
1824
|
+
onConnect,
|
|
1825
|
+
onConnectStart,
|
|
1826
|
+
onConnectEnd,
|
|
1827
|
+
isValidConnection,
|
|
1828
|
+
onSelectionChange,
|
|
1829
|
+
onPaneClick,
|
|
1830
|
+
onNodeClick,
|
|
1831
|
+
onEdgeClick,
|
|
1832
|
+
onEdgeLabelChange,
|
|
1833
|
+
onInit,
|
|
1834
|
+
onBeforeDelete,
|
|
1835
|
+
onNodeContextMenu,
|
|
1836
|
+
onEdgeContextMenu,
|
|
1837
|
+
onPaneContextMenu,
|
|
1838
|
+
nodesDraggable = true,
|
|
1839
|
+
nodesConnectable = true,
|
|
1840
|
+
panOnDrag: panOnDragProp,
|
|
1841
|
+
zoomOnScroll: zoomOnScrollProp,
|
|
1842
|
+
preset = "mouse",
|
|
1843
|
+
paneClickDistance = 4,
|
|
1844
|
+
paneClickClearsSelection = true,
|
|
1845
|
+
background = "dots",
|
|
1846
|
+
gridSize = 20,
|
|
1847
|
+
snapToGrid = false,
|
|
1848
|
+
nodeCollisionGap = -1,
|
|
1849
|
+
subflowCollisionGap,
|
|
1850
|
+
height = "100%",
|
|
1851
|
+
width = "100%",
|
|
1852
|
+
className,
|
|
1853
|
+
style,
|
|
1854
|
+
children,
|
|
1855
|
+
emptyState
|
|
1856
|
+
} = props;
|
|
1857
|
+
const presetDefaults = {
|
|
1858
|
+
mouse: { panOnDrag: true, zoomOnScroll: true },
|
|
1859
|
+
trackpad: { panOnDrag: true, zoomOnScroll: true },
|
|
1860
|
+
touch: { panOnDrag: false, zoomOnScroll: true }
|
|
1861
|
+
};
|
|
1862
|
+
const panOnDrag = panOnDragProp ?? presetDefaults[preset].panOnDrag;
|
|
1863
|
+
const zoomOnScroll = zoomOnScrollProp ?? presetDefaults[preset].zoomOnScroll;
|
|
1864
|
+
const store = useState2(
|
|
1865
|
+
() => createFlowStore({
|
|
1866
|
+
initialNodes: nodes,
|
|
1867
|
+
initialEdges: edges,
|
|
1868
|
+
initialViewport: controlledViewport ?? defaultViewport
|
|
1869
|
+
})
|
|
1870
|
+
)[0];
|
|
1871
|
+
const handleRegistry = useState2(() => createHandleRegistry())[0];
|
|
1872
|
+
const [handleVersion, setHandleVersion] = useState2(0);
|
|
1873
|
+
useEffect4(() => {
|
|
1874
|
+
const unsub = handleRegistry.subscribe(() => {
|
|
1875
|
+
setHandleVersion((v) => v + 1);
|
|
1876
|
+
});
|
|
1877
|
+
return unsub;
|
|
1878
|
+
}, [handleRegistry]);
|
|
1879
|
+
const kinds = useMemo3(() => buildNodeKindRegistry(DEFAULT_NODE_KINDS, nodeKinds), [nodeKinds]);
|
|
1880
|
+
const containerRef = useRef5(null);
|
|
1881
|
+
useEffect4(() => store.setNodes(nodes), [store, nodes]);
|
|
1882
|
+
useEffect4(() => store.setEdges(edges), [store, edges]);
|
|
1883
|
+
const [uncontrolledVp, setUncontrolledVp] = useState2(controlledViewport ?? defaultViewport);
|
|
1884
|
+
const viewport = controlledViewport ?? uncontrolledVp;
|
|
1885
|
+
useEffect4(() => store.setViewport(viewport), [store, viewport]);
|
|
1886
|
+
const setViewport = useCallback(
|
|
1887
|
+
(next) => {
|
|
1888
|
+
if (controlledViewport === void 0) setUncontrolledVp(next);
|
|
1889
|
+
onViewportChange?.(next);
|
|
1890
|
+
},
|
|
1891
|
+
[controlledViewport, onViewportChange]
|
|
1892
|
+
);
|
|
1893
|
+
const selectedNodeIds = useMemo3(() => {
|
|
1894
|
+
const s = /* @__PURE__ */ new Set();
|
|
1895
|
+
for (const n of nodes) if (n.selected) s.add(n.id);
|
|
1896
|
+
return s;
|
|
1897
|
+
}, [nodes]);
|
|
1898
|
+
const selectedEdgeIds = useMemo3(() => {
|
|
1899
|
+
const s = /* @__PURE__ */ new Set();
|
|
1900
|
+
for (const e of edges) if (e.selected) s.add(e.id);
|
|
1901
|
+
return s;
|
|
1902
|
+
}, [edges]);
|
|
1903
|
+
useEffect4(() => {
|
|
1904
|
+
store.setSelection(selectedNodeIds, selectedEdgeIds);
|
|
1905
|
+
if (onSelectionChange) {
|
|
1906
|
+
onSelectionChange({
|
|
1907
|
+
nodes: nodes.filter((n) => selectedNodeIds.has(n.id)),
|
|
1908
|
+
edges: edges.filter((e) => selectedEdgeIds.has(e.id))
|
|
1909
|
+
});
|
|
1910
|
+
}
|
|
1911
|
+
}, [store, selectedNodeIds, selectedEdgeIds, nodes, edges, onSelectionChange]);
|
|
1912
|
+
const selectNode = useCallback(
|
|
1913
|
+
(id, additive) => {
|
|
1914
|
+
const next = [];
|
|
1915
|
+
const nextEdges = [];
|
|
1916
|
+
if (!additive) {
|
|
1917
|
+
for (const n of nodes) {
|
|
1918
|
+
if (n.selected && n.id !== id) next.push(change.node.select(n.id, false));
|
|
1919
|
+
}
|
|
1920
|
+
for (const e of edges) {
|
|
1921
|
+
if (e.selected) nextEdges.push(change.edge.select(e.id, false));
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
const isSelected = selectedNodeIds.has(id);
|
|
1925
|
+
if (additive && isSelected) {
|
|
1926
|
+
next.push(change.node.select(id, false));
|
|
1927
|
+
} else if (!isSelected) {
|
|
1928
|
+
next.push(change.node.select(id, true));
|
|
1929
|
+
}
|
|
1930
|
+
if (next.length) onNodesChange?.(next);
|
|
1931
|
+
if (nextEdges.length) onEdgesChange?.(nextEdges);
|
|
1932
|
+
},
|
|
1933
|
+
[nodes, edges, selectedNodeIds, onNodesChange, onEdgesChange]
|
|
1934
|
+
);
|
|
1935
|
+
const notifyNodeClick = useCallback(
|
|
1936
|
+
(id) => {
|
|
1937
|
+
const node = nodes.find((n) => n.id === id);
|
|
1938
|
+
if (node) onNodeClick?.(node);
|
|
1939
|
+
},
|
|
1940
|
+
[nodes, onNodeClick]
|
|
1941
|
+
);
|
|
1942
|
+
const selectEdge = useCallback(
|
|
1943
|
+
(id, additive) => {
|
|
1944
|
+
const next = [];
|
|
1945
|
+
const nextNodes = [];
|
|
1946
|
+
if (!additive) {
|
|
1947
|
+
for (const e of edges) {
|
|
1948
|
+
if (e.selected && e.id !== id) next.push(change.edge.select(e.id, false));
|
|
1949
|
+
}
|
|
1950
|
+
for (const n of nodes) {
|
|
1951
|
+
if (n.selected) nextNodes.push(change.node.select(n.id, false));
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
const isSelected = selectedEdgeIds.has(id);
|
|
1955
|
+
if (additive && isSelected) {
|
|
1956
|
+
next.push(change.edge.select(id, false));
|
|
1957
|
+
} else if (!isSelected) {
|
|
1958
|
+
next.push(change.edge.select(id, true));
|
|
1959
|
+
}
|
|
1960
|
+
if (next.length) onEdgesChange?.(next);
|
|
1961
|
+
if (nextNodes.length) onNodesChange?.(nextNodes);
|
|
1962
|
+
const edge = edges.find((e) => e.id === id);
|
|
1963
|
+
if (edge) onEdgeClick?.(edge);
|
|
1964
|
+
},
|
|
1965
|
+
[nodes, edges, selectedEdgeIds, onEdgesChange, onNodesChange, onEdgeClick]
|
|
1966
|
+
);
|
|
1967
|
+
const clearSelection = useCallback(() => {
|
|
1968
|
+
const ns = [];
|
|
1969
|
+
const es = [];
|
|
1970
|
+
for (const n of nodes) if (n.selected) ns.push(change.node.select(n.id, false));
|
|
1971
|
+
for (const e of edges) if (e.selected) es.push(change.edge.select(e.id, false));
|
|
1972
|
+
if (ns.length) onNodesChange?.(ns);
|
|
1973
|
+
if (es.length) onEdgesChange?.(es);
|
|
1974
|
+
}, [nodes, edges, onNodesChange, onEdgesChange]);
|
|
1975
|
+
const dragRef = useRef5(null);
|
|
1976
|
+
const [draggingId, setDraggingId] = useState2(null);
|
|
1977
|
+
const beginNodeDrag = useCallback(
|
|
1978
|
+
(nodeId, pointerId, clientX, clientY, altKey = false) => {
|
|
1979
|
+
if (!nodesDraggable) return;
|
|
1980
|
+
const node = nodes.find((n) => n.id === nodeId);
|
|
1981
|
+
if (!node) return;
|
|
1982
|
+
const kids = descendantsOf(nodeId, nodes).map((d) => ({
|
|
1983
|
+
id: d.id,
|
|
1984
|
+
startPosition: d.position
|
|
1985
|
+
}));
|
|
1986
|
+
dragRef.current = {
|
|
1987
|
+
pointerId,
|
|
1988
|
+
nodeId,
|
|
1989
|
+
startClientX: clientX,
|
|
1990
|
+
startClientY: clientY,
|
|
1991
|
+
startPosition: node.position,
|
|
1992
|
+
descendants: kids,
|
|
1993
|
+
altDetach: altKey && !!node.parentId,
|
|
1994
|
+
rafScheduled: false,
|
|
1995
|
+
nextDelta: null
|
|
1996
|
+
};
|
|
1997
|
+
setDraggingId(nodeId);
|
|
1998
|
+
selectNode(nodeId, false);
|
|
1999
|
+
},
|
|
2000
|
+
[nodes, nodesDraggable, selectNode]
|
|
2001
|
+
);
|
|
2002
|
+
const [conn, setConn] = useState2(null);
|
|
2003
|
+
const connRef = useRef5(null);
|
|
2004
|
+
useEffect4(() => {
|
|
2005
|
+
connRef.current = conn;
|
|
2006
|
+
}, [conn]);
|
|
2007
|
+
const beginConnection = useCallback(
|
|
2008
|
+
(nodeId, handleId, handleType, pointerId, clientX, clientY) => {
|
|
2009
|
+
if (!nodesConnectable) return;
|
|
2010
|
+
const node = nodes.find((n) => n.id === nodeId);
|
|
2011
|
+
if (!node) return;
|
|
2012
|
+
const desc = handleRegistry.resolve(nodeId, handleType, handleId);
|
|
2013
|
+
const side = desc?.side ?? (handleType === "source" ? "bottom" : "top");
|
|
2014
|
+
const index = desc?.index ?? 0;
|
|
2015
|
+
const total = desc?.total ?? 1;
|
|
2016
|
+
const ratio = (index + 1) / (total + 1);
|
|
2017
|
+
const w = node.width ?? 368;
|
|
2018
|
+
const h = effectiveHeight(node);
|
|
2019
|
+
const start = (() => {
|
|
2020
|
+
switch (side) {
|
|
2021
|
+
case "top":
|
|
2022
|
+
return { x: node.position.x + w * ratio, y: node.position.y };
|
|
2023
|
+
case "bottom":
|
|
2024
|
+
return { x: node.position.x + w * ratio, y: node.position.y + h };
|
|
2025
|
+
case "left":
|
|
2026
|
+
return { x: node.position.x, y: node.position.y + h * ratio };
|
|
2027
|
+
case "right":
|
|
2028
|
+
return { x: node.position.x + w, y: node.position.y + h * ratio };
|
|
2029
|
+
}
|
|
2030
|
+
})();
|
|
2031
|
+
const from = { nodeId, handleId, handleType };
|
|
2032
|
+
const rect = containerRef.current?.getBoundingClientRect();
|
|
2033
|
+
const end = rect ? screenToFlow({ x: clientX - rect.left, y: clientY - rect.top }, viewport) : start;
|
|
2034
|
+
setConn({ pointerId, from, start, end });
|
|
2035
|
+
store.setConnection(from);
|
|
2036
|
+
onConnectStart?.({ clientX, clientY, pointerId }, from);
|
|
2037
|
+
},
|
|
2038
|
+
[nodes, nodesConnectable, handleRegistry, viewport, store, onConnectStart]
|
|
2039
|
+
);
|
|
2040
|
+
const viewportRef = useRef5(viewport);
|
|
2041
|
+
const nodesRef = useRef5(nodes);
|
|
2042
|
+
const edgesRef = useRef5(edges);
|
|
2043
|
+
const onNodesChangeRefForInstance = useRef5(onNodesChange);
|
|
2044
|
+
const onEdgesChangeRefForInstance = useRef5(onEdgesChange);
|
|
2045
|
+
const onBeforeDeleteRef = useRef5(onBeforeDelete);
|
|
2046
|
+
const snapToGridRef = useRef5(snapToGrid);
|
|
2047
|
+
const gridSizeRef = useRef5(gridSize);
|
|
2048
|
+
const nodeCollisionGapRef = useRef5(nodeCollisionGap);
|
|
2049
|
+
const subflowCollisionGapRef = useRef5(subflowCollisionGap ?? nodeCollisionGap);
|
|
2050
|
+
useEffect4(() => {
|
|
2051
|
+
edgesRef.current = edges;
|
|
2052
|
+
}, [edges]);
|
|
2053
|
+
useEffect4(() => {
|
|
2054
|
+
onNodesChangeRefForInstance.current = onNodesChange;
|
|
2055
|
+
}, [onNodesChange]);
|
|
2056
|
+
useEffect4(() => {
|
|
2057
|
+
onEdgesChangeRefForInstance.current = onEdgesChange;
|
|
2058
|
+
}, [onEdgesChange]);
|
|
2059
|
+
useEffect4(() => {
|
|
2060
|
+
onBeforeDeleteRef.current = onBeforeDelete;
|
|
2061
|
+
}, [onBeforeDelete]);
|
|
2062
|
+
useEffect4(() => {
|
|
2063
|
+
snapToGridRef.current = snapToGrid;
|
|
2064
|
+
}, [snapToGrid]);
|
|
2065
|
+
useEffect4(() => {
|
|
2066
|
+
gridSizeRef.current = gridSize;
|
|
2067
|
+
}, [gridSize]);
|
|
2068
|
+
useEffect4(() => {
|
|
2069
|
+
nodeCollisionGapRef.current = nodeCollisionGap;
|
|
2070
|
+
}, [nodeCollisionGap]);
|
|
2071
|
+
useEffect4(() => {
|
|
2072
|
+
subflowCollisionGapRef.current = subflowCollisionGap ?? nodeCollisionGap;
|
|
2073
|
+
}, [subflowCollisionGap, nodeCollisionGap]);
|
|
2074
|
+
const onNodesChangeRef = useRef5(onNodesChange);
|
|
2075
|
+
const onConnectRef = useRef5(onConnect);
|
|
2076
|
+
const onConnectEndRef = useRef5(onConnectEnd);
|
|
2077
|
+
const isValidConnectionRef = useRef5(isValidConnection);
|
|
2078
|
+
useEffect4(() => {
|
|
2079
|
+
viewportRef.current = viewport;
|
|
2080
|
+
}, [viewport]);
|
|
2081
|
+
useEffect4(() => {
|
|
2082
|
+
nodesRef.current = nodes;
|
|
2083
|
+
}, [nodes]);
|
|
2084
|
+
useEffect4(() => {
|
|
2085
|
+
onNodesChangeRef.current = onNodesChange;
|
|
2086
|
+
}, [onNodesChange]);
|
|
2087
|
+
useEffect4(() => {
|
|
2088
|
+
onConnectRef.current = onConnect;
|
|
2089
|
+
}, [onConnect]);
|
|
2090
|
+
useEffect4(() => {
|
|
2091
|
+
onConnectEndRef.current = onConnectEnd;
|
|
2092
|
+
}, [onConnectEnd]);
|
|
2093
|
+
useEffect4(() => {
|
|
2094
|
+
isValidConnectionRef.current = isValidConnection;
|
|
2095
|
+
}, [isValidConnection]);
|
|
2096
|
+
useEffect4(() => {
|
|
2097
|
+
const onPointerMove = (e) => {
|
|
2098
|
+
const vp = viewportRef.current;
|
|
2099
|
+
const drag = dragRef.current;
|
|
2100
|
+
if (drag && drag.pointerId === e.pointerId) {
|
|
2101
|
+
const dx = (e.clientX - drag.startClientX) / vp.zoom;
|
|
2102
|
+
const dy = (e.clientY - drag.startClientY) / vp.zoom;
|
|
2103
|
+
drag.nextDelta = { dx, dy };
|
|
2104
|
+
if (!drag.rafScheduled) {
|
|
2105
|
+
drag.rafScheduled = true;
|
|
2106
|
+
requestAnimationFrame(() => {
|
|
2107
|
+
const d = dragRef.current;
|
|
2108
|
+
if (!d) return;
|
|
2109
|
+
d.rafScheduled = false;
|
|
2110
|
+
const delta = d.nextDelta;
|
|
2111
|
+
if (!delta) return;
|
|
2112
|
+
const dragNode = nodesRef.current.find((n) => n.id === d.nodeId);
|
|
2113
|
+
if (!dragNode) return;
|
|
2114
|
+
const proposed = {
|
|
2115
|
+
x: d.startPosition.x + delta.dx,
|
|
2116
|
+
y: d.startPosition.y + delta.dy
|
|
2117
|
+
};
|
|
2118
|
+
const clamped = d.altDetach ? proposed : clampToParentExtent(dragNode, proposed, nodesRef.current);
|
|
2119
|
+
const isContainer = dragNode.type === "group" || dragNode.type === "forEach";
|
|
2120
|
+
const gap = isContainer ? subflowCollisionGapRef.current : nodeCollisionGapRef.current;
|
|
2121
|
+
const excludeIds = /* @__PURE__ */ new Set([d.nodeId, ...d.descendants.map((kid) => kid.id)]);
|
|
2122
|
+
const finalPos = resolveNodeCollisions(dragNode, clamped, nodesRef.current, {
|
|
2123
|
+
gap,
|
|
2124
|
+
exclude: excludeIds
|
|
2125
|
+
});
|
|
2126
|
+
const realDx = finalPos.x - d.startPosition.x;
|
|
2127
|
+
const realDy = finalPos.y - d.startPosition.y;
|
|
2128
|
+
const changes = [change.node.position(d.nodeId, finalPos, true)];
|
|
2129
|
+
for (const kid of d.descendants) {
|
|
2130
|
+
changes.push(
|
|
2131
|
+
change.node.position(
|
|
2132
|
+
kid.id,
|
|
2133
|
+
{ x: kid.startPosition.x + realDx, y: kid.startPosition.y + realDy },
|
|
2134
|
+
true
|
|
2135
|
+
)
|
|
2136
|
+
);
|
|
2137
|
+
}
|
|
2138
|
+
onNodesChangeRef.current?.(changes);
|
|
2139
|
+
});
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
const c = connRef.current;
|
|
2143
|
+
if (c && c.pointerId === e.pointerId) {
|
|
2144
|
+
const rect = containerRef.current?.getBoundingClientRect();
|
|
2145
|
+
if (rect) {
|
|
2146
|
+
const end = screenToFlow({ x: e.clientX - rect.left, y: e.clientY - rect.top }, vp);
|
|
2147
|
+
setConn({ ...c, end });
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
};
|
|
2151
|
+
const onPointerUp = (e) => {
|
|
2152
|
+
const vp = viewportRef.current;
|
|
2153
|
+
const drag = dragRef.current;
|
|
2154
|
+
if (drag && drag.pointerId === e.pointerId) {
|
|
2155
|
+
const dx = (e.clientX - drag.startClientX) / vp.zoom;
|
|
2156
|
+
const dy = (e.clientY - drag.startClientY) / vp.zoom;
|
|
2157
|
+
const dragNode = nodesRef.current.find((n) => n.id === drag.nodeId);
|
|
2158
|
+
if (dragNode) {
|
|
2159
|
+
let proposed = {
|
|
2160
|
+
x: drag.startPosition.x + dx,
|
|
2161
|
+
y: drag.startPosition.y + dy
|
|
2162
|
+
};
|
|
2163
|
+
if (snapToGridRef.current) {
|
|
2164
|
+
const g = gridSizeRef.current;
|
|
2165
|
+
proposed = {
|
|
2166
|
+
x: Math.round(proposed.x / g) * g,
|
|
2167
|
+
y: Math.round(proposed.y / g) * g
|
|
2168
|
+
};
|
|
2169
|
+
}
|
|
2170
|
+
const clamped = drag.altDetach ? proposed : clampToParentExtent(dragNode, proposed, nodesRef.current);
|
|
2171
|
+
const isContainer = dragNode.type === "group" || dragNode.type === "forEach";
|
|
2172
|
+
const gap = isContainer ? subflowCollisionGapRef.current : nodeCollisionGapRef.current;
|
|
2173
|
+
const excludeIds = /* @__PURE__ */ new Set([
|
|
2174
|
+
drag.nodeId,
|
|
2175
|
+
...drag.descendants.map((kid) => kid.id)
|
|
2176
|
+
]);
|
|
2177
|
+
const finalPos = resolveNodeCollisions(dragNode, clamped, nodesRef.current, {
|
|
2178
|
+
gap,
|
|
2179
|
+
exclude: excludeIds
|
|
2180
|
+
});
|
|
2181
|
+
const realDx = finalPos.x - drag.startPosition.x;
|
|
2182
|
+
const realDy = finalPos.y - drag.startPosition.y;
|
|
2183
|
+
const changes = [change.node.position(drag.nodeId, finalPos, false)];
|
|
2184
|
+
for (const kid of drag.descendants) {
|
|
2185
|
+
changes.push(
|
|
2186
|
+
change.node.position(
|
|
2187
|
+
kid.id,
|
|
2188
|
+
{ x: kid.startPosition.x + realDx, y: kid.startPosition.y + realDy },
|
|
2189
|
+
false
|
|
2190
|
+
)
|
|
2191
|
+
);
|
|
2192
|
+
}
|
|
2193
|
+
if (drag.altDetach && dragNode.parentId) {
|
|
2194
|
+
const targetGroup = findContainingGroup(
|
|
2195
|
+
{
|
|
2196
|
+
x: finalPos.x + (dragNode.width ?? 0) / 2,
|
|
2197
|
+
y: finalPos.y + (dragNode.height ?? 0) / 2
|
|
2198
|
+
},
|
|
2199
|
+
nodesRef.current,
|
|
2200
|
+
[drag.nodeId, ...drag.descendants.map((d) => d.id)]
|
|
2201
|
+
);
|
|
2202
|
+
const nextParentId = targetGroup?.id;
|
|
2203
|
+
const updated = {
|
|
2204
|
+
...dragNode,
|
|
2205
|
+
position: finalPos,
|
|
2206
|
+
parentId: nextParentId,
|
|
2207
|
+
// Preserve extent only when staying in a group.
|
|
2208
|
+
extent: nextParentId ? dragNode.extent : void 0
|
|
2209
|
+
};
|
|
2210
|
+
changes.push(change.node.replace(drag.nodeId, updated));
|
|
2211
|
+
}
|
|
2212
|
+
onNodesChangeRef.current?.(changes);
|
|
2213
|
+
}
|
|
2214
|
+
dragRef.current = null;
|
|
2215
|
+
setDraggingId(null);
|
|
2216
|
+
}
|
|
2217
|
+
const c = connRef.current;
|
|
2218
|
+
if (c && c.pointerId === e.pointerId) {
|
|
2219
|
+
const target = e.target;
|
|
2220
|
+
const handleEl = target?.closest("[data-handle-id]");
|
|
2221
|
+
let connection = null;
|
|
2222
|
+
let connectedTo;
|
|
2223
|
+
if (handleEl) {
|
|
2224
|
+
const targetNodeId = handleEl.dataset.handleNodeId;
|
|
2225
|
+
const targetHandleId = handleEl.dataset.handleId;
|
|
2226
|
+
const targetType = handleEl.dataset.handleType;
|
|
2227
|
+
const connectableEnd = handleEl.dataset.handleConnectableEnd === "true";
|
|
2228
|
+
if (connectableEnd && (targetNodeId !== c.from.nodeId || targetHandleId !== c.from.handleId) && targetType !== c.from.handleType) {
|
|
2229
|
+
const source = c.from.handleType === "source" ? c.from : { nodeId: targetNodeId, handleId: targetHandleId, handleType: "source" };
|
|
2230
|
+
const target2 = c.from.handleType === "target" ? c.from : { nodeId: targetNodeId, handleId: targetHandleId, handleType: "target" };
|
|
2231
|
+
connection = {
|
|
2232
|
+
source: source.nodeId,
|
|
2233
|
+
sourceHandle: source.handleId,
|
|
2234
|
+
target: target2.nodeId,
|
|
2235
|
+
targetHandle: target2.handleId
|
|
2236
|
+
};
|
|
2237
|
+
connectedTo = {
|
|
2238
|
+
nodeId: targetNodeId,
|
|
2239
|
+
handleId: targetHandleId,
|
|
2240
|
+
handleType: targetType
|
|
2241
|
+
};
|
|
2242
|
+
const validator = isValidConnectionRef.current;
|
|
2243
|
+
if (validator && !validator(connection)) {
|
|
2244
|
+
connection = null;
|
|
2245
|
+
connectedTo = void 0;
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
const rect = containerRef.current?.getBoundingClientRect();
|
|
2250
|
+
const flowPos = rect ? screenToFlow({ x: e.clientX - rect.left, y: e.clientY - rect.top }, vp) : { x: 0, y: 0 };
|
|
2251
|
+
const endState = {
|
|
2252
|
+
cancelled: !connection,
|
|
2253
|
+
position: { x: e.clientX, y: e.clientY },
|
|
2254
|
+
flowPosition: flowPos,
|
|
2255
|
+
from: c.from,
|
|
2256
|
+
to: connectedTo
|
|
2257
|
+
};
|
|
2258
|
+
if (connection) onConnectRef.current?.(connection);
|
|
2259
|
+
onConnectEndRef.current?.(e, endState);
|
|
2260
|
+
setConn(null);
|
|
2261
|
+
store.setConnection(null);
|
|
2262
|
+
}
|
|
2263
|
+
};
|
|
2264
|
+
const onPointerCancel = () => {
|
|
2265
|
+
if (dragRef.current) {
|
|
2266
|
+
dragRef.current = null;
|
|
2267
|
+
setDraggingId(null);
|
|
2268
|
+
}
|
|
2269
|
+
if (connRef.current) {
|
|
2270
|
+
setConn(null);
|
|
2271
|
+
store.setConnection(null);
|
|
2272
|
+
}
|
|
2273
|
+
};
|
|
2274
|
+
window.addEventListener("pointermove", onPointerMove);
|
|
2275
|
+
window.addEventListener("pointerup", onPointerUp);
|
|
2276
|
+
window.addEventListener("pointercancel", onPointerCancel);
|
|
2277
|
+
return () => {
|
|
2278
|
+
window.removeEventListener("pointermove", onPointerMove);
|
|
2279
|
+
window.removeEventListener("pointerup", onPointerUp);
|
|
2280
|
+
window.removeEventListener("pointercancel", onPointerCancel);
|
|
2281
|
+
};
|
|
2282
|
+
}, [store]);
|
|
2283
|
+
const panRef = useRef5(null);
|
|
2284
|
+
const onCanvasPointerDown = (e) => {
|
|
2285
|
+
if (e.button !== 0) return;
|
|
2286
|
+
const t = e.target;
|
|
2287
|
+
if (t.closest("[data-node-id]") || t.closest("[data-edge-id]") || t.closest("[data-handle-id]")) {
|
|
2288
|
+
return;
|
|
2289
|
+
}
|
|
2290
|
+
if (!panOnDrag) {
|
|
2291
|
+
panRef.current = {
|
|
2292
|
+
pointerId: e.pointerId,
|
|
2293
|
+
startClientX: e.clientX,
|
|
2294
|
+
startClientY: e.clientY,
|
|
2295
|
+
startVp: viewport,
|
|
2296
|
+
moved: false
|
|
2297
|
+
};
|
|
2298
|
+
return;
|
|
2299
|
+
}
|
|
2300
|
+
panRef.current = {
|
|
2301
|
+
pointerId: e.pointerId,
|
|
2302
|
+
startClientX: e.clientX,
|
|
2303
|
+
startClientY: e.clientY,
|
|
2304
|
+
startVp: viewport,
|
|
2305
|
+
moved: false
|
|
2306
|
+
};
|
|
2307
|
+
};
|
|
2308
|
+
useEffect4(() => {
|
|
2309
|
+
const onMove = (e) => {
|
|
2310
|
+
const pan = panRef.current;
|
|
2311
|
+
if (!pan || pan.pointerId !== e.pointerId) return;
|
|
2312
|
+
const dx = e.clientX - pan.startClientX;
|
|
2313
|
+
const dy = e.clientY - pan.startClientY;
|
|
2314
|
+
if (Math.abs(dx) > paneClickDistance || Math.abs(dy) > paneClickDistance) {
|
|
2315
|
+
if (!pan.moved) setPanGesture(true);
|
|
2316
|
+
pan.moved = true;
|
|
2317
|
+
}
|
|
2318
|
+
if (pan.moved && panOnDrag) {
|
|
2319
|
+
setViewport({ ...pan.startVp, x: pan.startVp.x + dx, y: pan.startVp.y + dy });
|
|
2320
|
+
}
|
|
2321
|
+
};
|
|
2322
|
+
const onUp = () => {
|
|
2323
|
+
const pan = panRef.current;
|
|
2324
|
+
if (!pan) return;
|
|
2325
|
+
if (!pan.moved) {
|
|
2326
|
+
onPaneClick?.();
|
|
2327
|
+
if (paneClickClearsSelection) clearSelection();
|
|
2328
|
+
}
|
|
2329
|
+
panRef.current = null;
|
|
2330
|
+
setPanGesture(false);
|
|
2331
|
+
};
|
|
2332
|
+
window.addEventListener("pointermove", onMove);
|
|
2333
|
+
window.addEventListener("pointerup", onUp);
|
|
2334
|
+
window.addEventListener("pointercancel", onUp);
|
|
2335
|
+
return () => {
|
|
2336
|
+
window.removeEventListener("pointermove", onMove);
|
|
2337
|
+
window.removeEventListener("pointerup", onUp);
|
|
2338
|
+
window.removeEventListener("pointercancel", onUp);
|
|
2339
|
+
};
|
|
2340
|
+
}, [
|
|
2341
|
+
setViewport,
|
|
2342
|
+
paneClickDistance,
|
|
2343
|
+
panOnDrag,
|
|
2344
|
+
onPaneClick,
|
|
2345
|
+
clearSelection,
|
|
2346
|
+
paneClickClearsSelection
|
|
2347
|
+
]);
|
|
2348
|
+
const handleWheel = (e) => {
|
|
2349
|
+
if (!zoomOnScroll) return;
|
|
2350
|
+
e.preventDefault();
|
|
2351
|
+
const rect = containerRef.current?.getBoundingClientRect();
|
|
2352
|
+
if (!rect) return;
|
|
2353
|
+
const px = e.clientX - rect.left;
|
|
2354
|
+
const py = e.clientY - rect.top;
|
|
2355
|
+
const factor = Math.exp(-e.deltaY * 1e-3);
|
|
2356
|
+
const nextZoom = Math.max(minZoom, Math.min(maxZoom, viewport.zoom * factor));
|
|
2357
|
+
const k = nextZoom / viewport.zoom;
|
|
2358
|
+
setViewport({
|
|
2359
|
+
zoom: nextZoom,
|
|
2360
|
+
x: px - (px - viewport.x) * k,
|
|
2361
|
+
y: py - (py - viewport.y) * k
|
|
2362
|
+
});
|
|
2363
|
+
};
|
|
2364
|
+
const dispatch = useCallback(
|
|
2365
|
+
(a) => {
|
|
2366
|
+
if (a.type === "connection/start") {
|
|
2367
|
+
beginConnection(a.nodeId, a.handleId, a.handleType, a.pointerId, a.clientX, a.clientY);
|
|
2368
|
+
}
|
|
2369
|
+
},
|
|
2370
|
+
[beginConnection]
|
|
2371
|
+
);
|
|
2372
|
+
const reportDimensions = useCallback(
|
|
2373
|
+
(nodeId, width2, height2) => {
|
|
2374
|
+
const node = nodes.find((n) => n.id === nodeId);
|
|
2375
|
+
if (!node) return;
|
|
2376
|
+
if (node.width === width2 && node.height === height2) return;
|
|
2377
|
+
onNodesChange?.([change.node.dimensions(nodeId, { width: width2, height: height2 })]);
|
|
2378
|
+
},
|
|
2379
|
+
[nodes, onNodesChange]
|
|
2380
|
+
);
|
|
2381
|
+
const toggleNodeCollapseImpl = useCallback(
|
|
2382
|
+
(nodeId) => {
|
|
2383
|
+
const node = nodes.find((n) => n.id === nodeId);
|
|
2384
|
+
if (!node) return;
|
|
2385
|
+
const prevData = node.data ?? {};
|
|
2386
|
+
const nextNode = {
|
|
2387
|
+
...node,
|
|
2388
|
+
data: { ...prevData, collapsed: !prevData.collapsed }
|
|
2389
|
+
};
|
|
2390
|
+
onNodesChange?.([change.node.replace(nodeId, nextNode)]);
|
|
2391
|
+
},
|
|
2392
|
+
[nodes, onNodesChange]
|
|
2393
|
+
);
|
|
2394
|
+
const deleteNodeImpl = useCallback(
|
|
2395
|
+
(nodeId) => {
|
|
2396
|
+
const incidentEdgeIds = edgesRef.current.filter((e) => e.source === nodeId || e.target === nodeId).map((e) => e.id);
|
|
2397
|
+
if (incidentEdgeIds.length > 0) {
|
|
2398
|
+
onEdgesChange?.(incidentEdgeIds.map((id) => change.edge.remove(id)));
|
|
2399
|
+
}
|
|
2400
|
+
onNodesChange?.([change.node.remove(nodeId)]);
|
|
2401
|
+
},
|
|
2402
|
+
[onNodesChange, onEdgesChange]
|
|
2403
|
+
);
|
|
2404
|
+
const instance = useMemo3(
|
|
2405
|
+
() => ({
|
|
2406
|
+
// viewport
|
|
2407
|
+
getViewport: () => viewportRef.current,
|
|
2408
|
+
setViewport: (vp) => setViewport(vp),
|
|
2409
|
+
setCenter: (x, y, opts) => {
|
|
2410
|
+
const z = opts?.zoom ?? viewportRef.current.zoom;
|
|
2411
|
+
const rect = containerRef.current?.getBoundingClientRect();
|
|
2412
|
+
if (!rect) return;
|
|
2413
|
+
setViewport({
|
|
2414
|
+
zoom: z,
|
|
2415
|
+
x: rect.width / 2 - x * z,
|
|
2416
|
+
y: rect.height / 2 - y * z
|
|
2417
|
+
});
|
|
2418
|
+
},
|
|
2419
|
+
fitView: async (opts) => {
|
|
2420
|
+
const rect = containerRef.current?.getBoundingClientRect();
|
|
2421
|
+
if (!rect) return false;
|
|
2422
|
+
const padding = opts?.padding ?? 80;
|
|
2423
|
+
const targetNodes = opts?.nodes ? nodesRef.current.filter((n) => opts.nodes.some((x) => x.id === n.id)) : nodesRef.current.filter((n) => !n.hidden);
|
|
2424
|
+
if (targetNodes.length === 0) return false;
|
|
2425
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
2426
|
+
let minY = Number.POSITIVE_INFINITY;
|
|
2427
|
+
let maxX = Number.NEGATIVE_INFINITY;
|
|
2428
|
+
let maxY = Number.NEGATIVE_INFINITY;
|
|
2429
|
+
for (const n of targetNodes) {
|
|
2430
|
+
const w = n.width ?? DEFAULT_NODE_WIDTH;
|
|
2431
|
+
const h = effectiveHeight(n);
|
|
2432
|
+
if (n.position.x < minX) minX = n.position.x;
|
|
2433
|
+
if (n.position.y < minY) minY = n.position.y;
|
|
2434
|
+
if (n.position.x + w > maxX) maxX = n.position.x + w;
|
|
2435
|
+
if (n.position.y + h > maxY) maxY = n.position.y + h;
|
|
2436
|
+
}
|
|
2437
|
+
const cw = maxX - minX;
|
|
2438
|
+
const ch = maxY - minY;
|
|
2439
|
+
const zx = (rect.width - padding * 2) / cw;
|
|
2440
|
+
const zy = (rect.height - padding * 2) / ch;
|
|
2441
|
+
const zoom = Math.max(
|
|
2442
|
+
opts?.minZoom ?? 0.25,
|
|
2443
|
+
Math.min(opts?.maxZoom ?? 1.5, Math.min(zx, zy))
|
|
2444
|
+
);
|
|
2445
|
+
setViewport({
|
|
2446
|
+
zoom,
|
|
2447
|
+
x: rect.width / 2 - (minX + maxX) / 2 * zoom,
|
|
2448
|
+
y: rect.height / 2 - (minY + maxY) / 2 * zoom
|
|
2449
|
+
});
|
|
2450
|
+
return true;
|
|
2451
|
+
},
|
|
2452
|
+
zoomIn: (opts) => {
|
|
2453
|
+
const step = opts?.step ?? 0.2;
|
|
2454
|
+
const next = Math.min(maxZoom, viewportRef.current.zoom * (1 + step));
|
|
2455
|
+
setViewport({ ...viewportRef.current, zoom: next });
|
|
2456
|
+
},
|
|
2457
|
+
zoomOut: (opts) => {
|
|
2458
|
+
const step = opts?.step ?? 0.2;
|
|
2459
|
+
const next = Math.max(minZoom, viewportRef.current.zoom / (1 + step));
|
|
2460
|
+
setViewport({ ...viewportRef.current, zoom: next });
|
|
2461
|
+
},
|
|
2462
|
+
zoomTo: (level) => {
|
|
2463
|
+
setViewport({
|
|
2464
|
+
...viewportRef.current,
|
|
2465
|
+
zoom: Math.max(minZoom, Math.min(maxZoom, level))
|
|
2466
|
+
});
|
|
2467
|
+
},
|
|
2468
|
+
// transforms
|
|
2469
|
+
screenToFlowPosition: (p) => screenToFlow(p, viewportRef.current),
|
|
2470
|
+
flowToScreenPosition: (p) => flowToScreen(p, viewportRef.current),
|
|
2471
|
+
// reads
|
|
2472
|
+
getNodes: () => nodesRef.current.slice(),
|
|
2473
|
+
getEdges: () => edgesRef.current.slice(),
|
|
2474
|
+
getNode: (id) => nodesRef.current.find((n) => n.id === id),
|
|
2475
|
+
getEdge: (id) => edgesRef.current.find((e) => e.id === id),
|
|
2476
|
+
getNodesBounds: (subset) => {
|
|
2477
|
+
const pool = subset ? nodesRef.current.filter((n) => subset.some((x) => x.id === n.id)) : nodesRef.current.filter((n) => !n.hidden);
|
|
2478
|
+
if (pool.length === 0) return { x: 0, y: 0, width: 0, height: 0 };
|
|
2479
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
2480
|
+
let minY = Number.POSITIVE_INFINITY;
|
|
2481
|
+
let maxX = Number.NEGATIVE_INFINITY;
|
|
2482
|
+
let maxY = Number.NEGATIVE_INFINITY;
|
|
2483
|
+
for (const n of pool) {
|
|
2484
|
+
const w = n.width ?? DEFAULT_NODE_WIDTH;
|
|
2485
|
+
const h = effectiveHeight(n);
|
|
2486
|
+
minX = Math.min(minX, n.position.x);
|
|
2487
|
+
minY = Math.min(minY, n.position.y);
|
|
2488
|
+
maxX = Math.max(maxX, n.position.x + w);
|
|
2489
|
+
maxY = Math.max(maxY, n.position.y + h);
|
|
2490
|
+
}
|
|
2491
|
+
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
|
2492
|
+
},
|
|
2493
|
+
getIntersectingNodes: (area, partially = true) => {
|
|
2494
|
+
return nodesRef.current.filter((n) => {
|
|
2495
|
+
if (n.hidden) return false;
|
|
2496
|
+
const w = n.width ?? DEFAULT_NODE_WIDTH;
|
|
2497
|
+
const h = effectiveHeight(n);
|
|
2498
|
+
if (partially) {
|
|
2499
|
+
return n.position.x < area.x + area.width && n.position.x + w > area.x && n.position.y < area.y + area.height && n.position.y + h > area.y;
|
|
2500
|
+
}
|
|
2501
|
+
return n.position.x >= area.x && n.position.y >= area.y && n.position.x + w <= area.x + area.width && n.position.y + h <= area.y + area.height;
|
|
2502
|
+
});
|
|
2503
|
+
},
|
|
2504
|
+
// mutations
|
|
2505
|
+
addNodes: (nodes2) => {
|
|
2506
|
+
const arr = Array.isArray(nodes2) ? nodes2 : [nodes2];
|
|
2507
|
+
onNodesChangeRefForInstance.current?.(arr.map((item) => change.node.add(item)));
|
|
2508
|
+
},
|
|
2509
|
+
addEdges: (edges2) => {
|
|
2510
|
+
const arr = Array.isArray(edges2) ? edges2 : [edges2];
|
|
2511
|
+
onEdgesChangeRefForInstance.current?.(arr.map((item) => change.edge.add(item)));
|
|
2512
|
+
},
|
|
2513
|
+
updateNodeData: (id, partial) => {
|
|
2514
|
+
const cur = nodesRef.current.find((n) => n.id === id);
|
|
2515
|
+
if (!cur) return;
|
|
2516
|
+
const nextData = { ...cur.data ?? {}, ...partial };
|
|
2517
|
+
onNodesChangeRefForInstance.current?.([
|
|
2518
|
+
change.node.replace(id, { ...cur, data: nextData })
|
|
2519
|
+
]);
|
|
2520
|
+
},
|
|
2521
|
+
updateNode: (id, partial) => {
|
|
2522
|
+
const cur = nodesRef.current.find((n) => n.id === id);
|
|
2523
|
+
if (!cur) return;
|
|
2524
|
+
onNodesChangeRefForInstance.current?.([change.node.replace(id, { ...cur, ...partial })]);
|
|
2525
|
+
},
|
|
2526
|
+
deleteElements: async ({ nodes: ns, edges: es }) => {
|
|
2527
|
+
const nodesToDelete = (ns ?? []).map((x) => nodesRef.current.find((n) => n.id === x.id)).filter((n) => !!n);
|
|
2528
|
+
const edgesToDelete = (es ?? []).map((x) => edgesRef.current.find((e) => e.id === x.id)).filter((e) => !!e);
|
|
2529
|
+
const before = onBeforeDeleteRef.current;
|
|
2530
|
+
if (before) {
|
|
2531
|
+
const ok = await before({ nodes: nodesToDelete, edges: edgesToDelete });
|
|
2532
|
+
if (!ok) return false;
|
|
2533
|
+
}
|
|
2534
|
+
if (nodesToDelete.length > 0) {
|
|
2535
|
+
onNodesChangeRefForInstance.current?.(nodesToDelete.map((n) => change.node.remove(n.id)));
|
|
2536
|
+
}
|
|
2537
|
+
if (edgesToDelete.length > 0) {
|
|
2538
|
+
onEdgesChangeRefForInstance.current?.(edgesToDelete.map((e) => change.edge.remove(e.id)));
|
|
2539
|
+
}
|
|
2540
|
+
return true;
|
|
2541
|
+
}
|
|
2542
|
+
}),
|
|
2543
|
+
[setViewport, minZoom, maxZoom]
|
|
2544
|
+
);
|
|
2545
|
+
const initFiredRef = useRef5(false);
|
|
2546
|
+
useEffect4(() => {
|
|
2547
|
+
if (initFiredRef.current) return;
|
|
2548
|
+
initFiredRef.current = true;
|
|
2549
|
+
onInit?.(instance);
|
|
2550
|
+
}, [instance, onInit]);
|
|
2551
|
+
const fitOnInitFiredRef = useRef5(false);
|
|
2552
|
+
useEffect4(() => {
|
|
2553
|
+
if (fitOnInitFiredRef.current) return;
|
|
2554
|
+
const opt = fitViewOnInit;
|
|
2555
|
+
const shouldFit = opt === false ? false : opt !== void 0 ? true : !viewportPropProvided;
|
|
2556
|
+
if (!shouldFit) return;
|
|
2557
|
+
if (nodes.length === 0) return;
|
|
2558
|
+
const rect = containerRef.current?.getBoundingClientRect();
|
|
2559
|
+
if (!rect || rect.width === 0 || rect.height === 0) return;
|
|
2560
|
+
fitOnInitFiredRef.current = true;
|
|
2561
|
+
const fitOpts = typeof opt === "object" && opt !== null ? opt : void 0;
|
|
2562
|
+
const raf = requestAnimationFrame(() => {
|
|
2563
|
+
void instance.fitView(fitOpts);
|
|
2564
|
+
});
|
|
2565
|
+
return () => cancelAnimationFrame(raf);
|
|
2566
|
+
}, [fitViewOnInit, viewportPropProvided, nodes.length, instance]);
|
|
2567
|
+
useEffect4(() => {
|
|
2568
|
+
const onKey = (e) => {
|
|
2569
|
+
if (e.key !== "Backspace" && e.key !== "Delete") return;
|
|
2570
|
+
const target = e.target;
|
|
2571
|
+
if (target && (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.tagName === "SELECT" || target.isContentEditable)) {
|
|
2572
|
+
return;
|
|
2573
|
+
}
|
|
2574
|
+
if (!containerRef.current?.contains(target) && document.activeElement !== document.body) {
|
|
2575
|
+
return;
|
|
2576
|
+
}
|
|
2577
|
+
const sel = store.getSnapshot();
|
|
2578
|
+
if (sel.selectedNodeIds.size === 0 && sel.selectedEdgeIds.size === 0) return;
|
|
2579
|
+
e.preventDefault();
|
|
2580
|
+
void instance.deleteElements({
|
|
2581
|
+
nodes: Array.from(sel.selectedNodeIds, (id) => ({ id })),
|
|
2582
|
+
edges: Array.from(sel.selectedEdgeIds, (id) => ({ id }))
|
|
2583
|
+
});
|
|
2584
|
+
};
|
|
2585
|
+
window.addEventListener("keydown", onKey);
|
|
2586
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
2587
|
+
}, [instance, store]);
|
|
2588
|
+
const bridge = useMemo3(
|
|
2589
|
+
() => ({
|
|
2590
|
+
beginNodeDrag,
|
|
2591
|
+
selectNode,
|
|
2592
|
+
notifyNodeClick,
|
|
2593
|
+
selectEdge,
|
|
2594
|
+
reportDimensions,
|
|
2595
|
+
deleteNode: deleteNodeImpl,
|
|
2596
|
+
toggleNodeCollapse: toggleNodeCollapseImpl
|
|
2597
|
+
}),
|
|
2598
|
+
[
|
|
2599
|
+
beginNodeDrag,
|
|
2600
|
+
selectNode,
|
|
2601
|
+
notifyNodeClick,
|
|
2602
|
+
selectEdge,
|
|
2603
|
+
reportDimensions,
|
|
2604
|
+
deleteNodeImpl,
|
|
2605
|
+
toggleNodeCollapseImpl
|
|
2606
|
+
]
|
|
2607
|
+
);
|
|
2608
|
+
const [panGesture, setPanGesture] = useState2(false);
|
|
2609
|
+
const isEmpty = nodes.length === 0 && edges.length === 0;
|
|
2610
|
+
const isConnecting = conn !== null;
|
|
2611
|
+
const visibleNodes = useMemo3(() => nodes.filter((n) => !n.hidden), [nodes]);
|
|
2612
|
+
const visibleEdges = useMemo3(() => {
|
|
2613
|
+
if (visibleNodes.length === nodes.length) return edges;
|
|
2614
|
+
const visibleIds = new Set(visibleNodes.map((n) => n.id));
|
|
2615
|
+
return edges.filter((e) => visibleIds.has(e.source) && visibleIds.has(e.target));
|
|
2616
|
+
}, [edges, nodes, visibleNodes]);
|
|
2617
|
+
const orderedNodes = useMemo3(() => {
|
|
2618
|
+
const isContainer = (n) => n.type === "group" || n.type === "forEach";
|
|
2619
|
+
const depth = (n) => {
|
|
2620
|
+
let d = 0;
|
|
2621
|
+
let cursor = n.parentId;
|
|
2622
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2623
|
+
while (cursor) {
|
|
2624
|
+
if (seen.has(cursor)) return Number.POSITIVE_INFINITY;
|
|
2625
|
+
seen.add(cursor);
|
|
2626
|
+
d++;
|
|
2627
|
+
cursor = visibleNodes.find((x) => x.id === cursor)?.parentId;
|
|
2628
|
+
}
|
|
2629
|
+
return d;
|
|
2630
|
+
};
|
|
2631
|
+
const containers = visibleNodes.filter(isContainer);
|
|
2632
|
+
const others = visibleNodes.filter((n) => !isContainer(n));
|
|
2633
|
+
containers.sort((a, b) => depth(a) - depth(b));
|
|
2634
|
+
return [...containers, ...others];
|
|
2635
|
+
}, [visibleNodes]);
|
|
2636
|
+
return /* @__PURE__ */ jsx7(FlowStoreContext.Provider, { value: store, children: /* @__PURE__ */ jsx7(FlowInstanceContext.Provider, { value: instance, children: /* @__PURE__ */ jsx7(HandleRegistryContext.Provider, { value: handleRegistry, children: /* @__PURE__ */ jsx7(FlowDispatchContext.Provider, { value: dispatch, children: /* @__PURE__ */ jsx7(FlowNodeBridgeContext.Provider, { value: bridge, children: /* @__PURE__ */ jsxs5(
|
|
2637
|
+
"div",
|
|
2638
|
+
{
|
|
2639
|
+
ref: containerRef,
|
|
2640
|
+
className: cn(
|
|
2641
|
+
"ods-flow-canvas-v2",
|
|
2642
|
+
panGesture && "ods-flow-canvas-v2--panning",
|
|
2643
|
+
isConnecting && "ods-flow-canvas-v2--connecting",
|
|
2644
|
+
draggingId && "ods-flow-canvas-v2--dragging",
|
|
2645
|
+
className
|
|
2646
|
+
),
|
|
2647
|
+
style: {
|
|
2648
|
+
position: "relative",
|
|
2649
|
+
width,
|
|
2650
|
+
height,
|
|
2651
|
+
overflow: "hidden",
|
|
2652
|
+
userSelect: "none",
|
|
2653
|
+
touchAction: "none",
|
|
2654
|
+
// Cursor: state-driven so it flips instantly with the
|
|
2655
|
+
// gesture. CSS `:active` would lag behind pointer-capture.
|
|
2656
|
+
cursor: panGesture ? "grabbing" : isConnecting ? "crosshair" : panOnDrag ? "grab" : "default",
|
|
2657
|
+
...style
|
|
2658
|
+
},
|
|
2659
|
+
onPointerDown: onCanvasPointerDown,
|
|
2660
|
+
onWheel: handleWheel,
|
|
2661
|
+
onContextMenu: (e) => {
|
|
2662
|
+
const t = e.target;
|
|
2663
|
+
const nodeEl = t.closest("[data-node-id]");
|
|
2664
|
+
if (nodeEl && onNodeContextMenu) {
|
|
2665
|
+
const id = nodeEl.dataset.nodeId;
|
|
2666
|
+
const node = nodes.find((n) => n.id === id);
|
|
2667
|
+
if (node) onNodeContextMenu(e, node);
|
|
2668
|
+
return;
|
|
2669
|
+
}
|
|
2670
|
+
const edgeEl = t.closest("[data-edge-id]");
|
|
2671
|
+
if (edgeEl && onEdgeContextMenu) {
|
|
2672
|
+
const id = edgeEl.dataset.edgeId;
|
|
2673
|
+
const edge = edges.find((x) => x.id === id);
|
|
2674
|
+
if (edge) onEdgeContextMenu(e, edge);
|
|
2675
|
+
return;
|
|
2676
|
+
}
|
|
2677
|
+
onPaneContextMenu?.(e);
|
|
2678
|
+
},
|
|
2679
|
+
"data-empty": isEmpty ? "true" : void 0,
|
|
2680
|
+
children: [
|
|
2681
|
+
background !== "none" && /* @__PURE__ */ jsx7(
|
|
2682
|
+
"div",
|
|
2683
|
+
{
|
|
2684
|
+
className: cn(
|
|
2685
|
+
"ods-flow-canvas-v2__grid",
|
|
2686
|
+
`ods-flow-canvas-v2__grid--${background}`
|
|
2687
|
+
),
|
|
2688
|
+
style: {
|
|
2689
|
+
// Custom property drives the four variants' SCSS.
|
|
2690
|
+
"--ods-flow-grid-size": `${gridSize * viewport.zoom}px`,
|
|
2691
|
+
backgroundPosition: `${viewport.x}px ${viewport.y}px`
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2694
|
+
),
|
|
2695
|
+
/* @__PURE__ */ jsxs5(
|
|
2696
|
+
"div",
|
|
2697
|
+
{
|
|
2698
|
+
className: "ods-flow-canvas-v2__viewport",
|
|
2699
|
+
style: {
|
|
2700
|
+
position: "absolute",
|
|
2701
|
+
inset: 0,
|
|
2702
|
+
transform: `translate(${viewport.x}px, ${viewport.y}px) scale(${viewport.zoom})`,
|
|
2703
|
+
transformOrigin: "0 0"
|
|
2704
|
+
},
|
|
2705
|
+
children: [
|
|
2706
|
+
/* @__PURE__ */ jsx7(
|
|
2707
|
+
EdgesLayer,
|
|
2708
|
+
{
|
|
2709
|
+
edges: visibleEdges,
|
|
2710
|
+
nodes: visibleNodes,
|
|
2711
|
+
onSelect: (id) => bridge.selectEdge(id, false),
|
|
2712
|
+
onDelete: (id) => onEdgesChangeRef(id, onEdgesChange),
|
|
2713
|
+
onLabelChange: onEdgeLabelChange,
|
|
2714
|
+
ghost: conn ? { start: conn.start, end: conn.end } : null,
|
|
2715
|
+
handleVersion
|
|
2716
|
+
}
|
|
2717
|
+
),
|
|
2718
|
+
orderedNodes.map((node) => {
|
|
2719
|
+
const Kind = kinds[node.type] ?? kinds.action;
|
|
2720
|
+
if (!Kind) return null;
|
|
2721
|
+
return /* @__PURE__ */ jsx7(
|
|
2722
|
+
FlowNode,
|
|
2723
|
+
{
|
|
2724
|
+
node,
|
|
2725
|
+
selected: selectedNodeIds.has(node.id),
|
|
2726
|
+
dragging: draggingId === node.id,
|
|
2727
|
+
isConnecting,
|
|
2728
|
+
Kind
|
|
2729
|
+
},
|
|
2730
|
+
node.id
|
|
2731
|
+
);
|
|
2732
|
+
})
|
|
2733
|
+
]
|
|
2734
|
+
}
|
|
2735
|
+
),
|
|
2736
|
+
isEmpty && emptyState && /* @__PURE__ */ jsx7("div", { className: "ods-flow-canvas-v2__empty", children: emptyState }),
|
|
2737
|
+
children
|
|
2738
|
+
]
|
|
2739
|
+
}
|
|
2740
|
+
) }) }) }) }) });
|
|
2741
|
+
}
|
|
2742
|
+
function onEdgesChangeRef(id, cb) {
|
|
2743
|
+
cb?.([change.edge.remove(id)]);
|
|
2744
|
+
}
|
|
2745
|
+
var EdgesLayer = memo2(function EdgesLayer2({
|
|
2746
|
+
edges,
|
|
2747
|
+
nodes,
|
|
2748
|
+
onSelect,
|
|
2749
|
+
onDelete,
|
|
2750
|
+
onLabelChange,
|
|
2751
|
+
ghost,
|
|
2752
|
+
handleVersion: _handleVersion
|
|
2753
|
+
}) {
|
|
2754
|
+
return /* @__PURE__ */ jsxs5(
|
|
2755
|
+
"svg",
|
|
2756
|
+
{
|
|
2757
|
+
className: "ods-flow-canvas-v2__edges",
|
|
2758
|
+
style: { position: "absolute", inset: 0, overflow: "visible", pointerEvents: "none" },
|
|
2759
|
+
width: "100%",
|
|
2760
|
+
height: "100%",
|
|
2761
|
+
children: [
|
|
2762
|
+
/* @__PURE__ */ jsx7("g", { style: { pointerEvents: "auto" }, children: edges.map((edge) => /* @__PURE__ */ jsx7(
|
|
2763
|
+
FlowEdge,
|
|
2764
|
+
{
|
|
2765
|
+
edge,
|
|
2766
|
+
nodes,
|
|
2767
|
+
onSelect,
|
|
2768
|
+
onDelete,
|
|
2769
|
+
onLabelChange,
|
|
2770
|
+
handleVersion: _handleVersion
|
|
2771
|
+
},
|
|
2772
|
+
edge.id
|
|
2773
|
+
)) }),
|
|
2774
|
+
ghost && /* @__PURE__ */ jsx7(
|
|
2775
|
+
"path",
|
|
2776
|
+
{
|
|
2777
|
+
d: `M ${ghost.start.x} ${ghost.start.y} L ${ghost.end.x} ${ghost.end.y}`,
|
|
2778
|
+
stroke: "var(--ods-accent, #4f46e5)",
|
|
2779
|
+
strokeWidth: 1.5,
|
|
2780
|
+
strokeDasharray: "4 4",
|
|
2781
|
+
fill: "none"
|
|
2782
|
+
}
|
|
2783
|
+
)
|
|
2784
|
+
]
|
|
2785
|
+
}
|
|
2786
|
+
);
|
|
2787
|
+
});
|
|
2788
|
+
|
|
2789
|
+
export {
|
|
2790
|
+
buildEdgePath,
|
|
2791
|
+
bezierPath,
|
|
2792
|
+
stepPath,
|
|
2793
|
+
smoothStepPath,
|
|
2794
|
+
straightPath,
|
|
2795
|
+
FlowInstanceContext,
|
|
2796
|
+
useFlow,
|
|
2797
|
+
FlowStoreContext,
|
|
2798
|
+
useFlowSelector,
|
|
2799
|
+
useNodes,
|
|
2800
|
+
useEdges,
|
|
2801
|
+
useViewport,
|
|
2802
|
+
useViewportOrNull,
|
|
2803
|
+
useNodeById,
|
|
2804
|
+
useNodeData,
|
|
2805
|
+
useEdgeById,
|
|
2806
|
+
useIsNodeSelected,
|
|
2807
|
+
useIsEdgeSelected,
|
|
2808
|
+
useConnection,
|
|
2809
|
+
useSelection,
|
|
2810
|
+
applyNodeChanges,
|
|
2811
|
+
applyEdgeChanges,
|
|
2812
|
+
change,
|
|
2813
|
+
DEFAULT_NODE_WIDTH,
|
|
2814
|
+
DEFAULT_NODE_HEIGHT,
|
|
2815
|
+
handleCentre,
|
|
2816
|
+
bezierPath2,
|
|
2817
|
+
screenToFlow,
|
|
2818
|
+
flowToScreen,
|
|
2819
|
+
descendantsOf,
|
|
2820
|
+
findAncestor,
|
|
2821
|
+
clampToParentExtent,
|
|
2822
|
+
findContainingGroup,
|
|
2823
|
+
FlowEdge,
|
|
2824
|
+
useFlowNodeContext,
|
|
2825
|
+
FlowNode,
|
|
2826
|
+
buildNodeKindRegistry,
|
|
2827
|
+
Handle,
|
|
2828
|
+
NodeResizer,
|
|
2829
|
+
ActionNode,
|
|
2830
|
+
TriggerNode,
|
|
2831
|
+
ConditionNode,
|
|
2832
|
+
GroupNode,
|
|
2833
|
+
ForEachNode,
|
|
2834
|
+
OutputNode,
|
|
2835
|
+
ErrorNode,
|
|
2836
|
+
WaitNode,
|
|
2837
|
+
ParallelNode,
|
|
2838
|
+
StickyNode,
|
|
2839
|
+
WebhookNode,
|
|
2840
|
+
HttpRequestNode,
|
|
2841
|
+
DEFAULT_NODE_KINDS,
|
|
2842
|
+
FlowCanvas
|
|
2843
|
+
};
|
|
2844
|
+
//# sourceMappingURL=chunk-GJA3GJUZ.js.map
|