@saltcorn/workflow-editor 1.6.0-alpha.5 → 1.6.0-alpha.7
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/workflow_bundle.js +53447 -2
- package/package.json +3 -2
- package/src/WorkflowEdge.jsx +0 -1
- package/src/WorkflowEditor.jsx +383 -131
- package/src/workflow.css +121 -1
- package/dist/workflow_bundle.js.LICENSE.txt +0 -61
- package/dist/workflow_bundle.js.map +0 -1
package/src/WorkflowEditor.jsx
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
Controls,
|
|
11
11
|
Handle,
|
|
12
12
|
MiniMap,
|
|
13
|
+
MarkerType,
|
|
13
14
|
Position,
|
|
14
15
|
useEdgesState,
|
|
15
16
|
useNodesState,
|
|
@@ -33,6 +34,13 @@ const H_GAP = 80;
|
|
|
33
34
|
const V_GAP = 60;
|
|
34
35
|
const ADD_NODE_SIZE = 32;
|
|
35
36
|
const ADD_GAP = 32;
|
|
37
|
+
const DEFAULT_ARROW_COLOR = "var(--wf-edge-label, #6c757d)";
|
|
38
|
+
const makeMarker = (color = DEFAULT_ARROW_COLOR) => ({
|
|
39
|
+
type: MarkerType.ArrowClosed,
|
|
40
|
+
width: 18,
|
|
41
|
+
height: 18,
|
|
42
|
+
color,
|
|
43
|
+
});
|
|
36
44
|
|
|
37
45
|
const normalizeSize = (size) => {
|
|
38
46
|
if (!size) return null;
|
|
@@ -52,7 +60,8 @@ const StartNode = ({ data }) => (
|
|
|
52
60
|
style={handleStyle}
|
|
53
61
|
id="start"
|
|
54
62
|
type="source"
|
|
55
|
-
|
|
63
|
+
className="wf-handle-source"
|
|
64
|
+
position={Position.Bottom}
|
|
56
65
|
/>
|
|
57
66
|
</div>
|
|
58
67
|
);
|
|
@@ -78,7 +87,7 @@ const AddNode = ({ data }) => {
|
|
|
78
87
|
onKeyDown={handleKeyDown}
|
|
79
88
|
>
|
|
80
89
|
<span aria-hidden>+</span>
|
|
81
|
-
<Handle type="target" position={Position.
|
|
90
|
+
<Handle type="target" position={Position.Top} />
|
|
82
91
|
</div>
|
|
83
92
|
);
|
|
84
93
|
};
|
|
@@ -108,11 +117,24 @@ const StepNode = ({ data }) => {
|
|
|
108
117
|
}
|
|
109
118
|
};
|
|
110
119
|
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
|
|
120
|
+
const targetPosition = Position.Top;
|
|
121
|
+
const sourcePosition = Position.Bottom;
|
|
122
|
+
const isLoop = data.action_name === "ForLoop";
|
|
123
|
+
const { showBackIn, showBackOut, showLoopIn, showLoopOut, hideMainSource } =
|
|
124
|
+
data || {};
|
|
125
|
+
|
|
126
|
+
const hasLeftSource = !!showBackOut;
|
|
127
|
+
const hasLeftTarget = !!showBackIn || !!showLoopIn;
|
|
128
|
+
|
|
129
|
+
let backInStyle = handleStyle;
|
|
130
|
+
let loopInStyle = handleStyle;
|
|
131
|
+
let backOutStyle = handleStyle;
|
|
132
|
+
|
|
133
|
+
if (hasLeftSource && hasLeftTarget) {
|
|
134
|
+
backInStyle = { ...handleStyle, top: "68%" };
|
|
135
|
+
loopInStyle = { ...handleStyle, top: "32%" };
|
|
136
|
+
backOutStyle = { ...handleStyle, top: "32%" };
|
|
137
|
+
}
|
|
116
138
|
|
|
117
139
|
return (
|
|
118
140
|
<div
|
|
@@ -154,12 +176,55 @@ const StepNode = ({ data }) => {
|
|
|
154
176
|
</div>
|
|
155
177
|
) : null} */}
|
|
156
178
|
<Handle style={handleStyle} type="target" position={targetPosition} />
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
179
|
+
{!hideMainSource ? (
|
|
180
|
+
<Handle
|
|
181
|
+
style={handleStyle}
|
|
182
|
+
id="main"
|
|
183
|
+
type="source"
|
|
184
|
+
className="wf-handle-source"
|
|
185
|
+
position={sourcePosition}
|
|
186
|
+
/>
|
|
187
|
+
) : null}
|
|
188
|
+
{/* Back edges use left handles to untangle cross-links */}
|
|
189
|
+
{showBackIn ? (
|
|
190
|
+
<Handle
|
|
191
|
+
style={backInStyle}
|
|
192
|
+
id="back-in"
|
|
193
|
+
type="target"
|
|
194
|
+
position={Position.Left}
|
|
195
|
+
/>
|
|
196
|
+
) : null}
|
|
197
|
+
{showBackOut ? (
|
|
198
|
+
<Handle
|
|
199
|
+
style={backOutStyle}
|
|
200
|
+
id="back-out"
|
|
201
|
+
type="source"
|
|
202
|
+
className="wf-handle-source"
|
|
203
|
+
position={Position.Left}
|
|
204
|
+
/>
|
|
205
|
+
) : null}
|
|
206
|
+
{isLoop ? (
|
|
207
|
+
<>
|
|
208
|
+
{showLoopIn ? (
|
|
209
|
+
<Handle
|
|
210
|
+
style={loopInStyle}
|
|
211
|
+
id="loop-in"
|
|
212
|
+
type="target"
|
|
213
|
+
className="wf-handle-target-loop"
|
|
214
|
+
position={Position.Left}
|
|
215
|
+
/>
|
|
216
|
+
) : null}
|
|
217
|
+
{showLoopOut ? (
|
|
218
|
+
<Handle
|
|
219
|
+
style={handleStyle}
|
|
220
|
+
id="loop-out"
|
|
221
|
+
type="source"
|
|
222
|
+
className="wf-handle-source-loop"
|
|
223
|
+
position={Position.Right}
|
|
224
|
+
/>
|
|
225
|
+
) : null}
|
|
226
|
+
</>
|
|
227
|
+
) : null}
|
|
163
228
|
</div>
|
|
164
229
|
);
|
|
165
230
|
};
|
|
@@ -234,6 +299,7 @@ const buildGraph = (
|
|
|
234
299
|
});
|
|
235
300
|
|
|
236
301
|
const allStepNames = steps.map((s) => s.name).filter(Boolean);
|
|
302
|
+
const orderIndex = new Map(steps.map((s, idx) => [String(s.id), idx]));
|
|
237
303
|
|
|
238
304
|
const escapeRegExp = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
239
305
|
|
|
@@ -264,6 +330,26 @@ const buildGraph = (
|
|
|
264
330
|
const loopBackLinks = findLoopBackLinks(steps);
|
|
265
331
|
const loopBodyIds = findLoopBodyStepIds(steps);
|
|
266
332
|
|
|
333
|
+
// Track which handles are actualy used by edges per node
|
|
334
|
+
const handleUsage = {};
|
|
335
|
+
const markHandleUsage = (nodeId, handleId) => {
|
|
336
|
+
if (!nodeId || !handleId) return;
|
|
337
|
+
const key = String(nodeId);
|
|
338
|
+
if (!handleUsage[key]) handleUsage[key] = {};
|
|
339
|
+
handleUsage[key][handleId] = true;
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
// Track whether a node has forward/back edges to decide when to hide main bottom handle
|
|
343
|
+
const edgeStats = {};
|
|
344
|
+
const markEdgeKind = (nodeId, { back = false, forward = false } = {}) => {
|
|
345
|
+
if (!nodeId) return;
|
|
346
|
+
const key = String(nodeId);
|
|
347
|
+
if (!edgeStats[key])
|
|
348
|
+
edgeStats[key] = { hasBackEdge: false, hasForwardEdge: false };
|
|
349
|
+
if (back) edgeStats[key].hasBackEdge = true;
|
|
350
|
+
if (forward) edgeStats[key].hasForwardEdge = true;
|
|
351
|
+
};
|
|
352
|
+
|
|
267
353
|
const adjacency = new Map();
|
|
268
354
|
steps.forEach((s) => {
|
|
269
355
|
const targets = [];
|
|
@@ -275,62 +361,33 @@ const buildGraph = (
|
|
|
275
361
|
adjacency.set(String(s.id), [...new Set(targets)]);
|
|
276
362
|
});
|
|
277
363
|
|
|
278
|
-
|
|
279
|
-
const queue = [];
|
|
364
|
+
// Revising logical ordering based on workflow graph traversal from initial step
|
|
280
365
|
if (initial) {
|
|
281
|
-
|
|
282
|
-
queue
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
queue.push(n);
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
let maxDepth = Object.values(depths).reduce((m, v) => Math.max(m, v), 0);
|
|
297
|
-
const sortedRemaining = steps
|
|
298
|
-
.map((s) => String(s.id))
|
|
299
|
-
.filter((id) => depths[id] === undefined)
|
|
300
|
-
.sort();
|
|
301
|
-
if (sortedRemaining.length) {
|
|
302
|
-
const orphanDepth = maxDepth + 1;
|
|
303
|
-
sortedRemaining.forEach((id) => {
|
|
304
|
-
depths[id] = orphanDepth;
|
|
305
|
-
});
|
|
306
|
-
maxDepth = orphanDepth;
|
|
366
|
+
const visited = new Set();
|
|
367
|
+
const queue = [String(initial.id)];
|
|
368
|
+
let index = 0;
|
|
369
|
+
while (queue.length) {
|
|
370
|
+
const nodeId = queue.shift();
|
|
371
|
+
if (visited.has(nodeId)) continue;
|
|
372
|
+
visited.add(nodeId);
|
|
373
|
+
orderIndex.set(nodeId, index++);
|
|
374
|
+
const neighbors = adjacency.get(nodeId) || [];
|
|
375
|
+
neighbors.forEach((n) => {
|
|
376
|
+
if (!visited.has(n)) queue.push(n);
|
|
377
|
+
});
|
|
378
|
+
}
|
|
307
379
|
}
|
|
308
380
|
|
|
309
|
-
const groupByDepth = new Map();
|
|
310
|
-
steps.forEach((s) => {
|
|
311
|
-
const d = depths[String(s.id)] || 1;
|
|
312
|
-
if (!groupByDepth.has(d)) groupByDepth.set(d, []);
|
|
313
|
-
groupByDepth.get(d).push(String(s.id));
|
|
314
|
-
});
|
|
315
|
-
[...groupByDepth.values()].forEach((arr) => arr.sort());
|
|
316
|
-
|
|
317
381
|
const positions = {};
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
});
|
|
328
|
-
depthEntries.forEach(([d, ids]) => {
|
|
329
|
-
let yCursor = 0;
|
|
330
|
-
ids.forEach((id) => {
|
|
331
|
-
positions[id] = { x: depthX[d], y: yCursor };
|
|
332
|
-
yCursor += (sizeById[id]?.height || DEFAULT_NODE_HEIGHT) + V_GAP;
|
|
333
|
-
});
|
|
382
|
+
let yCursor = 0;
|
|
383
|
+
steps.forEach((s) => {
|
|
384
|
+
const id = String(s.id);
|
|
385
|
+
const size = sizeById[id] || {
|
|
386
|
+
width: DEFAULT_NODE_WIDTH,
|
|
387
|
+
height: DEFAULT_NODE_HEIGHT,
|
|
388
|
+
};
|
|
389
|
+
positions[id] = { x: 0, y: yCursor };
|
|
390
|
+
yCursor += size.height + V_GAP;
|
|
334
391
|
});
|
|
335
392
|
|
|
336
393
|
// Nudge ForLoop bodies to be vertical under their loop parent for readability
|
|
@@ -378,10 +435,10 @@ const buildGraph = (
|
|
|
378
435
|
startPosition ||
|
|
379
436
|
(initial
|
|
380
437
|
? {
|
|
381
|
-
x: positions[String(initial.id)]?.x
|
|
382
|
-
y: positions[String(initial.id)]?.y
|
|
438
|
+
x: positions[String(initial.id)]?.x ?? 0,
|
|
439
|
+
y: (positions[String(initial.id)]?.y ?? 0) - 160,
|
|
383
440
|
}
|
|
384
|
-
: { x:
|
|
441
|
+
: { x: 0, y: -160 });
|
|
385
442
|
|
|
386
443
|
const nodes = [
|
|
387
444
|
{
|
|
@@ -431,6 +488,7 @@ const buildGraph = (
|
|
|
431
488
|
target: String(initial.id),
|
|
432
489
|
type: "smoothstep",
|
|
433
490
|
animated: true,
|
|
491
|
+
markerEnd: makeMarker(),
|
|
434
492
|
});
|
|
435
493
|
else {
|
|
436
494
|
const addId = "add-start";
|
|
@@ -445,6 +503,7 @@ const buildGraph = (
|
|
|
445
503
|
strokeDasharray: "4 2",
|
|
446
504
|
stroke: "var(--wf-edge-adder, #0d6efd)",
|
|
447
505
|
},
|
|
506
|
+
markerEnd: makeMarker("var(--wf-edge-adder, #0d6efd)"),
|
|
448
507
|
});
|
|
449
508
|
}
|
|
450
509
|
|
|
@@ -458,12 +517,42 @@ const buildGraph = (
|
|
|
458
517
|
nextNames.forEach((name) => {
|
|
459
518
|
const targetId = idByName[name];
|
|
460
519
|
if (!targetId) return;
|
|
520
|
+
const srcOrder = orderIndex.get(String(step.id));
|
|
521
|
+
const tgtOrder = orderIndex.get(targetId);
|
|
522
|
+
const srcPos = nodePositions[String(step.id)];
|
|
523
|
+
const tgtPos = nodePositions[targetId];
|
|
524
|
+
const isBackByOrder =
|
|
525
|
+
srcOrder !== undefined &&
|
|
526
|
+
tgtOrder !== undefined &&
|
|
527
|
+
tgtOrder < srcOrder;
|
|
528
|
+
const isBackVisually =
|
|
529
|
+
srcPos &&
|
|
530
|
+
tgtPos &&
|
|
531
|
+
typeof srcPos.y === "number" &&
|
|
532
|
+
typeof tgtPos.y === "number"
|
|
533
|
+
? tgtPos.y < srcPos.y
|
|
534
|
+
: false;
|
|
535
|
+
const isBackEdge = isBackByOrder && isBackVisually;
|
|
536
|
+
// record handle usage and edge direction for this source node
|
|
537
|
+
const sourceId = String(step.id);
|
|
538
|
+
if (isBackEdge) {
|
|
539
|
+
markHandleUsage(sourceId, "back-out");
|
|
540
|
+
markHandleUsage(targetId, "back-in");
|
|
541
|
+
markEdgeKind(sourceId, { back: true });
|
|
542
|
+
} else {
|
|
543
|
+
// uses the main bottom source handle
|
|
544
|
+
markHandleUsage(sourceId, "main");
|
|
545
|
+
markEdgeKind(sourceId, { forward: true });
|
|
546
|
+
}
|
|
461
547
|
edges.push({
|
|
462
548
|
id: `e-${step.id}-${name}`,
|
|
463
549
|
source: String(step.id),
|
|
464
550
|
target: targetId,
|
|
465
551
|
type: "workflow-main",
|
|
466
552
|
animated: true,
|
|
553
|
+
sourceHandle: isBackEdge ? "back-out" : undefined,
|
|
554
|
+
targetHandle: isBackEdge ? "back-in" : undefined,
|
|
555
|
+
markerEnd: makeMarker(),
|
|
467
556
|
data: {
|
|
468
557
|
startLabel: step.name,
|
|
469
558
|
sourceStepName: step.name,
|
|
@@ -478,17 +567,25 @@ const buildGraph = (
|
|
|
478
567
|
const loopTarget = step.configuration?.loop_body_initial_step;
|
|
479
568
|
if (loopTarget) {
|
|
480
569
|
const loopId = idByName[loopTarget];
|
|
570
|
+
const sourceId = String(step.id);
|
|
571
|
+
markHandleUsage(sourceId, "loop-out");
|
|
572
|
+
markEdgeKind(sourceId, { forward: true });
|
|
573
|
+
if (!loopId) {
|
|
574
|
+
markHandleUsage(sourceId, "loop-in");
|
|
575
|
+
}
|
|
481
576
|
edges.push({
|
|
482
577
|
id: `loop-${step.id}-${loopTarget}`,
|
|
483
578
|
source: String(step.id),
|
|
484
579
|
target: loopId || String(step.id),
|
|
485
580
|
type: "smoothstep",
|
|
581
|
+
sourceHandle: "loop-out",
|
|
582
|
+
targetHandle: loopId ? undefined : "loop-in",
|
|
486
583
|
style: {
|
|
487
584
|
stroke: "var(--wf-edge-loop, #f59f00)",
|
|
488
585
|
strokeDasharray: "6 4",
|
|
489
586
|
},
|
|
490
587
|
label: strings.loopBody,
|
|
491
|
-
markerEnd: "
|
|
588
|
+
markerEnd: makeMarker("var(--wf-edge-loop, #f59f00)"),
|
|
492
589
|
data: { loop: true, missing: !loopId },
|
|
493
590
|
});
|
|
494
591
|
}
|
|
@@ -498,17 +595,23 @@ const buildGraph = (
|
|
|
498
595
|
const forLoopName = loopBackLinks[step.name];
|
|
499
596
|
const loopId = idByName[forLoopName];
|
|
500
597
|
if (loopId) {
|
|
598
|
+
const sourceId = String(step.id);
|
|
599
|
+
markHandleUsage(sourceId, "back-out");
|
|
600
|
+
markHandleUsage(loopId, "loop-in");
|
|
601
|
+
markEdgeKind(sourceId, { back: true });
|
|
501
602
|
edges.push({
|
|
502
603
|
id: `loopback-${step.id}-${forLoopName}`,
|
|
503
604
|
source: String(step.id),
|
|
504
605
|
target: loopId,
|
|
505
606
|
type: "smoothstep",
|
|
607
|
+
sourceHandle: "back-out",
|
|
608
|
+
targetHandle: "loop-in",
|
|
506
609
|
style: {
|
|
507
610
|
stroke: "var(--wf-edge-loop, #f59f00)",
|
|
508
611
|
strokeDasharray: "6 4",
|
|
509
612
|
},
|
|
510
613
|
data: { loop: true, loopBack: true },
|
|
511
|
-
markerEnd: "
|
|
614
|
+
markerEnd: makeMarker("var(--wf-edge-loop, #f59f00)"),
|
|
512
615
|
deletable: false,
|
|
513
616
|
});
|
|
514
617
|
}
|
|
@@ -522,8 +625,8 @@ const buildGraph = (
|
|
|
522
625
|
id: addId,
|
|
523
626
|
type: "add",
|
|
524
627
|
position: addPositions?.[addId] || {
|
|
525
|
-
x: startNodePosition.x
|
|
526
|
-
y: startNodePosition.y,
|
|
628
|
+
x: startNodePosition.x,
|
|
629
|
+
y: startNodePosition.y + 160,
|
|
527
630
|
},
|
|
528
631
|
data: { strings, afterStepId: "start" },
|
|
529
632
|
draggable: true,
|
|
@@ -544,8 +647,8 @@ const buildGraph = (
|
|
|
544
647
|
id: addId,
|
|
545
648
|
type: "add",
|
|
546
649
|
position: addPositions?.[addId] || {
|
|
547
|
-
x: basePos.x + baseSize.width
|
|
548
|
-
y: basePos.y + baseSize.height
|
|
650
|
+
x: basePos.x + baseSize.width / 2 - ADD_NODE_SIZE / 2,
|
|
651
|
+
y: basePos.y + baseSize.height + ADD_GAP,
|
|
549
652
|
},
|
|
550
653
|
data: { strings, afterStepId: String(step.id) },
|
|
551
654
|
draggable: true,
|
|
@@ -563,15 +666,37 @@ const buildGraph = (
|
|
|
563
666
|
strokeDasharray: "4 2",
|
|
564
667
|
stroke: "var(--wf-edge-adder, #0d6efd)",
|
|
565
668
|
},
|
|
669
|
+
markerEnd: makeMarker("var(--wf-edge-adder, #0d6efd)"),
|
|
566
670
|
});
|
|
567
671
|
});
|
|
568
672
|
|
|
569
|
-
const allNodes = [...nodes, ...addNodes]
|
|
673
|
+
const allNodes = [...nodes, ...addNodes].map((node) => {
|
|
674
|
+
if (node.type !== "step") return node;
|
|
675
|
+
const id = String(node.id);
|
|
676
|
+
const usage = handleUsage[id] || {};
|
|
677
|
+
const stats = edgeStats[id] || {
|
|
678
|
+
hasBackEdge: false,
|
|
679
|
+
hasForwardEdge: false,
|
|
680
|
+
};
|
|
681
|
+
const hideMainSource = stats.hasBackEdge && !stats.hasForwardEdge;
|
|
682
|
+
|
|
683
|
+
return {
|
|
684
|
+
...node,
|
|
685
|
+
data: {
|
|
686
|
+
...node.data,
|
|
687
|
+
showBackIn: !!usage["back-in"],
|
|
688
|
+
showBackOut: !!usage["back-out"],
|
|
689
|
+
showLoopIn: !!usage["loop-in"],
|
|
690
|
+
showLoopOut: !!usage["loop-out"],
|
|
691
|
+
hideMainSource,
|
|
692
|
+
},
|
|
693
|
+
};
|
|
694
|
+
});
|
|
570
695
|
|
|
571
696
|
return { nodes: allNodes, edges, idByName, nameById };
|
|
572
697
|
};
|
|
573
698
|
|
|
574
|
-
const
|
|
699
|
+
const StepDrawer = ({
|
|
575
700
|
modal,
|
|
576
701
|
innerRef,
|
|
577
702
|
onClose,
|
|
@@ -579,52 +704,70 @@ const StepModal = ({
|
|
|
579
704
|
error,
|
|
580
705
|
data,
|
|
581
706
|
onDelete,
|
|
707
|
+
drawerWidth,
|
|
708
|
+
onResizeStart,
|
|
582
709
|
}) => {
|
|
583
|
-
|
|
710
|
+
const isOpen = !!modal;
|
|
584
711
|
return (
|
|
585
|
-
<div
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
/>
|
|
596
|
-
|
|
597
|
-
<div
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
)
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
712
|
+
<div
|
|
713
|
+
className={`wf-drawer${isOpen ? " wf-drawer--open" : ""}`}
|
|
714
|
+
role="dialog"
|
|
715
|
+
aria-modal="false"
|
|
716
|
+
aria-hidden={!isOpen}
|
|
717
|
+
aria-label={modal?.title || data?.strings?.configure}
|
|
718
|
+
style={{ "--wf-drawer-width": `${drawerWidth}px` }}
|
|
719
|
+
>
|
|
720
|
+
{modal ? (
|
|
721
|
+
<>
|
|
722
|
+
<div className="wf-drawer__resize" onPointerDown={onResizeStart} />
|
|
723
|
+
<div className="wf-drawer__header d-flex justify-content-between align-items-center">
|
|
724
|
+
<div>
|
|
725
|
+
<h5 className="mb-0">{modal.title}</h5>
|
|
726
|
+
</div>
|
|
727
|
+
<button
|
|
728
|
+
className="btn-close"
|
|
729
|
+
onClick={onClose}
|
|
730
|
+
aria-label="Close"
|
|
731
|
+
/>
|
|
732
|
+
</div>
|
|
733
|
+
<div className="wf-drawer__body">
|
|
734
|
+
<div
|
|
735
|
+
ref={innerRef}
|
|
736
|
+
dangerouslySetInnerHTML={{ __html: modal.body }}
|
|
737
|
+
/>
|
|
738
|
+
{error ? (
|
|
739
|
+
<div className="alert alert-danger mt-3">{error}</div>
|
|
740
|
+
) : null}
|
|
741
|
+
</div>
|
|
742
|
+
<div className="wf-drawer__footer d-flex justify-content-end gap-2">
|
|
743
|
+
<button
|
|
744
|
+
className="btn btn-sm btn-outline-danger"
|
|
745
|
+
onClick={(e) => {
|
|
746
|
+
e.stopPropagation();
|
|
747
|
+
onDelete(modal.stepId);
|
|
748
|
+
}}
|
|
749
|
+
>
|
|
750
|
+
{data.strings.deleteStep}
|
|
751
|
+
</button>
|
|
752
|
+
<button className="btn btn-secondary" onClick={onClose}>
|
|
753
|
+
Close
|
|
754
|
+
</button>
|
|
755
|
+
<button
|
|
756
|
+
className="btn btn-primary"
|
|
757
|
+
onClick={() =>
|
|
758
|
+
innerRef.current
|
|
759
|
+
?.querySelector("form")
|
|
760
|
+
?.dispatchEvent(
|
|
761
|
+
new Event("submit", { cancelable: true, bubbles: true })
|
|
762
|
+
)
|
|
763
|
+
}
|
|
764
|
+
disabled={submitting}
|
|
765
|
+
>
|
|
766
|
+
{submitting ? "Saving..." : "Save"}
|
|
767
|
+
</button>
|
|
768
|
+
</div>
|
|
769
|
+
</>
|
|
770
|
+
) : null}
|
|
628
771
|
</div>
|
|
629
772
|
);
|
|
630
773
|
};
|
|
@@ -649,7 +792,16 @@ const WorkflowEditor = ({ data }) => {
|
|
|
649
792
|
const [savingModal, setSavingModal] = useState(false);
|
|
650
793
|
const [savingPositions, setSavingPositions] = useState(false);
|
|
651
794
|
const [sizeSyncing, setSizeSyncing] = useState(false);
|
|
795
|
+
const [drawerWidth, setDrawerWidth] = useState(() => {
|
|
796
|
+
if (typeof window !== "undefined") {
|
|
797
|
+
return Math.max(600, Math.round(window.innerWidth * 0.5));
|
|
798
|
+
}
|
|
799
|
+
return 600;
|
|
800
|
+
});
|
|
652
801
|
const modalRef = useRef(null);
|
|
802
|
+
const resizingRef = useRef(false);
|
|
803
|
+
const connectSourceRef = useRef(null);
|
|
804
|
+
const [selectedEdgeId, setSelectedEdgeId] = useState(null);
|
|
653
805
|
|
|
654
806
|
const rfInstanceRef = useRef(null);
|
|
655
807
|
|
|
@@ -659,9 +811,10 @@ const WorkflowEditor = ({ data }) => {
|
|
|
659
811
|
|
|
660
812
|
const [selectedNodes, setSelectedNodes] = useState([]);
|
|
661
813
|
|
|
662
|
-
const handleSelectionChange = useCallback(({ nodes = [] }) => {
|
|
663
|
-
console.log("selection change", nodes);
|
|
814
|
+
const handleSelectionChange = useCallback(({ nodes = [], edges = [] }) => {
|
|
664
815
|
setSelectedNodes(nodes);
|
|
816
|
+
if (edges.length) setSelectedEdgeId(edges[0].id);
|
|
817
|
+
else setSelectedEdgeId(null);
|
|
665
818
|
}, []);
|
|
666
819
|
const handleKeyDelete = useCallback(
|
|
667
820
|
(e) => {
|
|
@@ -744,10 +897,12 @@ const WorkflowEditor = ({ data }) => {
|
|
|
744
897
|
const stepNode = stepById.get(node.id.replace("add-", ""));
|
|
745
898
|
if (!stepNode) return;
|
|
746
899
|
|
|
747
|
-
const nextX =
|
|
900
|
+
const nextX =
|
|
901
|
+
stepNode.position.x +
|
|
902
|
+
stepNode.width / 2 -
|
|
903
|
+
(node.width || ADD_NODE_SIZE) / 2;
|
|
748
904
|
|
|
749
|
-
const nextY =
|
|
750
|
-
stepNode.position.y + stepNode.height / 2 - (node.height || 24) / 2;
|
|
905
|
+
const nextY = stepNode.position.y + stepNode.height + ADD_GAP;
|
|
751
906
|
|
|
752
907
|
const nextPosition = { x: nextX, y: nextY };
|
|
753
908
|
|
|
@@ -1005,8 +1160,11 @@ const WorkflowEditor = ({ data }) => {
|
|
|
1005
1160
|
if (!formEl) return undefined;
|
|
1006
1161
|
// Re-run show-if logic so action-specific fields render correctly
|
|
1007
1162
|
setTimeout(() => {
|
|
1008
|
-
if (typeof window !== "undefined"
|
|
1009
|
-
window.apply_showif();
|
|
1163
|
+
if (typeof window !== "undefined") {
|
|
1164
|
+
if (window.apply_showif) window.apply_showif();
|
|
1165
|
+
// Initialize Monaco and any other dynamic widgets inside the drawer
|
|
1166
|
+
if (window.initialize_page) window.initialize_page();
|
|
1167
|
+
}
|
|
1010
1168
|
}, 0);
|
|
1011
1169
|
|
|
1012
1170
|
const actionSelect = formEl.querySelector('[name="wf_action_name"]');
|
|
@@ -1054,6 +1212,9 @@ const WorkflowEditor = ({ data }) => {
|
|
|
1054
1212
|
|
|
1055
1213
|
const onConnect = useCallback(
|
|
1056
1214
|
async (connection) => {
|
|
1215
|
+
// Valid connection landed; clear pending source so we don't treat it as a cancel
|
|
1216
|
+
connectSourceRef.current = null;
|
|
1217
|
+
setSelectedEdgeId(connection.id || null);
|
|
1057
1218
|
const loop = connection.sourceHandle === "loop";
|
|
1058
1219
|
const targetName = nameById[connection.target];
|
|
1059
1220
|
if (connection.source === "start") {
|
|
@@ -1074,12 +1235,22 @@ const WorkflowEditor = ({ data }) => {
|
|
|
1074
1235
|
const srcStep = steps.find(
|
|
1075
1236
|
(s) => String(s.id) === String(connection.source)
|
|
1076
1237
|
);
|
|
1238
|
+
const tgtStep = steps.find(
|
|
1239
|
+
(s) => String(s.id) === String(connection.target)
|
|
1240
|
+
);
|
|
1077
1241
|
const rawNext = srcStep?.next_step
|
|
1078
1242
|
? String(srcStep.next_step).trim()
|
|
1079
1243
|
: "";
|
|
1080
1244
|
const simpleNextId = rawNext && idByName[rawNext];
|
|
1245
|
+
const targetHasNext = !!(
|
|
1246
|
+
tgtStep && tgtStep.next_step && String(tgtStep.next_step).trim()
|
|
1247
|
+
);
|
|
1081
1248
|
|
|
1082
|
-
if (
|
|
1249
|
+
if (
|
|
1250
|
+
simpleNextId &&
|
|
1251
|
+
simpleNextId !== connection.target &&
|
|
1252
|
+
!targetHasNext
|
|
1253
|
+
) {
|
|
1083
1254
|
// Rewire: source -> new target, new target -> old target name
|
|
1084
1255
|
await updateConnection({
|
|
1085
1256
|
step_id: connection.target,
|
|
@@ -1097,6 +1268,28 @@ const WorkflowEditor = ({ data }) => {
|
|
|
1097
1268
|
[idByName, nameById, reload, steps, updateConnection]
|
|
1098
1269
|
);
|
|
1099
1270
|
|
|
1271
|
+
const onConnectStart = useCallback((_, params) => {
|
|
1272
|
+
if (params?.nodeId && params?.handleType === "source") {
|
|
1273
|
+
connectSourceRef.current = params;
|
|
1274
|
+
}
|
|
1275
|
+
}, []);
|
|
1276
|
+
|
|
1277
|
+
const onConnectEnd = useCallback(
|
|
1278
|
+
(event) => {
|
|
1279
|
+
const pending = connectSourceRef.current;
|
|
1280
|
+
connectSourceRef.current = null;
|
|
1281
|
+
if (!pending) return;
|
|
1282
|
+
const targetEl = event?.target;
|
|
1283
|
+
if (targetEl?.classList?.contains("react-flow__pane")) {
|
|
1284
|
+
// Dropped on empty space: clear next_step mark as final/unconnected
|
|
1285
|
+
if (pending.nodeId && pending.handleId !== "loop-out") {
|
|
1286
|
+
onClearNext(pending.nodeId);
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
},
|
|
1290
|
+
[onClearNext]
|
|
1291
|
+
);
|
|
1292
|
+
|
|
1100
1293
|
const onEdgesChange = useCallback(
|
|
1101
1294
|
(changes) => {
|
|
1102
1295
|
const removed = changes.filter((c) => c.type === "remove");
|
|
@@ -1289,9 +1482,63 @@ const WorkflowEditor = ({ data }) => {
|
|
|
1289
1482
|
}, [onInsertBetween, setEdges, edges]);
|
|
1290
1483
|
|
|
1291
1484
|
const hasSteps = steps.length > 0;
|
|
1485
|
+
const shellClass = modal ? "wf-shell wf-shell--drawer-open" : "wf-shell";
|
|
1486
|
+
const shellStyle = { "--wf-drawer-width": `${drawerWidth}px` };
|
|
1487
|
+
const showMiniMap = !modal;
|
|
1488
|
+
|
|
1489
|
+
const onEdgeClick = useCallback((_, edge) => {
|
|
1490
|
+
setSelectedEdgeId(edge.id);
|
|
1491
|
+
}, []);
|
|
1492
|
+
|
|
1493
|
+
useEffect(() => {
|
|
1494
|
+
setEdges((edgs) =>
|
|
1495
|
+
edgs.map((e) => {
|
|
1496
|
+
const selected = e.id === selectedEdgeId;
|
|
1497
|
+
const nextClass = selected
|
|
1498
|
+
? `${e.className || ""} wf-edge-selected`.trim()
|
|
1499
|
+
: (e.className || "").replace(/\s*wf-edge-selected\b/, "");
|
|
1500
|
+
const nextData =
|
|
1501
|
+
e.type === "workflow-main" ? { ...(e.data || {}), selected } : e.data;
|
|
1502
|
+
if (nextClass === (e.className || "") && nextData === e.data) return e;
|
|
1503
|
+
return { ...e, className: nextClass, data: nextData };
|
|
1504
|
+
})
|
|
1505
|
+
);
|
|
1506
|
+
}, [selectedEdgeId, setEdges]);
|
|
1507
|
+
|
|
1508
|
+
const handleResize = useCallback((e) => {
|
|
1509
|
+
if (!resizingRef.current) return;
|
|
1510
|
+
const maxWidth = Math.min(900, Math.max(360, window.innerWidth - 40));
|
|
1511
|
+
const nextWidth = Math.min(
|
|
1512
|
+
Math.max(600, window.innerWidth - e.clientX),
|
|
1513
|
+
maxWidth
|
|
1514
|
+
);
|
|
1515
|
+
setDrawerWidth(nextWidth);
|
|
1516
|
+
}, []);
|
|
1517
|
+
|
|
1518
|
+
const stopResize = useCallback(() => {
|
|
1519
|
+
if (!resizingRef.current) return;
|
|
1520
|
+
resizingRef.current = false;
|
|
1521
|
+
document.body.style.userSelect = "";
|
|
1522
|
+
window.removeEventListener("pointermove", handleResize);
|
|
1523
|
+
window.removeEventListener("pointerup", stopResize);
|
|
1524
|
+
}, [handleResize]);
|
|
1525
|
+
|
|
1526
|
+
const startResize = useCallback(
|
|
1527
|
+
(e) => {
|
|
1528
|
+
if (!modal) return;
|
|
1529
|
+
resizingRef.current = true;
|
|
1530
|
+
document.body.style.userSelect = "none";
|
|
1531
|
+
window.addEventListener("pointermove", handleResize);
|
|
1532
|
+
window.addEventListener("pointerup", stopResize);
|
|
1533
|
+
e.preventDefault();
|
|
1534
|
+
},
|
|
1535
|
+
[handleResize, modal, stopResize]
|
|
1536
|
+
);
|
|
1537
|
+
|
|
1538
|
+
useEffect(() => () => stopResize(), [stopResize]);
|
|
1292
1539
|
|
|
1293
1540
|
return (
|
|
1294
|
-
<div className=
|
|
1541
|
+
<div className={shellClass} style={shellStyle}>
|
|
1295
1542
|
<div className="wf-toolbar">
|
|
1296
1543
|
<div className="wf-toolbar__left">
|
|
1297
1544
|
<strong>{strings.configure}</strong>
|
|
@@ -1335,6 +1582,9 @@ const WorkflowEditor = ({ data }) => {
|
|
|
1335
1582
|
onNodesChange={onNodesChangeWrapped}
|
|
1336
1583
|
onEdgesChange={onEdgesChange}
|
|
1337
1584
|
onConnect={onConnect}
|
|
1585
|
+
onConnectStart={onConnectStart}
|
|
1586
|
+
onConnectEnd={onConnectEnd}
|
|
1587
|
+
onEdgeClick={onEdgeClick}
|
|
1338
1588
|
onSelectionChange={handleSelectionChange}
|
|
1339
1589
|
nodeTypes={nodeTypes}
|
|
1340
1590
|
edgeTypes={edgeTypes}
|
|
@@ -1344,12 +1594,12 @@ const WorkflowEditor = ({ data }) => {
|
|
|
1344
1594
|
defaultEdgeOptions={{ animated: true }}
|
|
1345
1595
|
colorMode={window._sc_lightmode || "light"}
|
|
1346
1596
|
>
|
|
1347
|
-
<MiniMap pannable zoomable />
|
|
1597
|
+
{showMiniMap && <MiniMap pannable zoomable />}
|
|
1348
1598
|
<Controls />
|
|
1349
1599
|
<Background gap={16} />
|
|
1350
1600
|
</ReactFlow>
|
|
1351
1601
|
</div>
|
|
1352
|
-
<
|
|
1602
|
+
<StepDrawer
|
|
1353
1603
|
modal={modal}
|
|
1354
1604
|
innerRef={modalRef}
|
|
1355
1605
|
onClose={() => {
|
|
@@ -1360,6 +1610,8 @@ const WorkflowEditor = ({ data }) => {
|
|
|
1360
1610
|
error={error && modal ? error : ""}
|
|
1361
1611
|
data={data}
|
|
1362
1612
|
onDelete={onDelete}
|
|
1613
|
+
drawerWidth={drawerWidth}
|
|
1614
|
+
onResizeStart={startResize}
|
|
1363
1615
|
/>
|
|
1364
1616
|
</div>
|
|
1365
1617
|
);
|