@saltcorn/workflow-editor 1.6.0-alpha.6 → 1.6.0-alpha.8
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 +53598 -2
- package/dist/workflow_bundle.js.LICENSE.txt +61 -0
- package/dist/workflow_bundle.js.map +1 -0
- package/package.json +3 -2
- package/src/WorkflowEdge.jsx +0 -1
- package/src/WorkflowEditor.jsx +566 -147
- package/src/workflow.css +123 -1
package/src/WorkflowEditor.jsx
CHANGED
|
@@ -4,12 +4,14 @@ import React, {
|
|
|
4
4
|
useRef,
|
|
5
5
|
useState,
|
|
6
6
|
useLayoutEffect,
|
|
7
|
+
useMemo,
|
|
7
8
|
} from "react";
|
|
8
9
|
import {
|
|
9
10
|
Background,
|
|
10
11
|
Controls,
|
|
11
12
|
Handle,
|
|
12
13
|
MiniMap,
|
|
14
|
+
MarkerType,
|
|
13
15
|
Position,
|
|
14
16
|
useEdgesState,
|
|
15
17
|
useNodesState,
|
|
@@ -33,6 +35,13 @@ const H_GAP = 80;
|
|
|
33
35
|
const V_GAP = 60;
|
|
34
36
|
const ADD_NODE_SIZE = 32;
|
|
35
37
|
const ADD_GAP = 32;
|
|
38
|
+
const DEFAULT_ARROW_COLOR = "var(--wf-edge-label, #6c757d)";
|
|
39
|
+
const makeMarker = (color = DEFAULT_ARROW_COLOR) => ({
|
|
40
|
+
type: MarkerType.ArrowClosed,
|
|
41
|
+
width: 18,
|
|
42
|
+
height: 18,
|
|
43
|
+
color,
|
|
44
|
+
});
|
|
36
45
|
|
|
37
46
|
const normalizeSize = (size) => {
|
|
38
47
|
if (!size) return null;
|
|
@@ -52,7 +61,8 @@ const StartNode = ({ data }) => (
|
|
|
52
61
|
style={handleStyle}
|
|
53
62
|
id="start"
|
|
54
63
|
type="source"
|
|
55
|
-
|
|
64
|
+
className="wf-handle-source"
|
|
65
|
+
position={Position.Bottom}
|
|
56
66
|
/>
|
|
57
67
|
</div>
|
|
58
68
|
);
|
|
@@ -78,7 +88,7 @@ const AddNode = ({ data }) => {
|
|
|
78
88
|
onKeyDown={handleKeyDown}
|
|
79
89
|
>
|
|
80
90
|
<span aria-hidden>+</span>
|
|
81
|
-
<Handle type="target" position={Position.
|
|
91
|
+
<Handle type="target" position={Position.Top} />
|
|
82
92
|
</div>
|
|
83
93
|
);
|
|
84
94
|
};
|
|
@@ -108,11 +118,24 @@ const StepNode = ({ data }) => {
|
|
|
108
118
|
}
|
|
109
119
|
};
|
|
110
120
|
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
|
|
121
|
+
const targetPosition = Position.Top;
|
|
122
|
+
const sourcePosition = Position.Bottom;
|
|
123
|
+
const isLoop = data.action_name === "ForLoop";
|
|
124
|
+
const { showBackIn, showBackOut, showLoopIn, showLoopOut, hideMainSource } =
|
|
125
|
+
data || {};
|
|
126
|
+
|
|
127
|
+
const hasLeftSource = !!showBackOut;
|
|
128
|
+
const hasLeftTarget = !!showBackIn || !!showLoopIn;
|
|
129
|
+
|
|
130
|
+
let backInStyle = handleStyle;
|
|
131
|
+
let loopInStyle = handleStyle;
|
|
132
|
+
let backOutStyle = handleStyle;
|
|
133
|
+
|
|
134
|
+
if (hasLeftSource && hasLeftTarget) {
|
|
135
|
+
backInStyle = { ...handleStyle, top: "68%" };
|
|
136
|
+
loopInStyle = { ...handleStyle, top: "32%" };
|
|
137
|
+
backOutStyle = { ...handleStyle, top: "32%" };
|
|
138
|
+
}
|
|
116
139
|
|
|
117
140
|
return (
|
|
118
141
|
<div
|
|
@@ -154,12 +177,55 @@ const StepNode = ({ data }) => {
|
|
|
154
177
|
</div>
|
|
155
178
|
) : null} */}
|
|
156
179
|
<Handle style={handleStyle} type="target" position={targetPosition} />
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
180
|
+
{!hideMainSource ? (
|
|
181
|
+
<Handle
|
|
182
|
+
style={handleStyle}
|
|
183
|
+
id="main"
|
|
184
|
+
type="source"
|
|
185
|
+
className="wf-handle-source"
|
|
186
|
+
position={sourcePosition}
|
|
187
|
+
/>
|
|
188
|
+
) : null}
|
|
189
|
+
{/* Back edges use left handles to untangle cross-links */}
|
|
190
|
+
{showBackIn ? (
|
|
191
|
+
<Handle
|
|
192
|
+
style={backInStyle}
|
|
193
|
+
id="back-in"
|
|
194
|
+
type="target"
|
|
195
|
+
position={Position.Left}
|
|
196
|
+
/>
|
|
197
|
+
) : null}
|
|
198
|
+
{showBackOut ? (
|
|
199
|
+
<Handle
|
|
200
|
+
style={backOutStyle}
|
|
201
|
+
id="back-out"
|
|
202
|
+
type="source"
|
|
203
|
+
className="wf-handle-source"
|
|
204
|
+
position={Position.Left}
|
|
205
|
+
/>
|
|
206
|
+
) : null}
|
|
207
|
+
{isLoop ? (
|
|
208
|
+
<>
|
|
209
|
+
{showLoopIn ? (
|
|
210
|
+
<Handle
|
|
211
|
+
style={loopInStyle}
|
|
212
|
+
id="loop-in"
|
|
213
|
+
type="target"
|
|
214
|
+
className="wf-handle-target-loop"
|
|
215
|
+
position={Position.Left}
|
|
216
|
+
/>
|
|
217
|
+
) : null}
|
|
218
|
+
{showLoopOut ? (
|
|
219
|
+
<Handle
|
|
220
|
+
style={handleStyle}
|
|
221
|
+
id="loop-out"
|
|
222
|
+
type="source"
|
|
223
|
+
className="wf-handle-source-loop"
|
|
224
|
+
position={Position.Right}
|
|
225
|
+
/>
|
|
226
|
+
) : null}
|
|
227
|
+
</>
|
|
228
|
+
) : null}
|
|
163
229
|
</div>
|
|
164
230
|
);
|
|
165
231
|
};
|
|
@@ -234,6 +300,7 @@ const buildGraph = (
|
|
|
234
300
|
});
|
|
235
301
|
|
|
236
302
|
const allStepNames = steps.map((s) => s.name).filter(Boolean);
|
|
303
|
+
const orderIndex = new Map(steps.map((s, idx) => [String(s.id), idx]));
|
|
237
304
|
|
|
238
305
|
const escapeRegExp = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
239
306
|
|
|
@@ -264,6 +331,26 @@ const buildGraph = (
|
|
|
264
331
|
const loopBackLinks = findLoopBackLinks(steps);
|
|
265
332
|
const loopBodyIds = findLoopBodyStepIds(steps);
|
|
266
333
|
|
|
334
|
+
// Track which handles are actualy used by edges per node
|
|
335
|
+
const handleUsage = {};
|
|
336
|
+
const markHandleUsage = (nodeId, handleId) => {
|
|
337
|
+
if (!nodeId || !handleId) return;
|
|
338
|
+
const key = String(nodeId);
|
|
339
|
+
if (!handleUsage[key]) handleUsage[key] = {};
|
|
340
|
+
handleUsage[key][handleId] = true;
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
// Track whether a node has forward/back edges to decide when to hide main bottom handle
|
|
344
|
+
const edgeStats = {};
|
|
345
|
+
const markEdgeKind = (nodeId, { back = false, forward = false } = {}) => {
|
|
346
|
+
if (!nodeId) return;
|
|
347
|
+
const key = String(nodeId);
|
|
348
|
+
if (!edgeStats[key])
|
|
349
|
+
edgeStats[key] = { hasBackEdge: false, hasForwardEdge: false };
|
|
350
|
+
if (back) edgeStats[key].hasBackEdge = true;
|
|
351
|
+
if (forward) edgeStats[key].hasForwardEdge = true;
|
|
352
|
+
};
|
|
353
|
+
|
|
267
354
|
const adjacency = new Map();
|
|
268
355
|
steps.forEach((s) => {
|
|
269
356
|
const targets = [];
|
|
@@ -275,62 +362,33 @@ const buildGraph = (
|
|
|
275
362
|
adjacency.set(String(s.id), [...new Set(targets)]);
|
|
276
363
|
});
|
|
277
364
|
|
|
278
|
-
|
|
279
|
-
const queue = [];
|
|
365
|
+
// Revising logical ordering based on workflow graph traversal from initial step
|
|
280
366
|
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;
|
|
367
|
+
const visited = new Set();
|
|
368
|
+
const queue = [String(initial.id)];
|
|
369
|
+
let index = 0;
|
|
370
|
+
while (queue.length) {
|
|
371
|
+
const nodeId = queue.shift();
|
|
372
|
+
if (visited.has(nodeId)) continue;
|
|
373
|
+
visited.add(nodeId);
|
|
374
|
+
orderIndex.set(nodeId, index++);
|
|
375
|
+
const neighbors = adjacency.get(nodeId) || [];
|
|
376
|
+
neighbors.forEach((n) => {
|
|
377
|
+
if (!visited.has(n)) queue.push(n);
|
|
378
|
+
});
|
|
379
|
+
}
|
|
307
380
|
}
|
|
308
381
|
|
|
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
382
|
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
|
-
});
|
|
383
|
+
let yCursor = 0;
|
|
384
|
+
steps.forEach((s) => {
|
|
385
|
+
const id = String(s.id);
|
|
386
|
+
const size = sizeById[id] || {
|
|
387
|
+
width: DEFAULT_NODE_WIDTH,
|
|
388
|
+
height: DEFAULT_NODE_HEIGHT,
|
|
389
|
+
};
|
|
390
|
+
positions[id] = { x: 0, y: yCursor };
|
|
391
|
+
yCursor += size.height + V_GAP;
|
|
334
392
|
});
|
|
335
393
|
|
|
336
394
|
// Nudge ForLoop bodies to be vertical under their loop parent for readability
|
|
@@ -378,10 +436,10 @@ const buildGraph = (
|
|
|
378
436
|
startPosition ||
|
|
379
437
|
(initial
|
|
380
438
|
? {
|
|
381
|
-
x: positions[String(initial.id)]?.x
|
|
382
|
-
y: positions[String(initial.id)]?.y
|
|
439
|
+
x: positions[String(initial.id)]?.x ?? 0,
|
|
440
|
+
y: (positions[String(initial.id)]?.y ?? 0) - 160,
|
|
383
441
|
}
|
|
384
|
-
: { x:
|
|
442
|
+
: { x: 0, y: -160 });
|
|
385
443
|
|
|
386
444
|
const nodes = [
|
|
387
445
|
{
|
|
@@ -431,6 +489,7 @@ const buildGraph = (
|
|
|
431
489
|
target: String(initial.id),
|
|
432
490
|
type: "smoothstep",
|
|
433
491
|
animated: true,
|
|
492
|
+
markerEnd: makeMarker(),
|
|
434
493
|
});
|
|
435
494
|
else {
|
|
436
495
|
const addId = "add-start";
|
|
@@ -445,6 +504,7 @@ const buildGraph = (
|
|
|
445
504
|
strokeDasharray: "4 2",
|
|
446
505
|
stroke: "var(--wf-edge-adder, #0d6efd)",
|
|
447
506
|
},
|
|
507
|
+
markerEnd: makeMarker("var(--wf-edge-adder, #0d6efd)"),
|
|
448
508
|
});
|
|
449
509
|
}
|
|
450
510
|
|
|
@@ -458,12 +518,42 @@ const buildGraph = (
|
|
|
458
518
|
nextNames.forEach((name) => {
|
|
459
519
|
const targetId = idByName[name];
|
|
460
520
|
if (!targetId) return;
|
|
521
|
+
const srcOrder = orderIndex.get(String(step.id));
|
|
522
|
+
const tgtOrder = orderIndex.get(targetId);
|
|
523
|
+
const srcPos = nodePositions[String(step.id)];
|
|
524
|
+
const tgtPos = nodePositions[targetId];
|
|
525
|
+
const isBackByOrder =
|
|
526
|
+
srcOrder !== undefined &&
|
|
527
|
+
tgtOrder !== undefined &&
|
|
528
|
+
tgtOrder < srcOrder;
|
|
529
|
+
const isBackVisually =
|
|
530
|
+
srcPos &&
|
|
531
|
+
tgtPos &&
|
|
532
|
+
typeof srcPos.y === "number" &&
|
|
533
|
+
typeof tgtPos.y === "number"
|
|
534
|
+
? tgtPos.y < srcPos.y
|
|
535
|
+
: false;
|
|
536
|
+
const isBackEdge = isBackByOrder && isBackVisually;
|
|
537
|
+
// record handle usage and edge direction for this source node
|
|
538
|
+
const sourceId = String(step.id);
|
|
539
|
+
if (isBackEdge) {
|
|
540
|
+
markHandleUsage(sourceId, "back-out");
|
|
541
|
+
markHandleUsage(targetId, "back-in");
|
|
542
|
+
markEdgeKind(sourceId, { back: true });
|
|
543
|
+
} else {
|
|
544
|
+
// uses the main bottom source handle
|
|
545
|
+
markHandleUsage(sourceId, "main");
|
|
546
|
+
markEdgeKind(sourceId, { forward: true });
|
|
547
|
+
}
|
|
461
548
|
edges.push({
|
|
462
549
|
id: `e-${step.id}-${name}`,
|
|
463
550
|
source: String(step.id),
|
|
464
551
|
target: targetId,
|
|
465
552
|
type: "workflow-main",
|
|
466
553
|
animated: true,
|
|
554
|
+
sourceHandle: isBackEdge ? "back-out" : undefined,
|
|
555
|
+
targetHandle: isBackEdge ? "back-in" : undefined,
|
|
556
|
+
markerEnd: makeMarker(),
|
|
467
557
|
data: {
|
|
468
558
|
startLabel: step.name,
|
|
469
559
|
sourceStepName: step.name,
|
|
@@ -478,17 +568,25 @@ const buildGraph = (
|
|
|
478
568
|
const loopTarget = step.configuration?.loop_body_initial_step;
|
|
479
569
|
if (loopTarget) {
|
|
480
570
|
const loopId = idByName[loopTarget];
|
|
571
|
+
const sourceId = String(step.id);
|
|
572
|
+
markHandleUsage(sourceId, "loop-out");
|
|
573
|
+
markEdgeKind(sourceId, { forward: true });
|
|
574
|
+
if (!loopId) {
|
|
575
|
+
markHandleUsage(sourceId, "loop-in");
|
|
576
|
+
}
|
|
481
577
|
edges.push({
|
|
482
578
|
id: `loop-${step.id}-${loopTarget}`,
|
|
483
579
|
source: String(step.id),
|
|
484
580
|
target: loopId || String(step.id),
|
|
485
581
|
type: "smoothstep",
|
|
582
|
+
sourceHandle: "loop-out",
|
|
583
|
+
targetHandle: loopId ? undefined : "loop-in",
|
|
486
584
|
style: {
|
|
487
585
|
stroke: "var(--wf-edge-loop, #f59f00)",
|
|
488
586
|
strokeDasharray: "6 4",
|
|
489
587
|
},
|
|
490
588
|
label: strings.loopBody,
|
|
491
|
-
markerEnd: "
|
|
589
|
+
markerEnd: makeMarker("var(--wf-edge-loop, #f59f00)"),
|
|
492
590
|
data: { loop: true, missing: !loopId },
|
|
493
591
|
});
|
|
494
592
|
}
|
|
@@ -498,17 +596,23 @@ const buildGraph = (
|
|
|
498
596
|
const forLoopName = loopBackLinks[step.name];
|
|
499
597
|
const loopId = idByName[forLoopName];
|
|
500
598
|
if (loopId) {
|
|
599
|
+
const sourceId = String(step.id);
|
|
600
|
+
markHandleUsage(sourceId, "back-out");
|
|
601
|
+
markHandleUsage(loopId, "loop-in");
|
|
602
|
+
markEdgeKind(sourceId, { back: true });
|
|
501
603
|
edges.push({
|
|
502
604
|
id: `loopback-${step.id}-${forLoopName}`,
|
|
503
605
|
source: String(step.id),
|
|
504
606
|
target: loopId,
|
|
505
607
|
type: "smoothstep",
|
|
608
|
+
sourceHandle: "back-out",
|
|
609
|
+
targetHandle: "loop-in",
|
|
506
610
|
style: {
|
|
507
611
|
stroke: "var(--wf-edge-loop, #f59f00)",
|
|
508
612
|
strokeDasharray: "6 4",
|
|
509
613
|
},
|
|
510
614
|
data: { loop: true, loopBack: true },
|
|
511
|
-
markerEnd: "
|
|
615
|
+
markerEnd: makeMarker("var(--wf-edge-loop, #f59f00)"),
|
|
512
616
|
deletable: false,
|
|
513
617
|
});
|
|
514
618
|
}
|
|
@@ -522,8 +626,8 @@ const buildGraph = (
|
|
|
522
626
|
id: addId,
|
|
523
627
|
type: "add",
|
|
524
628
|
position: addPositions?.[addId] || {
|
|
525
|
-
x: startNodePosition.x
|
|
526
|
-
y: startNodePosition.y,
|
|
629
|
+
x: startNodePosition.x,
|
|
630
|
+
y: startNodePosition.y + 160,
|
|
527
631
|
},
|
|
528
632
|
data: { strings, afterStepId: "start" },
|
|
529
633
|
draggable: true,
|
|
@@ -544,8 +648,8 @@ const buildGraph = (
|
|
|
544
648
|
id: addId,
|
|
545
649
|
type: "add",
|
|
546
650
|
position: addPositions?.[addId] || {
|
|
547
|
-
x: basePos.x + baseSize.width
|
|
548
|
-
y: basePos.y + baseSize.height
|
|
651
|
+
x: basePos.x + baseSize.width / 2 - ADD_NODE_SIZE / 2,
|
|
652
|
+
y: basePos.y + baseSize.height + ADD_GAP,
|
|
549
653
|
},
|
|
550
654
|
data: { strings, afterStepId: String(step.id) },
|
|
551
655
|
draggable: true,
|
|
@@ -563,68 +667,110 @@ const buildGraph = (
|
|
|
563
667
|
strokeDasharray: "4 2",
|
|
564
668
|
stroke: "var(--wf-edge-adder, #0d6efd)",
|
|
565
669
|
},
|
|
670
|
+
markerEnd: makeMarker("var(--wf-edge-adder, #0d6efd)"),
|
|
566
671
|
});
|
|
567
672
|
});
|
|
568
673
|
|
|
569
|
-
const allNodes = [...nodes, ...addNodes]
|
|
674
|
+
const allNodes = [...nodes, ...addNodes].map((node) => {
|
|
675
|
+
if (node.type !== "step") return node;
|
|
676
|
+
const id = String(node.id);
|
|
677
|
+
const usage = handleUsage[id] || {};
|
|
678
|
+
const stats = edgeStats[id] || {
|
|
679
|
+
hasBackEdge: false,
|
|
680
|
+
hasForwardEdge: false,
|
|
681
|
+
};
|
|
682
|
+
const hideMainSource = stats.hasBackEdge && !stats.hasForwardEdge;
|
|
683
|
+
|
|
684
|
+
return {
|
|
685
|
+
...node,
|
|
686
|
+
data: {
|
|
687
|
+
...node.data,
|
|
688
|
+
showBackIn: !!usage["back-in"],
|
|
689
|
+
showBackOut: !!usage["back-out"],
|
|
690
|
+
showLoopIn: !!usage["loop-in"],
|
|
691
|
+
showLoopOut: !!usage["loop-out"],
|
|
692
|
+
hideMainSource,
|
|
693
|
+
},
|
|
694
|
+
};
|
|
695
|
+
});
|
|
570
696
|
|
|
571
697
|
return { nodes: allNodes, edges, idByName, nameById };
|
|
572
698
|
};
|
|
573
699
|
|
|
574
|
-
const
|
|
700
|
+
const StepDrawer = ({
|
|
575
701
|
modal,
|
|
576
702
|
innerRef,
|
|
703
|
+
outerRef,
|
|
577
704
|
onClose,
|
|
578
705
|
submitting,
|
|
579
706
|
error,
|
|
580
707
|
data,
|
|
581
708
|
onDelete,
|
|
709
|
+
drawerWidth,
|
|
710
|
+
onResizeStart,
|
|
582
711
|
}) => {
|
|
583
|
-
|
|
712
|
+
const isOpen = !!modal;
|
|
584
713
|
return (
|
|
585
|
-
<div
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
{
|
|
597
|
-
|
|
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
|
-
|
|
714
|
+
<div
|
|
715
|
+
ref={outerRef}
|
|
716
|
+
className={`wf-drawer${isOpen ? " wf-drawer--open" : ""}`}
|
|
717
|
+
role="dialog"
|
|
718
|
+
aria-modal="false"
|
|
719
|
+
aria-hidden={!isOpen}
|
|
720
|
+
aria-label={modal?.title || data?.strings?.configure}
|
|
721
|
+
style={{ "--wf-drawer-width": `${drawerWidth}px` }}
|
|
722
|
+
>
|
|
723
|
+
{modal ? (
|
|
724
|
+
<>
|
|
725
|
+
<div className="wf-drawer__resize" onPointerDown={onResizeStart} />
|
|
726
|
+
<div className="wf-drawer__header d-flex justify-content-between align-items-center">
|
|
727
|
+
<div>
|
|
728
|
+
<h5 className="mb-0">{modal.title}</h5>
|
|
729
|
+
</div>
|
|
730
|
+
<button
|
|
731
|
+
className="btn-close"
|
|
732
|
+
onClick={onClose}
|
|
733
|
+
aria-label="Close"
|
|
734
|
+
/>
|
|
735
|
+
</div>
|
|
736
|
+
<div className="wf-drawer__body">
|
|
737
|
+
<div
|
|
738
|
+
ref={innerRef}
|
|
739
|
+
dangerouslySetInnerHTML={{ __html: modal.body }}
|
|
740
|
+
/>
|
|
741
|
+
{error ? (
|
|
742
|
+
<div className="alert alert-danger mt-3">{error}</div>
|
|
743
|
+
) : null}
|
|
744
|
+
</div>
|
|
745
|
+
<div className="wf-drawer__footer d-flex justify-content-end gap-2">
|
|
746
|
+
<button
|
|
747
|
+
className="btn btn-sm btn-outline-danger"
|
|
748
|
+
onClick={(e) => {
|
|
749
|
+
e.stopPropagation();
|
|
750
|
+
onDelete(modal.stepId);
|
|
751
|
+
}}
|
|
752
|
+
>
|
|
753
|
+
{data.strings.deleteStep}
|
|
754
|
+
</button>
|
|
755
|
+
<button className="btn btn-secondary" onClick={onClose}>
|
|
756
|
+
Close
|
|
757
|
+
</button>
|
|
758
|
+
<button
|
|
759
|
+
className="btn btn-primary"
|
|
760
|
+
onClick={() =>
|
|
761
|
+
innerRef.current
|
|
762
|
+
?.querySelector("form")
|
|
763
|
+
?.dispatchEvent(
|
|
764
|
+
new Event("submit", { cancelable: true, bubbles: true })
|
|
765
|
+
)
|
|
766
|
+
}
|
|
767
|
+
disabled={submitting}
|
|
768
|
+
>
|
|
769
|
+
{submitting ? "Saving..." : "Save"}
|
|
770
|
+
</button>
|
|
771
|
+
</div>
|
|
772
|
+
</>
|
|
773
|
+
) : null}
|
|
628
774
|
</div>
|
|
629
775
|
);
|
|
630
776
|
};
|
|
@@ -649,19 +795,111 @@ const WorkflowEditor = ({ data }) => {
|
|
|
649
795
|
const [savingModal, setSavingModal] = useState(false);
|
|
650
796
|
const [savingPositions, setSavingPositions] = useState(false);
|
|
651
797
|
const [sizeSyncing, setSizeSyncing] = useState(false);
|
|
798
|
+
const [drawerWidth, setDrawerWidth] = useState(() => {
|
|
799
|
+
if (typeof window !== "undefined") {
|
|
800
|
+
return Math.max(600, Math.round(window.innerWidth * 0.5));
|
|
801
|
+
}
|
|
802
|
+
return 600;
|
|
803
|
+
});
|
|
652
804
|
const modalRef = useRef(null);
|
|
805
|
+
const drawerRef = useRef(null);
|
|
806
|
+
const shellRef = useRef(null);
|
|
807
|
+
const resizingRef = useRef(false);
|
|
808
|
+
const connectSourceRef = useRef(null);
|
|
809
|
+
const [selectedEdgeId, setSelectedEdgeId] = useState(null);
|
|
810
|
+
|
|
811
|
+
const drawerStorageKey = useMemo(
|
|
812
|
+
() => `wf-drawer:${data?.trigger?.id || "default"}`,
|
|
813
|
+
[data?.trigger?.id]
|
|
814
|
+
);
|
|
815
|
+
|
|
816
|
+
const viewportStorageKey = useMemo(
|
|
817
|
+
() => `wf-viewport:${data?.trigger?.id || "default"}`,
|
|
818
|
+
[data?.trigger?.id]
|
|
819
|
+
);
|
|
653
820
|
|
|
654
821
|
const rfInstanceRef = useRef(null);
|
|
655
822
|
|
|
823
|
+
const persistDrawerState = useCallback(
|
|
824
|
+
(payload) => {
|
|
825
|
+
try {
|
|
826
|
+
if (typeof window === "undefined") return;
|
|
827
|
+
window.sessionStorage.setItem(
|
|
828
|
+
drawerStorageKey,
|
|
829
|
+
JSON.stringify(payload || {})
|
|
830
|
+
);
|
|
831
|
+
} catch (e) {
|
|
832
|
+
// ignore storage errors
|
|
833
|
+
}
|
|
834
|
+
},
|
|
835
|
+
[drawerStorageKey]
|
|
836
|
+
);
|
|
837
|
+
|
|
838
|
+
const clearDrawerState = useCallback(() => {
|
|
839
|
+
try {
|
|
840
|
+
if (typeof window === "undefined") return;
|
|
841
|
+
window.sessionStorage.removeItem(drawerStorageKey);
|
|
842
|
+
} catch (e) {
|
|
843
|
+
// ignore storage errors
|
|
844
|
+
}
|
|
845
|
+
}, [drawerStorageKey]);
|
|
846
|
+
|
|
847
|
+
const loadViewport = useCallback(() => {
|
|
848
|
+
if (typeof window === "undefined") return null;
|
|
849
|
+
try {
|
|
850
|
+
const raw = window.sessionStorage.getItem(viewportStorageKey);
|
|
851
|
+
if (!raw) return null;
|
|
852
|
+
const parsed = JSON.parse(raw);
|
|
853
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
854
|
+
const { x, y, zoom } = parsed;
|
|
855
|
+
if (
|
|
856
|
+
Number.isFinite(Number(x)) &&
|
|
857
|
+
Number.isFinite(Number(y)) &&
|
|
858
|
+
Number.isFinite(Number(zoom))
|
|
859
|
+
) {
|
|
860
|
+
return { x: Number(x), y: Number(y), zoom: Number(zoom) };
|
|
861
|
+
}
|
|
862
|
+
} catch (e) {
|
|
863
|
+
// ignore storage errors
|
|
864
|
+
}
|
|
865
|
+
return null;
|
|
866
|
+
}, [viewportStorageKey]);
|
|
867
|
+
|
|
868
|
+
const persistViewport = useCallback(
|
|
869
|
+
(viewport) => {
|
|
870
|
+
if (!viewport) return;
|
|
871
|
+
const { x, y, zoom } = viewport;
|
|
872
|
+
if (
|
|
873
|
+
!Number.isFinite(Number(x)) ||
|
|
874
|
+
!Number.isFinite(Number(y)) ||
|
|
875
|
+
!Number.isFinite(Number(zoom))
|
|
876
|
+
)
|
|
877
|
+
return;
|
|
878
|
+
try {
|
|
879
|
+
if (typeof window === "undefined") return;
|
|
880
|
+
window.sessionStorage.setItem(
|
|
881
|
+
viewportStorageKey,
|
|
882
|
+
JSON.stringify({ x: Number(x), y: Number(y), zoom: Number(zoom) })
|
|
883
|
+
);
|
|
884
|
+
} catch (e) {
|
|
885
|
+
// ignore storage errors
|
|
886
|
+
}
|
|
887
|
+
},
|
|
888
|
+
[viewportStorageKey]
|
|
889
|
+
);
|
|
890
|
+
|
|
656
891
|
const onInit = useCallback((instance) => {
|
|
657
892
|
rfInstanceRef.current = instance;
|
|
658
893
|
}, []);
|
|
659
894
|
|
|
660
895
|
const [selectedNodes, setSelectedNodes] = useState([]);
|
|
896
|
+
const pendingViewportRef = useRef(loadViewport());
|
|
897
|
+
const appliedViewportRef = useRef(false);
|
|
661
898
|
|
|
662
|
-
const handleSelectionChange = useCallback(({ nodes = [] }) => {
|
|
663
|
-
console.log("selection change", nodes);
|
|
899
|
+
const handleSelectionChange = useCallback(({ nodes = [], edges = [] }) => {
|
|
664
900
|
setSelectedNodes(nodes);
|
|
901
|
+
if (edges.length) setSelectedEdgeId(edges[0].id);
|
|
902
|
+
else setSelectedEdgeId(null);
|
|
665
903
|
}, []);
|
|
666
904
|
const handleKeyDelete = useCallback(
|
|
667
905
|
(e) => {
|
|
@@ -715,6 +953,16 @@ const WorkflowEditor = ({ data }) => {
|
|
|
715
953
|
refreshGraph(steps);
|
|
716
954
|
}, [steps, refreshGraph]);
|
|
717
955
|
|
|
956
|
+
useEffect(() => {
|
|
957
|
+
if (!rfInstanceRef.current) return;
|
|
958
|
+
if (!pendingViewportRef.current) return;
|
|
959
|
+
if (appliedViewportRef.current) return;
|
|
960
|
+
rfInstanceRef.current.setViewport(pendingViewportRef.current, {
|
|
961
|
+
duration: 0,
|
|
962
|
+
});
|
|
963
|
+
appliedViewportRef.current = true;
|
|
964
|
+
}, [nodes]);
|
|
965
|
+
|
|
718
966
|
useEffect(() => {
|
|
719
967
|
window.addEventListener("keydown", handleKeyDelete);
|
|
720
968
|
return () => window.removeEventListener("keydown", handleKeyDelete);
|
|
@@ -744,10 +992,12 @@ const WorkflowEditor = ({ data }) => {
|
|
|
744
992
|
const stepNode = stepById.get(node.id.replace("add-", ""));
|
|
745
993
|
if (!stepNode) return;
|
|
746
994
|
|
|
747
|
-
const nextX =
|
|
995
|
+
const nextX =
|
|
996
|
+
stepNode.position.x +
|
|
997
|
+
stepNode.width / 2 -
|
|
998
|
+
(node.width || ADD_NODE_SIZE) / 2;
|
|
748
999
|
|
|
749
|
-
const nextY =
|
|
750
|
-
stepNode.position.y + stepNode.height / 2 - (node.height || 24) / 2;
|
|
1000
|
+
const nextY = stepNode.position.y + stepNode.height + ADD_GAP;
|
|
751
1001
|
|
|
752
1002
|
const nextPosition = { x: nextX, y: nextY };
|
|
753
1003
|
|
|
@@ -817,7 +1067,6 @@ const WorkflowEditor = ({ data }) => {
|
|
|
817
1067
|
|
|
818
1068
|
const reload = useCallback(async () => {
|
|
819
1069
|
setLoading(true);
|
|
820
|
-
setModal(null);
|
|
821
1070
|
setError("");
|
|
822
1071
|
try {
|
|
823
1072
|
let fresh = await fetchJson(data.urls.data);
|
|
@@ -962,6 +1211,12 @@ const WorkflowEditor = ({ data }) => {
|
|
|
962
1211
|
async ({ stepId, initial_step, after_step } = {}) => {
|
|
963
1212
|
try {
|
|
964
1213
|
setError("");
|
|
1214
|
+
const descriptor = {
|
|
1215
|
+
stepId: stepId || null,
|
|
1216
|
+
initial_step: !!initial_step,
|
|
1217
|
+
after_step: after_step || null,
|
|
1218
|
+
};
|
|
1219
|
+
persistDrawerState(descriptor);
|
|
965
1220
|
const base = `${data.urls.stepForm}${stepId ? `/${stepId}` : ""}`;
|
|
966
1221
|
const url = new URL(base, window.location.origin);
|
|
967
1222
|
url.searchParams.set("render", "dialog");
|
|
@@ -970,10 +1225,11 @@ const WorkflowEditor = ({ data }) => {
|
|
|
970
1225
|
const res = await fetchJson(url.toString());
|
|
971
1226
|
setModal({ title: res.title, body: res.form, stepId: stepId || null });
|
|
972
1227
|
} catch (e) {
|
|
1228
|
+
clearDrawerState();
|
|
973
1229
|
setError(e.message);
|
|
974
1230
|
}
|
|
975
1231
|
},
|
|
976
|
-
[data.urls.stepForm, fetchJson]
|
|
1232
|
+
[clearDrawerState, data.urls.stepForm, fetchJson, persistDrawerState]
|
|
977
1233
|
);
|
|
978
1234
|
|
|
979
1235
|
const handleModalSubmit = useCallback(
|
|
@@ -994,9 +1250,10 @@ const WorkflowEditor = ({ data }) => {
|
|
|
994
1250
|
const json = await res.json();
|
|
995
1251
|
if (json.error) throw new Error(json.error);
|
|
996
1252
|
setModal(null);
|
|
1253
|
+
clearDrawerState();
|
|
997
1254
|
await reload();
|
|
998
1255
|
},
|
|
999
|
-
[data.csrfToken, data.urls.stepForm, reload]
|
|
1256
|
+
[clearDrawerState, data.csrfToken, data.urls.stepForm, reload]
|
|
1000
1257
|
);
|
|
1001
1258
|
|
|
1002
1259
|
useEffect(() => {
|
|
@@ -1005,8 +1262,11 @@ const WorkflowEditor = ({ data }) => {
|
|
|
1005
1262
|
if (!formEl) return undefined;
|
|
1006
1263
|
// Re-run show-if logic so action-specific fields render correctly
|
|
1007
1264
|
setTimeout(() => {
|
|
1008
|
-
if (typeof window !== "undefined"
|
|
1009
|
-
window.apply_showif();
|
|
1265
|
+
if (typeof window !== "undefined") {
|
|
1266
|
+
if (window.apply_showif) window.apply_showif();
|
|
1267
|
+
// Initialize Monaco and any other dynamic widgets inside the drawer
|
|
1268
|
+
if (window.initialize_page) window.initialize_page();
|
|
1269
|
+
}
|
|
1010
1270
|
}, 0);
|
|
1011
1271
|
|
|
1012
1272
|
const actionSelect = formEl.querySelector('[name="wf_action_name"]');
|
|
@@ -1035,6 +1295,19 @@ const WorkflowEditor = ({ data }) => {
|
|
|
1035
1295
|
};
|
|
1036
1296
|
}, [handleModalSubmit, modal]);
|
|
1037
1297
|
|
|
1298
|
+
useEffect(() => {
|
|
1299
|
+
if (typeof window === "undefined") return undefined;
|
|
1300
|
+
const raw = window.sessionStorage.getItem(drawerStorageKey);
|
|
1301
|
+
if (!raw) return undefined;
|
|
1302
|
+
try {
|
|
1303
|
+
const parsed = JSON.parse(raw);
|
|
1304
|
+
if (parsed && typeof parsed === "object") openStepForm(parsed);
|
|
1305
|
+
} catch (e) {
|
|
1306
|
+
clearDrawerState();
|
|
1307
|
+
}
|
|
1308
|
+
return undefined;
|
|
1309
|
+
}, [clearDrawerState, drawerStorageKey, openStepForm]);
|
|
1310
|
+
|
|
1038
1311
|
const updateConnection = useCallback(
|
|
1039
1312
|
async (payload) => {
|
|
1040
1313
|
const body = new URLSearchParams();
|
|
@@ -1054,6 +1327,9 @@ const WorkflowEditor = ({ data }) => {
|
|
|
1054
1327
|
|
|
1055
1328
|
const onConnect = useCallback(
|
|
1056
1329
|
async (connection) => {
|
|
1330
|
+
// Valid connection landed; clear pending source so we don't treat it as a cancel
|
|
1331
|
+
connectSourceRef.current = null;
|
|
1332
|
+
setSelectedEdgeId(connection.id || null);
|
|
1057
1333
|
const loop = connection.sourceHandle === "loop";
|
|
1058
1334
|
const targetName = nameById[connection.target];
|
|
1059
1335
|
if (connection.source === "start") {
|
|
@@ -1074,12 +1350,24 @@ const WorkflowEditor = ({ data }) => {
|
|
|
1074
1350
|
const srcStep = steps.find(
|
|
1075
1351
|
(s) => String(s.id) === String(connection.source)
|
|
1076
1352
|
);
|
|
1353
|
+
const tgtStep = steps.find(
|
|
1354
|
+
(s) => String(s.id) === String(connection.target)
|
|
1355
|
+
);
|
|
1077
1356
|
const rawNext = srcStep?.next_step
|
|
1078
1357
|
? String(srcStep.next_step).trim()
|
|
1079
1358
|
: "";
|
|
1080
1359
|
const simpleNextId = rawNext && idByName[rawNext];
|
|
1360
|
+
const targetHasNext = !!(
|
|
1361
|
+
tgtStep &&
|
|
1362
|
+
tgtStep.next_step &&
|
|
1363
|
+
String(tgtStep.next_step).trim()
|
|
1364
|
+
);
|
|
1081
1365
|
|
|
1082
|
-
if (
|
|
1366
|
+
if (
|
|
1367
|
+
simpleNextId &&
|
|
1368
|
+
simpleNextId !== connection.target &&
|
|
1369
|
+
!targetHasNext
|
|
1370
|
+
) {
|
|
1083
1371
|
// Rewire: source -> new target, new target -> old target name
|
|
1084
1372
|
await updateConnection({
|
|
1085
1373
|
step_id: connection.target,
|
|
@@ -1097,6 +1385,28 @@ const WorkflowEditor = ({ data }) => {
|
|
|
1097
1385
|
[idByName, nameById, reload, steps, updateConnection]
|
|
1098
1386
|
);
|
|
1099
1387
|
|
|
1388
|
+
const onConnectStart = useCallback((_, params) => {
|
|
1389
|
+
if (params?.nodeId && params?.handleType === "source") {
|
|
1390
|
+
connectSourceRef.current = params;
|
|
1391
|
+
}
|
|
1392
|
+
}, []);
|
|
1393
|
+
|
|
1394
|
+
const onConnectEnd = useCallback(
|
|
1395
|
+
(event) => {
|
|
1396
|
+
const pending = connectSourceRef.current;
|
|
1397
|
+
connectSourceRef.current = null;
|
|
1398
|
+
if (!pending) return;
|
|
1399
|
+
const targetEl = event?.target;
|
|
1400
|
+
if (targetEl?.classList?.contains("react-flow__pane")) {
|
|
1401
|
+
// Dropped on empty space: clear next_step mark as final/unconnected
|
|
1402
|
+
if (pending.nodeId && pending.handleId !== "loop-out") {
|
|
1403
|
+
onClearNext(pending.nodeId);
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
},
|
|
1407
|
+
[onClearNext]
|
|
1408
|
+
);
|
|
1409
|
+
|
|
1100
1410
|
const onEdgesChange = useCallback(
|
|
1101
1411
|
(changes) => {
|
|
1102
1412
|
const removed = changes.filter((c) => c.type === "remove");
|
|
@@ -1216,15 +1526,22 @@ const WorkflowEditor = ({ data }) => {
|
|
|
1216
1526
|
const onDelete = useCallback(
|
|
1217
1527
|
async (id) => {
|
|
1218
1528
|
if (!window.confirm(strings.confirmDelete)) return;
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1529
|
+
try {
|
|
1530
|
+
const formData = new FormData();
|
|
1531
|
+
formData.append("_csrf", data.csrfToken || "");
|
|
1532
|
+
await fetchJson(`${data.urls.deleteStep}/${id}`, {
|
|
1533
|
+
method: "POST",
|
|
1534
|
+
body: formData,
|
|
1535
|
+
});
|
|
1536
|
+
setModal(null);
|
|
1537
|
+
setError("");
|
|
1538
|
+
clearDrawerState();
|
|
1539
|
+
await reload();
|
|
1540
|
+
} catch (e) {
|
|
1541
|
+
setError(e.message);
|
|
1542
|
+
}
|
|
1226
1543
|
},
|
|
1227
|
-
[data.csrfToken, fetchJson, reload, strings.confirmDelete]
|
|
1544
|
+
[clearDrawerState, data.csrfToken, fetchJson, reload, strings.confirmDelete]
|
|
1228
1545
|
);
|
|
1229
1546
|
|
|
1230
1547
|
useEffect(() => {
|
|
@@ -1289,9 +1606,106 @@ const WorkflowEditor = ({ data }) => {
|
|
|
1289
1606
|
}, [onInsertBetween, setEdges, edges]);
|
|
1290
1607
|
|
|
1291
1608
|
const hasSteps = steps.length > 0;
|
|
1609
|
+
const shellClass = modal ? "wf-shell wf-shell--drawer-open" : "wf-shell";
|
|
1610
|
+
const shellStyle = { "--wf-drawer-width": `${drawerWidth}px` };
|
|
1611
|
+
const showMiniMap = !modal;
|
|
1612
|
+
const shouldFitView = !pendingViewportRef.current;
|
|
1613
|
+
|
|
1614
|
+
const onEdgeClick = useCallback((_, edge) => {
|
|
1615
|
+
setSelectedEdgeId(edge.id);
|
|
1616
|
+
}, []);
|
|
1617
|
+
|
|
1618
|
+
useEffect(() => {
|
|
1619
|
+
setEdges((edgs) =>
|
|
1620
|
+
edgs.map((e) => {
|
|
1621
|
+
const selected = e.id === selectedEdgeId;
|
|
1622
|
+
const nextClass = selected
|
|
1623
|
+
? `${e.className || ""} wf-edge-selected`.trim()
|
|
1624
|
+
: (e.className || "").replace(/\s*wf-edge-selected\b/, "");
|
|
1625
|
+
const nextData =
|
|
1626
|
+
e.type === "workflow-main" ? { ...(e.data || {}), selected } : e.data;
|
|
1627
|
+
if (nextClass === (e.className || "") && nextData === e.data) return e;
|
|
1628
|
+
return { ...e, className: nextClass, data: nextData };
|
|
1629
|
+
})
|
|
1630
|
+
);
|
|
1631
|
+
}, [selectedEdgeId, setEdges]);
|
|
1632
|
+
|
|
1633
|
+
const handleResize = useCallback((e) => {
|
|
1634
|
+
if (!resizingRef.current) return;
|
|
1635
|
+
const maxWidth = Math.min(900, Math.max(360, window.innerWidth - 40));
|
|
1636
|
+
const nextWidth = Math.min(
|
|
1637
|
+
Math.max(600, window.innerWidth - e.clientX),
|
|
1638
|
+
maxWidth
|
|
1639
|
+
);
|
|
1640
|
+
setDrawerWidth(nextWidth);
|
|
1641
|
+
}, []);
|
|
1642
|
+
|
|
1643
|
+
const stopResize = useCallback(() => {
|
|
1644
|
+
if (!resizingRef.current) return;
|
|
1645
|
+
resizingRef.current = false;
|
|
1646
|
+
document.body.style.userSelect = "";
|
|
1647
|
+
window.removeEventListener("pointermove", handleResize);
|
|
1648
|
+
window.removeEventListener("pointerup", stopResize);
|
|
1649
|
+
}, [handleResize]);
|
|
1650
|
+
|
|
1651
|
+
const startResize = useCallback(
|
|
1652
|
+
(e) => {
|
|
1653
|
+
if (!modal) return;
|
|
1654
|
+
resizingRef.current = true;
|
|
1655
|
+
document.body.style.userSelect = "none";
|
|
1656
|
+
window.addEventListener("pointermove", handleResize);
|
|
1657
|
+
window.addEventListener("pointerup", stopResize);
|
|
1658
|
+
e.preventDefault();
|
|
1659
|
+
},
|
|
1660
|
+
[handleResize, modal, stopResize]
|
|
1661
|
+
);
|
|
1662
|
+
|
|
1663
|
+
useEffect(() => () => stopResize(), [stopResize]);
|
|
1664
|
+
|
|
1665
|
+
const closeModal = useCallback(() => {
|
|
1666
|
+
setModal(null);
|
|
1667
|
+
setError("");
|
|
1668
|
+
clearDrawerState();
|
|
1669
|
+
}, [clearDrawerState]);
|
|
1670
|
+
|
|
1671
|
+
useLayoutEffect(() => {
|
|
1672
|
+
const shellEl = shellRef.current;
|
|
1673
|
+
const drawerEl = drawerRef.current;
|
|
1674
|
+
if (!shellEl) return undefined;
|
|
1675
|
+
const hasResizeObserver = typeof ResizeObserver !== "undefined";
|
|
1676
|
+
|
|
1677
|
+
const computePadding = () => {
|
|
1678
|
+
if (!drawerEl || !modal) {
|
|
1679
|
+
shellEl.style.removeProperty("--wf-shell-padding-right");
|
|
1680
|
+
return;
|
|
1681
|
+
}
|
|
1682
|
+
const shellRect = shellEl.getBoundingClientRect();
|
|
1683
|
+
const drawerRect = drawerEl.getBoundingClientRect();
|
|
1684
|
+
// Space already available to the right of the shell (e.g. due to sidebars)
|
|
1685
|
+
const naturalGap = Math.max(0, window.innerWidth - shellRect.right);
|
|
1686
|
+
const needed = Math.max(0, drawerRect.width - naturalGap) + 4;
|
|
1687
|
+
shellEl.style.setProperty("--wf-shell-padding-right", `${needed}px`);
|
|
1688
|
+
};
|
|
1689
|
+
|
|
1690
|
+
computePadding();
|
|
1691
|
+
|
|
1692
|
+
const resizeObserver = hasResizeObserver
|
|
1693
|
+
? new ResizeObserver(() => computePadding())
|
|
1694
|
+
: null;
|
|
1695
|
+
if (resizeObserver) {
|
|
1696
|
+
resizeObserver.observe(shellEl);
|
|
1697
|
+
if (drawerEl) resizeObserver.observe(drawerEl);
|
|
1698
|
+
}
|
|
1699
|
+
window.addEventListener("resize", computePadding);
|
|
1700
|
+
|
|
1701
|
+
return () => {
|
|
1702
|
+
if (resizeObserver) resizeObserver.disconnect();
|
|
1703
|
+
window.removeEventListener("resize", computePadding);
|
|
1704
|
+
};
|
|
1705
|
+
}, [drawerWidth, modal]);
|
|
1292
1706
|
|
|
1293
1707
|
return (
|
|
1294
|
-
<div className=
|
|
1708
|
+
<div className={shellClass} style={shellStyle} ref={shellRef}>
|
|
1295
1709
|
<div className="wf-toolbar">
|
|
1296
1710
|
<div className="wf-toolbar__left">
|
|
1297
1711
|
<strong>{strings.configure}</strong>
|
|
@@ -1335,31 +1749,36 @@ const WorkflowEditor = ({ data }) => {
|
|
|
1335
1749
|
onNodesChange={onNodesChangeWrapped}
|
|
1336
1750
|
onEdgesChange={onEdgesChange}
|
|
1337
1751
|
onConnect={onConnect}
|
|
1752
|
+
onConnectStart={onConnectStart}
|
|
1753
|
+
onConnectEnd={onConnectEnd}
|
|
1754
|
+
onEdgeClick={onEdgeClick}
|
|
1338
1755
|
onSelectionChange={handleSelectionChange}
|
|
1339
1756
|
nodeTypes={nodeTypes}
|
|
1340
1757
|
edgeTypes={edgeTypes}
|
|
1341
1758
|
onInit={onInit}
|
|
1342
|
-
|
|
1759
|
+
defaultViewport={pendingViewportRef.current || undefined}
|
|
1760
|
+
fitView={shouldFitView}
|
|
1343
1761
|
proOptions={{ hideAttribution: true }}
|
|
1344
1762
|
defaultEdgeOptions={{ animated: true }}
|
|
1345
1763
|
colorMode={window._sc_lightmode || "light"}
|
|
1764
|
+
onMoveEnd={(_, viewport) => persistViewport(viewport)}
|
|
1346
1765
|
>
|
|
1347
|
-
<MiniMap pannable zoomable />
|
|
1766
|
+
{showMiniMap && <MiniMap pannable zoomable />}
|
|
1348
1767
|
<Controls />
|
|
1349
1768
|
<Background gap={16} />
|
|
1350
1769
|
</ReactFlow>
|
|
1351
1770
|
</div>
|
|
1352
|
-
<
|
|
1771
|
+
<StepDrawer
|
|
1353
1772
|
modal={modal}
|
|
1354
1773
|
innerRef={modalRef}
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
setError("");
|
|
1358
|
-
}}
|
|
1774
|
+
outerRef={drawerRef}
|
|
1775
|
+
onClose={closeModal}
|
|
1359
1776
|
submitting={savingModal}
|
|
1360
1777
|
error={error && modal ? error : ""}
|
|
1361
1778
|
data={data}
|
|
1362
1779
|
onDelete={onDelete}
|
|
1780
|
+
drawerWidth={drawerWidth}
|
|
1781
|
+
onResizeStart={startResize}
|
|
1363
1782
|
/>
|
|
1364
1783
|
</div>
|
|
1365
1784
|
);
|