@saltcorn/workflow-editor 1.6.0-alpha.10 → 1.6.0-alpha.12
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 +2 -53598
- package/dist/workflow_bundle.js.map +1 -1
- package/package.json +7 -5
- package/src/WorkflowEditor.jsx +641 -139
- package/src/workflow.css +2 -0
package/src/WorkflowEditor.jsx
CHANGED
|
@@ -6,6 +6,7 @@ import React, {
|
|
|
6
6
|
useLayoutEffect,
|
|
7
7
|
useMemo,
|
|
8
8
|
} from "react";
|
|
9
|
+
import { createPortal } from "react-dom";
|
|
9
10
|
import {
|
|
10
11
|
Background,
|
|
11
12
|
Controls,
|
|
@@ -487,9 +488,16 @@ const buildGraph = (
|
|
|
487
488
|
id: `e-start-${initial.id}`,
|
|
488
489
|
source: "start",
|
|
489
490
|
target: String(initial.id),
|
|
490
|
-
type: "
|
|
491
|
+
type: "workflow-main",
|
|
491
492
|
animated: true,
|
|
492
493
|
markerEnd: makeMarker(),
|
|
494
|
+
data: {
|
|
495
|
+
startLabel: strings.start,
|
|
496
|
+
sourceStepName: "start",
|
|
497
|
+
sourceId: "start",
|
|
498
|
+
targetId: String(initial.id),
|
|
499
|
+
canInsertBetween: true,
|
|
500
|
+
},
|
|
493
501
|
});
|
|
494
502
|
else {
|
|
495
503
|
const addId = "add-start";
|
|
@@ -620,7 +628,7 @@ const buildGraph = (
|
|
|
620
628
|
});
|
|
621
629
|
|
|
622
630
|
const addNodes = [];
|
|
623
|
-
if (!
|
|
631
|
+
if (!initial) {
|
|
624
632
|
const addId = "add-start";
|
|
625
633
|
addNodes.push({
|
|
626
634
|
id: addId,
|
|
@@ -706,10 +714,16 @@ const StepDrawer = ({
|
|
|
706
714
|
error,
|
|
707
715
|
data,
|
|
708
716
|
onDelete,
|
|
717
|
+
onCopy,
|
|
709
718
|
drawerWidth,
|
|
710
719
|
onResizeStart,
|
|
711
720
|
}) => {
|
|
712
721
|
const isOpen = !!modal;
|
|
722
|
+
const stopDrawerKeyPropagation = useCallback((e) => {
|
|
723
|
+
e.stopPropagation();
|
|
724
|
+
if (e?.nativeEvent?.stopImmediatePropagation)
|
|
725
|
+
e.nativeEvent.stopImmediatePropagation();
|
|
726
|
+
}, []);
|
|
713
727
|
return (
|
|
714
728
|
<div
|
|
715
729
|
ref={outerRef}
|
|
@@ -719,6 +733,8 @@ const StepDrawer = ({
|
|
|
719
733
|
aria-hidden={!isOpen}
|
|
720
734
|
aria-label={modal?.title || data?.strings?.configure}
|
|
721
735
|
style={{ "--wf-drawer-width": `${drawerWidth}px` }}
|
|
736
|
+
onKeyDown={stopDrawerKeyPropagation}
|
|
737
|
+
onKeyUp={stopDrawerKeyPropagation}
|
|
722
738
|
>
|
|
723
739
|
{modal ? (
|
|
724
740
|
<>
|
|
@@ -743,31 +759,46 @@ const StepDrawer = ({
|
|
|
743
759
|
) : null}
|
|
744
760
|
</div>
|
|
745
761
|
<div className="wf-drawer__footer d-flex justify-content-end gap-2">
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
e
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
762
|
+
{modal.stepId && (
|
|
763
|
+
<button
|
|
764
|
+
className="btn btn-sm btn-outline-danger"
|
|
765
|
+
onClick={(e) => {
|
|
766
|
+
e.stopPropagation();
|
|
767
|
+
onDelete(modal.stepId);
|
|
768
|
+
}}
|
|
769
|
+
>
|
|
770
|
+
{data.strings.deleteStep}
|
|
771
|
+
</button>
|
|
772
|
+
)}
|
|
773
|
+
{modal.stepId ? (
|
|
774
|
+
<button
|
|
775
|
+
className="btn btn-sm btn-outline-secondary"
|
|
776
|
+
onClick={(e) => {
|
|
777
|
+
e.stopPropagation();
|
|
778
|
+
onCopy(modal.stepId);
|
|
779
|
+
}}
|
|
780
|
+
>
|
|
781
|
+
{data.strings.copyStep || "Copy"}
|
|
782
|
+
</button>
|
|
783
|
+
) : null}
|
|
755
784
|
<button className="btn btn-secondary" onClick={onClose}>
|
|
756
785
|
Close
|
|
757
786
|
</button>
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
787
|
+
{!modal.stepId && (
|
|
788
|
+
<button
|
|
789
|
+
className="btn btn-primary"
|
|
790
|
+
onClick={() =>
|
|
791
|
+
innerRef.current
|
|
792
|
+
?.querySelector("form")
|
|
793
|
+
?.dispatchEvent(
|
|
794
|
+
new Event("submit", { cancelable: true, bubbles: true })
|
|
795
|
+
)
|
|
796
|
+
}
|
|
797
|
+
disabled={submitting}
|
|
798
|
+
>
|
|
799
|
+
{submitting ? "Saving..." : "Save"}
|
|
800
|
+
</button>
|
|
801
|
+
)}
|
|
771
802
|
</div>
|
|
772
803
|
</>
|
|
773
804
|
) : null}
|
|
@@ -807,6 +838,13 @@ const WorkflowEditor = ({ data }) => {
|
|
|
807
838
|
const resizingRef = useRef(false);
|
|
808
839
|
const connectSourceRef = useRef(null);
|
|
809
840
|
const [selectedEdgeId, setSelectedEdgeId] = useState(null);
|
|
841
|
+
const autoSaveTimerRef = useRef(null);
|
|
842
|
+
const autoSavingRef = useRef(false);
|
|
843
|
+
const nodesRef = useRef([]);
|
|
844
|
+
const [headerHost, setHeaderHost] = useState(null);
|
|
845
|
+
const [shellHeight, setShellHeight] = useState(null);
|
|
846
|
+
const lastHeightRef = useRef(null);
|
|
847
|
+
const heightDebounceRef = useRef(null);
|
|
810
848
|
|
|
811
849
|
const drawerStorageKey = useMemo(
|
|
812
850
|
() => `wf-drawer:${data?.trigger?.id || "default"}`,
|
|
@@ -819,6 +857,57 @@ const WorkflowEditor = ({ data }) => {
|
|
|
819
857
|
);
|
|
820
858
|
|
|
821
859
|
const rfInstanceRef = useRef(null);
|
|
860
|
+
const headerHtmlRef = useRef("");
|
|
861
|
+
|
|
862
|
+
const recomputeShellHeight = useCallback(() => {
|
|
863
|
+
if (typeof window === "undefined") return;
|
|
864
|
+
const shellEl = shellRef.current;
|
|
865
|
+
if (!shellEl) return;
|
|
866
|
+
|
|
867
|
+
const wrapperEl = shellEl.closest(".workflow-editor-wrapper");
|
|
868
|
+
if (!wrapperEl) return;
|
|
869
|
+
|
|
870
|
+
const afterEl = wrapperEl.nextElementSibling;
|
|
871
|
+
const cardBodyEl = shellEl.closest(".card-body");
|
|
872
|
+
if (!cardBodyEl) return;
|
|
873
|
+
|
|
874
|
+
const minHeight = 320;
|
|
875
|
+
const bottomMargin = 28;
|
|
876
|
+
|
|
877
|
+
const cardBodyRect = cardBodyEl.getBoundingClientRect();
|
|
878
|
+
const styles = window.getComputedStyle(cardBodyEl);
|
|
879
|
+
const cardBodyPaddingTop = parseFloat(styles.paddingTop) || 0;
|
|
880
|
+
const cardBodyPaddingBottom = parseFloat(styles.paddingBottom) || 0;
|
|
881
|
+
|
|
882
|
+
const effectiveTop = Math.max(0, cardBodyRect.top);
|
|
883
|
+
|
|
884
|
+
let footerHeight = 0;
|
|
885
|
+
if (afterEl && typeof afterEl.getBoundingClientRect === "function") {
|
|
886
|
+
const afterRect = afterEl.getBoundingClientRect();
|
|
887
|
+
const afterStyles = window.getComputedStyle(afterEl);
|
|
888
|
+
const mt = parseFloat(afterStyles.marginTop) || 0;
|
|
889
|
+
const mb = parseFloat(afterStyles.marginBottom) || 0;
|
|
890
|
+
footerHeight = afterRect.height + mt + mb;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
const availableHeight =
|
|
894
|
+
window.innerHeight -
|
|
895
|
+
effectiveTop -
|
|
896
|
+
bottomMargin -
|
|
897
|
+
cardBodyPaddingTop -
|
|
898
|
+
cardBodyPaddingBottom -
|
|
899
|
+
footerHeight;
|
|
900
|
+
|
|
901
|
+
const wrapperHeight = Math.max(minHeight, availableHeight);
|
|
902
|
+
|
|
903
|
+
if (lastHeightRef.current !== wrapperHeight) {
|
|
904
|
+
lastHeightRef.current = wrapperHeight;
|
|
905
|
+
if (wrapperEl) {
|
|
906
|
+
wrapperEl.style.height = `${wrapperHeight}px`;
|
|
907
|
+
}
|
|
908
|
+
if (Number.isFinite(wrapperHeight)) setShellHeight(wrapperHeight);
|
|
909
|
+
}
|
|
910
|
+
}, []);
|
|
822
911
|
|
|
823
912
|
const persistDrawerState = useCallback(
|
|
824
913
|
(payload) => {
|
|
@@ -914,10 +1003,12 @@ const WorkflowEditor = ({ data }) => {
|
|
|
914
1003
|
)
|
|
915
1004
|
return;
|
|
916
1005
|
if (e.key !== "Delete" && e.key !== "Backspace") return;
|
|
917
|
-
const
|
|
918
|
-
|
|
1006
|
+
const stepIds = selectedNodes
|
|
1007
|
+
.filter((n) => n.type === "step")
|
|
1008
|
+
.map((n) => n.id);
|
|
1009
|
+
if (!stepIds.length) return;
|
|
919
1010
|
e.preventDefault();
|
|
920
|
-
onDelete(
|
|
1011
|
+
onDelete(stepIds);
|
|
921
1012
|
},
|
|
922
1013
|
[modal, onDelete, selectedNodes]
|
|
923
1014
|
);
|
|
@@ -953,6 +1044,10 @@ const WorkflowEditor = ({ data }) => {
|
|
|
953
1044
|
refreshGraph(steps);
|
|
954
1045
|
}, [steps, refreshGraph]);
|
|
955
1046
|
|
|
1047
|
+
useEffect(() => {
|
|
1048
|
+
nodesRef.current = nodes;
|
|
1049
|
+
}, [nodes]);
|
|
1050
|
+
|
|
956
1051
|
useEffect(() => {
|
|
957
1052
|
if (!rfInstanceRef.current) return;
|
|
958
1053
|
if (!pendingViewportRef.current) return;
|
|
@@ -1070,25 +1165,33 @@ const WorkflowEditor = ({ data }) => {
|
|
|
1070
1165
|
setError("");
|
|
1071
1166
|
try {
|
|
1072
1167
|
let fresh = await fetchJson(data.urls.data);
|
|
1168
|
+
let updatedSteps = null;
|
|
1073
1169
|
|
|
1074
1170
|
// If a step was just added via an adder node, wire up next_step
|
|
1075
1171
|
if (pendingAddRef.current) {
|
|
1076
|
-
const { afterStepId, prevIds, insertBetween } =
|
|
1172
|
+
const { afterStepId, prevIds, insertBetween, fromStart } =
|
|
1173
|
+
pendingAddRef.current;
|
|
1077
1174
|
const newSteps = (fresh.steps || []).filter(
|
|
1078
1175
|
(s) => !prevIds.has(String(s.id))
|
|
1079
1176
|
);
|
|
1080
1177
|
if (newSteps.length === 1) {
|
|
1081
1178
|
const newStep = newSteps[0];
|
|
1082
1179
|
if (insertBetween && insertBetween.originalTargetName) {
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1180
|
+
if (fromStart) {
|
|
1181
|
+
await updateConnection({
|
|
1182
|
+
step_id: newStep.id,
|
|
1183
|
+
initial_step: true,
|
|
1184
|
+
});
|
|
1185
|
+
} else {
|
|
1186
|
+
await updateConnection({
|
|
1187
|
+
step_id: newStep.id,
|
|
1188
|
+
next_step: insertBetween.originalTargetName,
|
|
1189
|
+
});
|
|
1190
|
+
await updateConnection({
|
|
1191
|
+
step_id: afterStepId,
|
|
1192
|
+
next_step: newStep.name,
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1092
1195
|
} else {
|
|
1093
1196
|
await updateConnection({
|
|
1094
1197
|
step_id: afterStepId,
|
|
@@ -1098,20 +1201,38 @@ const WorkflowEditor = ({ data }) => {
|
|
|
1098
1201
|
|
|
1099
1202
|
// Re-fetch so we get the updated connections reflected in steps
|
|
1100
1203
|
fresh = await fetchJson(data.urls.data);
|
|
1204
|
+
|
|
1205
|
+
if (insertBetween && insertBetween.originalTargetName) {
|
|
1206
|
+
updatedSteps = await applyInsertBetweenLayout(fresh.steps || [], {
|
|
1207
|
+
newStep,
|
|
1208
|
+
afterStepId,
|
|
1209
|
+
targetName: insertBetween.originalTargetName,
|
|
1210
|
+
targetId: insertBetween.targetId,
|
|
1211
|
+
snapshot: insertBetween.positionsSnapshot,
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1101
1214
|
}
|
|
1102
1215
|
pendingAddRef.current = null;
|
|
1103
1216
|
}
|
|
1104
1217
|
|
|
1105
|
-
|
|
1218
|
+
const nextSteps = updatedSteps || fresh.steps || [];
|
|
1219
|
+
setSteps(nextSteps);
|
|
1106
1220
|
|
|
1107
1221
|
setMessage(strings.refresh);
|
|
1108
1222
|
setTimeout(() => setMessage(""), 1200);
|
|
1223
|
+
return nextSteps;
|
|
1109
1224
|
} catch (e) {
|
|
1110
1225
|
setError(e.message);
|
|
1111
1226
|
} finally {
|
|
1112
1227
|
setLoading(false);
|
|
1113
1228
|
}
|
|
1114
|
-
}, [
|
|
1229
|
+
}, [
|
|
1230
|
+
applyInsertBetweenLayout,
|
|
1231
|
+
data.urls.data,
|
|
1232
|
+
fetchJson,
|
|
1233
|
+
strings.refresh,
|
|
1234
|
+
updateConnection,
|
|
1235
|
+
]);
|
|
1115
1236
|
|
|
1116
1237
|
const persistPositions = useCallback(
|
|
1117
1238
|
async (positions = []) => {
|
|
@@ -1165,6 +1286,104 @@ const WorkflowEditor = ({ data }) => {
|
|
|
1165
1286
|
[persistPositions]
|
|
1166
1287
|
);
|
|
1167
1288
|
|
|
1289
|
+
const applyLocalPositionUpdates = useCallback(
|
|
1290
|
+
(updates = []) => {
|
|
1291
|
+
if (!updates.length) return;
|
|
1292
|
+
setNodes((nds) =>
|
|
1293
|
+
nds.map((n) => {
|
|
1294
|
+
const hit = updates.find((u) => String(u.id) === String(n.id));
|
|
1295
|
+
return hit
|
|
1296
|
+
? { ...n, position: { ...(n.position || {}), x: hit.x, y: hit.y } }
|
|
1297
|
+
: n;
|
|
1298
|
+
})
|
|
1299
|
+
);
|
|
1300
|
+
},
|
|
1301
|
+
[setNodes]
|
|
1302
|
+
);
|
|
1303
|
+
|
|
1304
|
+
const applyInsertBetweenLayout = useCallback(
|
|
1305
|
+
async (
|
|
1306
|
+
freshSteps,
|
|
1307
|
+
{ newStep, afterStepId, targetName, targetId, snapshot }
|
|
1308
|
+
) => {
|
|
1309
|
+
if (!newStep || !snapshot) return null;
|
|
1310
|
+
const snap = snapshot || {};
|
|
1311
|
+
const newStepSize = getWorkflowSize(newStep) || {
|
|
1312
|
+
width: DEFAULT_NODE_WIDTH,
|
|
1313
|
+
height: DEFAULT_NODE_HEIGHT,
|
|
1314
|
+
};
|
|
1315
|
+
const sourceSnap = snap[String(afterStepId)];
|
|
1316
|
+
const resolvedTargetId =
|
|
1317
|
+
targetId || freshSteps.find((s) => s.name === targetName)?.id;
|
|
1318
|
+
const targetSnap = resolvedTargetId
|
|
1319
|
+
? snap[String(resolvedTargetId)]
|
|
1320
|
+
: null;
|
|
1321
|
+
const shiftAmount = newStepSize.height + V_GAP;
|
|
1322
|
+
const fallbackY = sourceSnap?.y ?? 0;
|
|
1323
|
+
const fallbackX = sourceSnap?.x ?? targetSnap?.x ?? 0;
|
|
1324
|
+
const newY = targetSnap?.y ?? fallbackY;
|
|
1325
|
+
const newX =
|
|
1326
|
+
sourceSnap && targetSnap
|
|
1327
|
+
? (sourceSnap.x + targetSnap.x) / 2
|
|
1328
|
+
: (targetSnap?.x ?? sourceSnap?.x ?? fallbackX);
|
|
1329
|
+
|
|
1330
|
+
const newHalfWidth = (newStepSize.width || DEFAULT_NODE_WIDTH) / 2;
|
|
1331
|
+
const bandPadding = H_GAP / 2;
|
|
1332
|
+
const bandMinX = newX - newHalfWidth - bandPadding;
|
|
1333
|
+
const bandMaxX = newX + newHalfWidth + bandPadding;
|
|
1334
|
+
|
|
1335
|
+
const updates = [];
|
|
1336
|
+
freshSteps.forEach((step) => {
|
|
1337
|
+
const idStr = String(step.id);
|
|
1338
|
+
const nodeSnap = snap[idStr];
|
|
1339
|
+
|
|
1340
|
+
if (idStr === String(newStep.id)) {
|
|
1341
|
+
updates.push({ id: idStr, x: newX, y: newY });
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
if (!nodeSnap) return;
|
|
1346
|
+
|
|
1347
|
+
const nodeWidth = nodeSnap.width || DEFAULT_NODE_WIDTH;
|
|
1348
|
+
const nodeHalfWidth = nodeWidth / 2;
|
|
1349
|
+
const nodeMinX = nodeSnap.x - nodeHalfWidth;
|
|
1350
|
+
const nodeMaxX = nodeSnap.x + nodeHalfWidth;
|
|
1351
|
+
const overlapsBand = !(nodeMaxX < bandMinX || nodeMinX > bandMaxX);
|
|
1352
|
+
const shouldShift =
|
|
1353
|
+
nodeSnap.y >= newY && idStr !== String(afterStepId) && overlapsBand;
|
|
1354
|
+
|
|
1355
|
+
if (shouldShift) {
|
|
1356
|
+
updates.push({
|
|
1357
|
+
id: idStr,
|
|
1358
|
+
x: nodeSnap.x,
|
|
1359
|
+
y: nodeSnap.y + shiftAmount,
|
|
1360
|
+
});
|
|
1361
|
+
} else {
|
|
1362
|
+
updates.push({ id: idStr, x: nodeSnap.x, y: nodeSnap.y });
|
|
1363
|
+
}
|
|
1364
|
+
});
|
|
1365
|
+
if (!updates.length) return freshSteps;
|
|
1366
|
+
|
|
1367
|
+
applyLocalPositionUpdates(updates);
|
|
1368
|
+
|
|
1369
|
+
const stepsWithPositions = freshSteps.map((s) => {
|
|
1370
|
+
const hit = updates.find((u) => String(u.id) === String(s.id));
|
|
1371
|
+
if (!hit) return s;
|
|
1372
|
+
return {
|
|
1373
|
+
...s,
|
|
1374
|
+
configuration: {
|
|
1375
|
+
...(s.configuration || {}),
|
|
1376
|
+
workflow_position: { x: hit.x, y: hit.y },
|
|
1377
|
+
},
|
|
1378
|
+
};
|
|
1379
|
+
});
|
|
1380
|
+
|
|
1381
|
+
await persistPositions(updates);
|
|
1382
|
+
return stepsWithPositions;
|
|
1383
|
+
},
|
|
1384
|
+
[applyLocalPositionUpdates, persistPositions]
|
|
1385
|
+
);
|
|
1386
|
+
|
|
1168
1387
|
useEffect(() => {
|
|
1169
1388
|
return () => {
|
|
1170
1389
|
if (positionDebounceRef.current)
|
|
@@ -1220,6 +1439,7 @@ const WorkflowEditor = ({ data }) => {
|
|
|
1220
1439
|
const base = `${data.urls.stepForm}${stepId ? `/${stepId}` : ""}`;
|
|
1221
1440
|
const url = new URL(base, window.location.origin);
|
|
1222
1441
|
url.searchParams.set("render", "dialog");
|
|
1442
|
+
url.searchParams.set("vw", window.innerWidth);
|
|
1223
1443
|
if (initial_step) url.searchParams.set("initial_step", "true");
|
|
1224
1444
|
if (after_step) url.searchParams.set("after_step", after_step);
|
|
1225
1445
|
const res = await fetchJson(url.toString());
|
|
@@ -1232,8 +1452,8 @@ const WorkflowEditor = ({ data }) => {
|
|
|
1232
1452
|
[clearDrawerState, data.urls.stepForm, fetchJson, persistDrawerState]
|
|
1233
1453
|
);
|
|
1234
1454
|
|
|
1235
|
-
const
|
|
1236
|
-
async (formEl) => {
|
|
1455
|
+
const submitStepForm = useCallback(
|
|
1456
|
+
async (formEl, { closeOnSuccess = true } = {}) => {
|
|
1237
1457
|
if (!formEl) return;
|
|
1238
1458
|
setSavingModal(true);
|
|
1239
1459
|
setError("");
|
|
@@ -1249,9 +1469,12 @@ const WorkflowEditor = ({ data }) => {
|
|
|
1249
1469
|
});
|
|
1250
1470
|
const json = await res.json();
|
|
1251
1471
|
if (json.error) throw new Error(json.error);
|
|
1252
|
-
|
|
1253
|
-
|
|
1472
|
+
if (closeOnSuccess) {
|
|
1473
|
+
setModal(null);
|
|
1474
|
+
clearDrawerState();
|
|
1475
|
+
}
|
|
1254
1476
|
await reload();
|
|
1477
|
+
setSavingModal(false);
|
|
1255
1478
|
},
|
|
1256
1479
|
[clearDrawerState, data.csrfToken, data.urls.stepForm, reload]
|
|
1257
1480
|
);
|
|
@@ -1270,6 +1493,7 @@ const WorkflowEditor = ({ data }) => {
|
|
|
1270
1493
|
}, 0);
|
|
1271
1494
|
|
|
1272
1495
|
const actionSelect = formEl.querySelector('[name="wf_action_name"]');
|
|
1496
|
+
const allowAutoSave = !!modal?.stepId; // avoid auto-save for brand new steps to prevent insert errors
|
|
1273
1497
|
const handleActionChange = () => {
|
|
1274
1498
|
if (typeof window !== "undefined" && window.apply_showif)
|
|
1275
1499
|
window.apply_showif();
|
|
@@ -1277,10 +1501,28 @@ const WorkflowEditor = ({ data }) => {
|
|
|
1277
1501
|
if (actionSelect)
|
|
1278
1502
|
actionSelect.addEventListener("change", handleActionChange);
|
|
1279
1503
|
|
|
1504
|
+
const scheduleAutoSave = allowAutoSave
|
|
1505
|
+
? () => {
|
|
1506
|
+
if (autoSavingRef.current) return;
|
|
1507
|
+
if (autoSaveTimerRef.current) clearTimeout(autoSaveTimerRef.current);
|
|
1508
|
+
autoSaveTimerRef.current = setTimeout(async () => {
|
|
1509
|
+
autoSavingRef.current = true;
|
|
1510
|
+
try {
|
|
1511
|
+
await submitStepForm(formEl, { closeOnSuccess: false });
|
|
1512
|
+
} catch (err) {
|
|
1513
|
+
setError(err.message);
|
|
1514
|
+
} finally {
|
|
1515
|
+
autoSavingRef.current = false;
|
|
1516
|
+
setSavingModal(false);
|
|
1517
|
+
}
|
|
1518
|
+
}, 800);
|
|
1519
|
+
}
|
|
1520
|
+
: null;
|
|
1521
|
+
|
|
1280
1522
|
const submitHandler = async (e) => {
|
|
1281
1523
|
e.preventDefault();
|
|
1282
1524
|
try {
|
|
1283
|
-
await
|
|
1525
|
+
await submitStepForm(formEl);
|
|
1284
1526
|
} catch (err) {
|
|
1285
1527
|
setError(err.message);
|
|
1286
1528
|
} finally {
|
|
@@ -1288,12 +1530,21 @@ const WorkflowEditor = ({ data }) => {
|
|
|
1288
1530
|
}
|
|
1289
1531
|
};
|
|
1290
1532
|
formEl.addEventListener("submit", submitHandler);
|
|
1533
|
+
if (scheduleAutoSave) {
|
|
1534
|
+
formEl.addEventListener("input", scheduleAutoSave);
|
|
1535
|
+
formEl.addEventListener("change", scheduleAutoSave);
|
|
1536
|
+
}
|
|
1291
1537
|
return () => {
|
|
1538
|
+
if (autoSaveTimerRef.current) clearTimeout(autoSaveTimerRef.current);
|
|
1292
1539
|
formEl.removeEventListener("submit", submitHandler);
|
|
1540
|
+
if (scheduleAutoSave) {
|
|
1541
|
+
formEl.removeEventListener("input", scheduleAutoSave);
|
|
1542
|
+
formEl.removeEventListener("change", scheduleAutoSave);
|
|
1543
|
+
}
|
|
1293
1544
|
if (actionSelect)
|
|
1294
1545
|
actionSelect.removeEventListener("change", handleActionChange);
|
|
1295
1546
|
};
|
|
1296
|
-
}, [
|
|
1547
|
+
}, [modal, submitStepForm]);
|
|
1297
1548
|
|
|
1298
1549
|
useEffect(() => {
|
|
1299
1550
|
if (typeof window === "undefined") return undefined;
|
|
@@ -1400,16 +1651,32 @@ const WorkflowEditor = ({ data }) => {
|
|
|
1400
1651
|
if (targetEl?.classList?.contains("react-flow__pane")) {
|
|
1401
1652
|
// Dropped on empty space: clear next_step mark as final/unconnected
|
|
1402
1653
|
if (pending.nodeId && pending.handleId !== "loop-out") {
|
|
1403
|
-
|
|
1654
|
+
if (pending.nodeId === "start") {
|
|
1655
|
+
const currentInitial = steps.find((s) => s.initial_step);
|
|
1656
|
+
if (currentInitial) {
|
|
1657
|
+
updateConnection({
|
|
1658
|
+
step_id: currentInitial.id,
|
|
1659
|
+
initial_step: false,
|
|
1660
|
+
})
|
|
1661
|
+
.then(() => reload())
|
|
1662
|
+
.catch((e) => setError(e.message));
|
|
1663
|
+
}
|
|
1664
|
+
} else {
|
|
1665
|
+
onClearNext(pending.nodeId);
|
|
1666
|
+
}
|
|
1404
1667
|
}
|
|
1405
1668
|
}
|
|
1406
1669
|
},
|
|
1407
|
-
[onClearNext]
|
|
1670
|
+
[onClearNext, reload, steps, updateConnection]
|
|
1408
1671
|
);
|
|
1409
1672
|
|
|
1410
1673
|
const onEdgesChange = useCallback(
|
|
1411
1674
|
(changes) => {
|
|
1412
|
-
|
|
1675
|
+
// Block edge deletions while a modal is open so ReactFlow cannot remove links under dialogs
|
|
1676
|
+
const safeChanges = modal
|
|
1677
|
+
? changes.filter((c) => c.type !== "remove")
|
|
1678
|
+
: changes;
|
|
1679
|
+
const removed = safeChanges.filter((c) => c.type === "remove");
|
|
1413
1680
|
if (removed.length) {
|
|
1414
1681
|
const updates = [];
|
|
1415
1682
|
removed.forEach((chg) => {
|
|
@@ -1438,13 +1705,18 @@ const WorkflowEditor = ({ data }) => {
|
|
|
1438
1705
|
.then(() => reload())
|
|
1439
1706
|
.catch((e) => setError(e.message));
|
|
1440
1707
|
}
|
|
1441
|
-
rfOnEdgesChange(
|
|
1708
|
+
rfOnEdgesChange(safeChanges);
|
|
1442
1709
|
},
|
|
1443
|
-
[edges, reload, rfOnEdgesChange, updateConnection]
|
|
1710
|
+
[edges, modal, reload, rfOnEdgesChange, updateConnection]
|
|
1444
1711
|
);
|
|
1445
1712
|
|
|
1446
1713
|
const onNodesChangeWrapped = useCallback(
|
|
1447
1714
|
(changes) => {
|
|
1715
|
+
// Prevent node deletion by reactflow while drawer is open
|
|
1716
|
+
const safeChanges = modal
|
|
1717
|
+
? changes.filter((c) => c.type !== "remove")
|
|
1718
|
+
: changes;
|
|
1719
|
+
|
|
1448
1720
|
const finishedPositions = changes
|
|
1449
1721
|
.filter((c) => c.type === "position" && c.position && c.dragging)
|
|
1450
1722
|
.map((c) => ({ id: c.id, x: c.position.x, y: c.position.y }));
|
|
@@ -1472,9 +1744,15 @@ const WorkflowEditor = ({ data }) => {
|
|
|
1472
1744
|
);
|
|
1473
1745
|
if (stepPositions.length) schedulePersistPositions(stepPositions);
|
|
1474
1746
|
}
|
|
1475
|
-
onNodesChange(
|
|
1747
|
+
onNodesChange(safeChanges);
|
|
1476
1748
|
},
|
|
1477
|
-
[
|
|
1749
|
+
[
|
|
1750
|
+
modal,
|
|
1751
|
+
onNodesChange,
|
|
1752
|
+
schedulePersistPositions,
|
|
1753
|
+
setAddPositions,
|
|
1754
|
+
setStartPosition,
|
|
1755
|
+
]
|
|
1478
1756
|
);
|
|
1479
1757
|
|
|
1480
1758
|
const onAddAfter = useCallback(
|
|
@@ -1494,15 +1772,45 @@ const WorkflowEditor = ({ data }) => {
|
|
|
1494
1772
|
|
|
1495
1773
|
const onInsertBetween = useCallback(
|
|
1496
1774
|
({ source, target }) => {
|
|
1497
|
-
if (!source || !target
|
|
1775
|
+
if (!source || !target) return;
|
|
1498
1776
|
const originalTargetName = nameById[target];
|
|
1499
1777
|
if (!originalTargetName) return;
|
|
1778
|
+
const fromStart = source === "start";
|
|
1779
|
+
const sizeById = new Map(
|
|
1780
|
+
steps.map((s) => [
|
|
1781
|
+
String(s.id),
|
|
1782
|
+
getWorkflowSize(s) || {
|
|
1783
|
+
width: DEFAULT_NODE_WIDTH,
|
|
1784
|
+
height: DEFAULT_NODE_HEIGHT,
|
|
1785
|
+
},
|
|
1786
|
+
])
|
|
1787
|
+
);
|
|
1788
|
+
const positionsSnapshot = {};
|
|
1789
|
+
nodesRef.current
|
|
1790
|
+
.filter((n) => n.type === "step")
|
|
1791
|
+
.forEach((n) => {
|
|
1792
|
+
const size = sizeById.get(String(n.id)) || {
|
|
1793
|
+
width: DEFAULT_NODE_WIDTH,
|
|
1794
|
+
height: DEFAULT_NODE_HEIGHT,
|
|
1795
|
+
};
|
|
1796
|
+
positionsSnapshot[String(n.id)] = {
|
|
1797
|
+
x: n.position?.x ?? 0,
|
|
1798
|
+
y: n.position?.y ?? 0,
|
|
1799
|
+
width: size.width || DEFAULT_NODE_WIDTH,
|
|
1800
|
+
height: size.height || DEFAULT_NODE_HEIGHT,
|
|
1801
|
+
};
|
|
1802
|
+
});
|
|
1500
1803
|
pendingAddRef.current = {
|
|
1501
|
-
afterStepId: source,
|
|
1804
|
+
afterStepId: fromStart ? "start" : source,
|
|
1502
1805
|
prevIds: new Set(steps.map((s) => String(s.id))),
|
|
1503
|
-
|
|
1806
|
+
fromStart,
|
|
1807
|
+
insertBetween: {
|
|
1808
|
+
originalTargetName,
|
|
1809
|
+
targetId: target,
|
|
1810
|
+
positionsSnapshot,
|
|
1811
|
+
},
|
|
1504
1812
|
};
|
|
1505
|
-
openStepForm({ after_step: source });
|
|
1813
|
+
openStepForm(fromStart ? { initial_step: true } : { after_step: source });
|
|
1506
1814
|
},
|
|
1507
1815
|
[nameById, openStepForm, steps]
|
|
1508
1816
|
);
|
|
@@ -1515,6 +1823,127 @@ const WorkflowEditor = ({ data }) => {
|
|
|
1515
1823
|
[reload, updateConnection]
|
|
1516
1824
|
);
|
|
1517
1825
|
|
|
1826
|
+
const onCopy = useCallback(
|
|
1827
|
+
async (id) => {
|
|
1828
|
+
try {
|
|
1829
|
+
setError("");
|
|
1830
|
+
const res = await fetchJson(data.urls.copy, {
|
|
1831
|
+
method: "POST",
|
|
1832
|
+
body: JSON.stringify({ step_id: id }),
|
|
1833
|
+
headers: {
|
|
1834
|
+
"X-CSRF-Token": data.csrfToken || "",
|
|
1835
|
+
},
|
|
1836
|
+
});
|
|
1837
|
+
|
|
1838
|
+
if (!res || res.success !== "ok" || !res.step) {
|
|
1839
|
+
await reload();
|
|
1840
|
+
return;
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
const copiedStep = res.step;
|
|
1844
|
+
const copiedName = copiedStep.name;
|
|
1845
|
+
|
|
1846
|
+
// Reload full workflow data so the new step has
|
|
1847
|
+
// the same serialized shape as all other steps
|
|
1848
|
+
const freshSteps = (await reload()) || [];
|
|
1849
|
+
if (!freshSteps.length) return;
|
|
1850
|
+
|
|
1851
|
+
const sourceStep = freshSteps.find((s) => String(s.id) === String(id));
|
|
1852
|
+
const newStep = freshSteps.find((s) => s.name === copiedName);
|
|
1853
|
+
|
|
1854
|
+
if (!sourceStep || !newStep || !newStep.id) return;
|
|
1855
|
+
|
|
1856
|
+
const sourceSize = getWorkflowSize(sourceStep) || {
|
|
1857
|
+
width: DEFAULT_NODE_WIDTH,
|
|
1858
|
+
height: DEFAULT_NODE_HEIGHT,
|
|
1859
|
+
};
|
|
1860
|
+
const newSize = getWorkflowSize(newStep) || {
|
|
1861
|
+
width: DEFAULT_NODE_WIDTH,
|
|
1862
|
+
height: DEFAULT_NODE_HEIGHT,
|
|
1863
|
+
};
|
|
1864
|
+
|
|
1865
|
+
const sourcePos = sourceStep.configuration?.workflow_position || {
|
|
1866
|
+
x: 0,
|
|
1867
|
+
y: 0,
|
|
1868
|
+
};
|
|
1869
|
+
|
|
1870
|
+
// Desired location: directly to the right of the source step
|
|
1871
|
+
const targetX = sourcePos.x + sourceSize.width + ADD_GAP;
|
|
1872
|
+
const targetY = sourcePos.y;
|
|
1873
|
+
|
|
1874
|
+
const updates = [];
|
|
1875
|
+
|
|
1876
|
+
// Push steps that are to the right of the insertion band
|
|
1877
|
+
const bandMinX = targetX - newSize.width * 0.25;
|
|
1878
|
+
const shiftX = newSize.width + ADD_GAP;
|
|
1879
|
+
const newHalfHeight = (newSize.height || DEFAULT_NODE_HEIGHT) / 2;
|
|
1880
|
+
const bandPaddingY = V_GAP / 2;
|
|
1881
|
+
const bandMinY = targetY - newHalfHeight - bandPaddingY;
|
|
1882
|
+
const bandMaxY = targetY + newHalfHeight + bandPaddingY;
|
|
1883
|
+
|
|
1884
|
+
freshSteps.forEach((s) => {
|
|
1885
|
+
const cfgPos = s.configuration?.workflow_position;
|
|
1886
|
+
if (!cfgPos) return;
|
|
1887
|
+
if (String(s.id) === String(newStep.id)) return;
|
|
1888
|
+
const stepSize = getWorkflowSize(s) || {
|
|
1889
|
+
width: DEFAULT_NODE_WIDTH,
|
|
1890
|
+
height: DEFAULT_NODE_HEIGHT,
|
|
1891
|
+
};
|
|
1892
|
+
const halfHeight = (stepSize.height || DEFAULT_NODE_HEIGHT) / 2;
|
|
1893
|
+
const nodeMinY = cfgPos.y - halfHeight;
|
|
1894
|
+
const nodeMaxY = cfgPos.y + halfHeight;
|
|
1895
|
+
const overlapsBandY = !(nodeMaxY < bandMinY || nodeMinY > bandMaxY);
|
|
1896
|
+
|
|
1897
|
+
if (cfgPos.x >= bandMinX && overlapsBandY) {
|
|
1898
|
+
updates.push({
|
|
1899
|
+
id: String(s.id),
|
|
1900
|
+
x: cfgPos.x + shiftX,
|
|
1901
|
+
y: cfgPos.y,
|
|
1902
|
+
});
|
|
1903
|
+
}
|
|
1904
|
+
});
|
|
1905
|
+
|
|
1906
|
+
// Position for the new copied step
|
|
1907
|
+
updates.push({
|
|
1908
|
+
id: String(newStep.id),
|
|
1909
|
+
x: targetX,
|
|
1910
|
+
y: targetY,
|
|
1911
|
+
});
|
|
1912
|
+
|
|
1913
|
+
if (!updates.length) return;
|
|
1914
|
+
|
|
1915
|
+
// Move nodes immediately in the UI
|
|
1916
|
+
applyLocalPositionUpdates(updates);
|
|
1917
|
+
|
|
1918
|
+
// Update local steps state with new positions
|
|
1919
|
+
const stepsWithPositions = freshSteps.map((s) => {
|
|
1920
|
+
const hit = updates.find((u) => u.id === String(s.id));
|
|
1921
|
+
if (!hit) return s;
|
|
1922
|
+
return {
|
|
1923
|
+
...s,
|
|
1924
|
+
configuration: {
|
|
1925
|
+
...(s.configuration || {}),
|
|
1926
|
+
workflow_position: { x: hit.x, y: hit.y },
|
|
1927
|
+
},
|
|
1928
|
+
};
|
|
1929
|
+
});
|
|
1930
|
+
|
|
1931
|
+
setSteps(stepsWithPositions);
|
|
1932
|
+
await persistPositions(updates);
|
|
1933
|
+
} catch (e) {
|
|
1934
|
+
setError(e.message);
|
|
1935
|
+
}
|
|
1936
|
+
},
|
|
1937
|
+
[
|
|
1938
|
+
data.csrfToken,
|
|
1939
|
+
data.urls.copy,
|
|
1940
|
+
fetchJson,
|
|
1941
|
+
applyLocalPositionUpdates,
|
|
1942
|
+
persistPositions,
|
|
1943
|
+
reload,
|
|
1944
|
+
]
|
|
1945
|
+
);
|
|
1946
|
+
|
|
1518
1947
|
const onClearNext = useCallback(
|
|
1519
1948
|
async (id) => {
|
|
1520
1949
|
await updateConnection({ step_id: id, next_step: "" });
|
|
@@ -1524,15 +1953,29 @@ const WorkflowEditor = ({ data }) => {
|
|
|
1524
1953
|
);
|
|
1525
1954
|
|
|
1526
1955
|
const onDelete = useCallback(
|
|
1527
|
-
async (
|
|
1528
|
-
|
|
1956
|
+
async (ids) => {
|
|
1957
|
+
const normalizedIds = Array.isArray(ids) ? ids : [ids];
|
|
1958
|
+
const uniqueIds = [...new Set(normalizedIds.map((id) => String(id)))];
|
|
1959
|
+
if (!uniqueIds.length) return;
|
|
1960
|
+
|
|
1961
|
+
const singleConfirm = strings.confirmDelete || "Delete step?";
|
|
1962
|
+
const confirmMessage =
|
|
1963
|
+
uniqueIds.length > 1
|
|
1964
|
+
? strings.confirmDeleteMany ||
|
|
1965
|
+
`${singleConfirm} (${uniqueIds.length} steps)`
|
|
1966
|
+
: singleConfirm;
|
|
1967
|
+
|
|
1968
|
+
if (!window.confirm(confirmMessage)) return;
|
|
1969
|
+
|
|
1529
1970
|
try {
|
|
1530
|
-
const
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1971
|
+
for (const id of uniqueIds) {
|
|
1972
|
+
const formData = new FormData();
|
|
1973
|
+
formData.append("_csrf", data.csrfToken || "");
|
|
1974
|
+
await fetchJson(`${data.urls.deleteStep}/${id}`, {
|
|
1975
|
+
method: "POST",
|
|
1976
|
+
body: formData,
|
|
1977
|
+
});
|
|
1978
|
+
}
|
|
1536
1979
|
setModal(null);
|
|
1537
1980
|
setError("");
|
|
1538
1981
|
clearDrawerState();
|
|
@@ -1541,7 +1984,14 @@ const WorkflowEditor = ({ data }) => {
|
|
|
1541
1984
|
setError(e.message);
|
|
1542
1985
|
}
|
|
1543
1986
|
},
|
|
1544
|
-
[
|
|
1987
|
+
[
|
|
1988
|
+
clearDrawerState,
|
|
1989
|
+
data.csrfToken,
|
|
1990
|
+
fetchJson,
|
|
1991
|
+
reload,
|
|
1992
|
+
strings.confirmDelete,
|
|
1993
|
+
strings.confirmDeleteMany,
|
|
1994
|
+
]
|
|
1545
1995
|
);
|
|
1546
1996
|
|
|
1547
1997
|
useEffect(() => {
|
|
@@ -1607,7 +2057,11 @@ const WorkflowEditor = ({ data }) => {
|
|
|
1607
2057
|
|
|
1608
2058
|
const hasSteps = steps.length > 0;
|
|
1609
2059
|
const shellClass = modal ? "wf-shell wf-shell--drawer-open" : "wf-shell";
|
|
1610
|
-
const shellStyle = {
|
|
2060
|
+
const shellStyle = {
|
|
2061
|
+
"--wf-drawer-width": `${drawerWidth}px`,
|
|
2062
|
+
height: shellHeight ? `${shellHeight}px` : undefined,
|
|
2063
|
+
maxHeight: shellHeight ? `${shellHeight}px` : undefined,
|
|
2064
|
+
};
|
|
1611
2065
|
const showMiniMap = !modal;
|
|
1612
2066
|
const shouldFitView = !pendingViewportRef.current;
|
|
1613
2067
|
|
|
@@ -1704,84 +2158,132 @@ const WorkflowEditor = ({ data }) => {
|
|
|
1704
2158
|
};
|
|
1705
2159
|
}, [drawerWidth, modal]);
|
|
1706
2160
|
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
2161
|
+
useEffect(() => {
|
|
2162
|
+
const shellEl = shellRef.current;
|
|
2163
|
+
if (!shellEl) return;
|
|
2164
|
+
const cardEl = shellEl.closest(".card");
|
|
2165
|
+
const headerEl = cardEl?.querySelector?.(".card-header");
|
|
2166
|
+
if (headerEl && headerEl !== headerHost) {
|
|
2167
|
+
headerHtmlRef.current = headerEl.innerHTML || "";
|
|
2168
|
+
headerEl.dataset.wfHeaderInjected = "true";
|
|
2169
|
+
headerEl.classList.add("d-flex", "align-items-center");
|
|
2170
|
+
headerEl.innerHTML = "";
|
|
2171
|
+
setHeaderHost(headerEl);
|
|
2172
|
+
}
|
|
2173
|
+
// recalc height once we know our real position
|
|
2174
|
+
requestAnimationFrame(() => recomputeShellHeight());
|
|
2175
|
+
}, [headerHost, recomputeShellHeight]);
|
|
2176
|
+
|
|
2177
|
+
useLayoutEffect(() => {
|
|
2178
|
+
recomputeShellHeight();
|
|
2179
|
+
|
|
2180
|
+
// Debounce resize events to avoid jumpiness
|
|
2181
|
+
const handleResize = () => {
|
|
2182
|
+
if (heightDebounceRef.current) clearTimeout(heightDebounceRef.current);
|
|
2183
|
+
heightDebounceRef.current = setTimeout(() => {
|
|
2184
|
+
recomputeShellHeight();
|
|
2185
|
+
}, 100);
|
|
2186
|
+
};
|
|
2187
|
+
|
|
2188
|
+
window.addEventListener("resize", handleResize);
|
|
2189
|
+
return () => {
|
|
2190
|
+
window.removeEventListener("resize", handleResize);
|
|
2191
|
+
if (heightDebounceRef.current) clearTimeout(heightDebounceRef.current);
|
|
2192
|
+
};
|
|
2193
|
+
}, [recomputeShellHeight]);
|
|
2194
|
+
|
|
2195
|
+
useLayoutEffect(() => {
|
|
2196
|
+
requestAnimationFrame(() => recomputeShellHeight());
|
|
2197
|
+
}, [steps.length, recomputeShellHeight]);
|
|
2198
|
+
|
|
2199
|
+
const headerContent = (
|
|
2200
|
+
<div className="d-flex align-items-center w-100">
|
|
2201
|
+
<div className="d-flex align-items-center flex-wrap">
|
|
2202
|
+
{headerHtmlRef.current ? (
|
|
2203
|
+
<span
|
|
2204
|
+
className="d-flex align-items-center flex-wrap text-muted"
|
|
2205
|
+
dangerouslySetInnerHTML={{ __html: headerHtmlRef.current }}
|
|
2206
|
+
/>
|
|
2207
|
+
) : (
|
|
2208
|
+
<h5 className="m-0 fw-bold text-primary d-inline">
|
|
2209
|
+
{strings.configure}
|
|
2210
|
+
</h5>
|
|
2211
|
+
)}
|
|
2212
|
+
{message ? <span className="text-success ms-2">{message}</span> : null}
|
|
2213
|
+
{error ? <span className="text-danger ms-2">{error}</span> : null}
|
|
1740
2214
|
</div>
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
onConnectStart={onConnectStart}
|
|
1753
|
-
onConnectEnd={onConnectEnd}
|
|
1754
|
-
onEdgeClick={onEdgeClick}
|
|
1755
|
-
onSelectionChange={handleSelectionChange}
|
|
1756
|
-
nodeTypes={nodeTypes}
|
|
1757
|
-
edgeTypes={edgeTypes}
|
|
1758
|
-
onInit={onInit}
|
|
1759
|
-
defaultViewport={pendingViewportRef.current || undefined}
|
|
1760
|
-
fitView={shouldFitView}
|
|
1761
|
-
proOptions={{ hideAttribution: true }}
|
|
1762
|
-
defaultEdgeOptions={{ animated: true }}
|
|
1763
|
-
colorMode={window._sc_lightmode || "light"}
|
|
1764
|
-
onMoveEnd={(_, viewport) => persistViewport(viewport)}
|
|
2215
|
+
<div className="wf-toolbar__actions ms-auto d-flex flex-wrap">
|
|
2216
|
+
<button
|
|
2217
|
+
className="btn btn-sm btn-outline-primary"
|
|
2218
|
+
onClick={() => openStepForm({ initial_step: !hasSteps })}
|
|
2219
|
+
>
|
|
2220
|
+
{strings.addStep}
|
|
2221
|
+
</button>
|
|
2222
|
+
<button
|
|
2223
|
+
className="btn btn-sm btn-outline-secondary ms-2"
|
|
2224
|
+
onClick={reload}
|
|
2225
|
+
disabled={loading}
|
|
1765
2226
|
>
|
|
1766
|
-
{
|
|
1767
|
-
|
|
1768
|
-
<Background gap={16} />
|
|
1769
|
-
</ReactFlow>
|
|
2227
|
+
{loading ? strings.loading : strings.refresh}
|
|
2228
|
+
</button>
|
|
1770
2229
|
</div>
|
|
1771
|
-
<StepDrawer
|
|
1772
|
-
modal={modal}
|
|
1773
|
-
innerRef={modalRef}
|
|
1774
|
-
outerRef={drawerRef}
|
|
1775
|
-
onClose={closeModal}
|
|
1776
|
-
submitting={savingModal}
|
|
1777
|
-
error={error && modal ? error : ""}
|
|
1778
|
-
data={data}
|
|
1779
|
-
onDelete={onDelete}
|
|
1780
|
-
drawerWidth={drawerWidth}
|
|
1781
|
-
onResizeStart={startResize}
|
|
1782
|
-
/>
|
|
1783
2230
|
</div>
|
|
1784
2231
|
);
|
|
2232
|
+
|
|
2233
|
+
return (
|
|
2234
|
+
<>
|
|
2235
|
+
{headerHost ? (
|
|
2236
|
+
createPortal(headerContent, headerHost)
|
|
2237
|
+
) : (
|
|
2238
|
+
<div className="wf-toolbar">{headerContent}</div>
|
|
2239
|
+
)}
|
|
2240
|
+
<div className={shellClass} style={shellStyle} ref={shellRef}>
|
|
2241
|
+
{!hasSteps ? <div className="wf-empty">{strings.empty}</div> : null}
|
|
2242
|
+
<div className="wf-canvas">
|
|
2243
|
+
<ReactFlow
|
|
2244
|
+
nodesFocusable={true}
|
|
2245
|
+
edgesFocusable={true}
|
|
2246
|
+
disableKeyboardA11y={false}
|
|
2247
|
+
nodes={nodes}
|
|
2248
|
+
edges={edges}
|
|
2249
|
+
onNodesChange={onNodesChangeWrapped}
|
|
2250
|
+
onEdgesChange={onEdgesChange}
|
|
2251
|
+
onConnect={onConnect}
|
|
2252
|
+
onConnectStart={onConnectStart}
|
|
2253
|
+
onConnectEnd={onConnectEnd}
|
|
2254
|
+
onEdgeClick={onEdgeClick}
|
|
2255
|
+
onSelectionChange={handleSelectionChange}
|
|
2256
|
+
nodeTypes={nodeTypes}
|
|
2257
|
+
edgeTypes={edgeTypes}
|
|
2258
|
+
onInit={onInit}
|
|
2259
|
+
defaultViewport={pendingViewportRef.current || undefined}
|
|
2260
|
+
fitView={shouldFitView}
|
|
2261
|
+
proOptions={{ hideAttribution: true }}
|
|
2262
|
+
defaultEdgeOptions={{ animated: true }}
|
|
2263
|
+
colorMode={window._sc_lightmode || "light"}
|
|
2264
|
+
onMoveEnd={(_, viewport) => persistViewport(viewport)}
|
|
2265
|
+
>
|
|
2266
|
+
{showMiniMap && <MiniMap pannable zoomable />}
|
|
2267
|
+
<Controls />
|
|
2268
|
+
<Background gap={16} />
|
|
2269
|
+
</ReactFlow>
|
|
2270
|
+
</div>
|
|
2271
|
+
<StepDrawer
|
|
2272
|
+
modal={modal}
|
|
2273
|
+
innerRef={modalRef}
|
|
2274
|
+
outerRef={drawerRef}
|
|
2275
|
+
onClose={closeModal}
|
|
2276
|
+
submitting={savingModal}
|
|
2277
|
+
error={error && modal ? error : ""}
|
|
2278
|
+
data={data}
|
|
2279
|
+
onDelete={onDelete}
|
|
2280
|
+
onCopy={onCopy}
|
|
2281
|
+
drawerWidth={drawerWidth}
|
|
2282
|
+
onResizeStart={startResize}
|
|
2283
|
+
/>
|
|
2284
|
+
</div>
|
|
2285
|
+
</>
|
|
2286
|
+
);
|
|
1785
2287
|
};
|
|
1786
2288
|
|
|
1787
2289
|
export default WorkflowEditor;
|