@saltcorn/workflow-editor 1.6.0-alpha.11 → 1.6.0-alpha.13

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saltcorn/workflow-editor",
3
- "version": "1.6.0-alpha.11",
3
+ "version": "1.6.0-alpha.13",
4
4
  "description": "React Flow-based workflow editor for Saltcorn",
5
5
  "main": "index.js",
6
6
  "homepage": "https://saltcorn.com",
@@ -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,
@@ -302,6 +303,11 @@ const buildGraph = (
302
303
  const allStepNames = steps.map((s) => s.name).filter(Boolean);
303
304
  const orderIndex = new Map(steps.map((s, idx) => [String(s.id), idx]));
304
305
 
306
+ const hasAnyWorkflowPosition = steps.some(
307
+ (s) => s?.configuration?.workflow_position
308
+ );
309
+ const shouldIndentLoopBodies = steps.length > 0 && !hasAnyWorkflowPosition;
310
+
305
311
  const escapeRegExp = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
306
312
 
307
313
  const extractNextStepNames = (rawNextStep) => {
@@ -415,6 +421,9 @@ const buildGraph = (
415
421
  width: DEFAULT_NODE_WIDTH,
416
422
  height: DEFAULT_NODE_HEIGHT,
417
423
  };
424
+ const bodyX = shouldIndentLoopBodies
425
+ ? (loopPos.x || 0) + (loopSize.width || DEFAULT_NODE_WIDTH) + H_GAP
426
+ : loopPos.x;
418
427
  let yCursor = loopPos.y + loopSize.height + V_GAP;
419
428
 
420
429
  body.forEach((s) => {
@@ -424,7 +433,7 @@ const buildGraph = (
424
433
  height: DEFAULT_NODE_HEIGHT,
425
434
  };
426
435
  positionOverrides[String(s.id)] = {
427
- x: loopPos.x,
436
+ x: bodyX,
428
437
  y: yCursor,
429
438
  };
430
439
  yCursor += bodySize.height + V_GAP;
@@ -758,15 +767,17 @@ const StepDrawer = ({
758
767
  ) : null}
759
768
  </div>
760
769
  <div className="wf-drawer__footer d-flex justify-content-end gap-2">
761
- <button
762
- className="btn btn-sm btn-outline-danger"
763
- onClick={(e) => {
764
- e.stopPropagation();
765
- onDelete(modal.stepId);
766
- }}
767
- >
768
- {data.strings.deleteStep}
769
- </button>
770
+ {modal.stepId && (
771
+ <button
772
+ className="btn btn-sm btn-outline-danger"
773
+ onClick={(e) => {
774
+ e.stopPropagation();
775
+ onDelete(modal.stepId);
776
+ }}
777
+ >
778
+ {data.strings.deleteStep}
779
+ </button>
780
+ )}
770
781
  {modal.stepId ? (
771
782
  <button
772
783
  className="btn btn-sm btn-outline-secondary"
@@ -781,19 +792,21 @@ const StepDrawer = ({
781
792
  <button className="btn btn-secondary" onClick={onClose}>
782
793
  Close
783
794
  </button>
784
- <button
785
- className="btn btn-primary"
786
- onClick={() =>
787
- innerRef.current
788
- ?.querySelector("form")
789
- ?.dispatchEvent(
790
- new Event("submit", { cancelable: true, bubbles: true })
791
- )
792
- }
793
- disabled={submitting}
794
- >
795
- {submitting ? "Saving..." : "Save"}
796
- </button>
795
+ {!modal.stepId && (
796
+ <button
797
+ className="btn btn-primary"
798
+ onClick={() =>
799
+ innerRef.current
800
+ ?.querySelector("form")
801
+ ?.dispatchEvent(
802
+ new Event("submit", { cancelable: true, bubbles: true })
803
+ )
804
+ }
805
+ disabled={submitting}
806
+ >
807
+ {submitting ? "Saving..." : "Save"}
808
+ </button>
809
+ )}
797
810
  </div>
798
811
  </>
799
812
  ) : null}
@@ -836,6 +849,10 @@ const WorkflowEditor = ({ data }) => {
836
849
  const autoSaveTimerRef = useRef(null);
837
850
  const autoSavingRef = useRef(false);
838
851
  const nodesRef = useRef([]);
852
+ const [headerHost, setHeaderHost] = useState(null);
853
+ const [shellHeight, setShellHeight] = useState(null);
854
+ const lastHeightRef = useRef(null);
855
+ const heightDebounceRef = useRef(null);
839
856
 
840
857
  const drawerStorageKey = useMemo(
841
858
  () => `wf-drawer:${data?.trigger?.id || "default"}`,
@@ -848,6 +865,57 @@ const WorkflowEditor = ({ data }) => {
848
865
  );
849
866
 
850
867
  const rfInstanceRef = useRef(null);
868
+ const headerHtmlRef = useRef("");
869
+
870
+ const recomputeShellHeight = useCallback(() => {
871
+ if (typeof window === "undefined") return;
872
+ const shellEl = shellRef.current;
873
+ if (!shellEl) return;
874
+
875
+ const wrapperEl = shellEl.closest(".workflow-editor-wrapper");
876
+ if (!wrapperEl) return;
877
+
878
+ const afterEl = wrapperEl.nextElementSibling;
879
+ const cardBodyEl = shellEl.closest(".card-body");
880
+ if (!cardBodyEl) return;
881
+
882
+ const minHeight = 400;
883
+ const bottomMargin = 28;
884
+
885
+ const cardBodyRect = cardBodyEl.getBoundingClientRect();
886
+ const styles = window.getComputedStyle(cardBodyEl);
887
+ const cardBodyPaddingTop = parseFloat(styles.paddingTop) || 0;
888
+ const cardBodyPaddingBottom = parseFloat(styles.paddingBottom) || 0;
889
+
890
+ const effectiveTop = Math.max(0, cardBodyRect.top);
891
+
892
+ let footerHeight = 0;
893
+ if (afterEl && typeof afterEl.getBoundingClientRect === "function") {
894
+ const afterRect = afterEl.getBoundingClientRect();
895
+ const afterStyles = window.getComputedStyle(afterEl);
896
+ const mt = parseFloat(afterStyles.marginTop) || 0;
897
+ const mb = parseFloat(afterStyles.marginBottom) || 0;
898
+ footerHeight = afterRect.height + mt + mb;
899
+ }
900
+
901
+ const availableHeight =
902
+ window.innerHeight -
903
+ effectiveTop -
904
+ bottomMargin -
905
+ cardBodyPaddingTop -
906
+ cardBodyPaddingBottom -
907
+ footerHeight;
908
+
909
+ const wrapperHeight = Math.max(minHeight, availableHeight);
910
+
911
+ if (lastHeightRef.current !== wrapperHeight) {
912
+ lastHeightRef.current = wrapperHeight;
913
+ if (wrapperEl) {
914
+ wrapperEl.style.height = `${wrapperHeight}px`;
915
+ }
916
+ if (Number.isFinite(wrapperHeight)) setShellHeight(wrapperHeight);
917
+ }
918
+ }, []);
851
919
 
852
920
  const persistDrawerState = useCallback(
853
921
  (payload) => {
@@ -1166,7 +1234,13 @@ const WorkflowEditor = ({ data }) => {
1166
1234
  } finally {
1167
1235
  setLoading(false);
1168
1236
  }
1169
- }, [applyInsertBetweenLayout, data.urls.data, fetchJson, strings.refresh, updateConnection]);
1237
+ }, [
1238
+ applyInsertBetweenLayout,
1239
+ data.urls.data,
1240
+ fetchJson,
1241
+ strings.refresh,
1242
+ updateConnection,
1243
+ ]);
1170
1244
 
1171
1245
  const persistPositions = useCallback(
1172
1246
  async (positions = []) => {
@@ -1236,18 +1310,22 @@ const WorkflowEditor = ({ data }) => {
1236
1310
  );
1237
1311
 
1238
1312
  const applyInsertBetweenLayout = useCallback(
1239
- async (freshSteps, { newStep, afterStepId, targetName, targetId, snapshot }) => {
1313
+ async (
1314
+ freshSteps,
1315
+ { newStep, afterStepId, targetName, targetId, snapshot }
1316
+ ) => {
1240
1317
  if (!newStep || !snapshot) return null;
1241
1318
  const snap = snapshot || {};
1242
- const newStepSize =
1243
- getWorkflowSize(newStep) || {
1244
- width: DEFAULT_NODE_WIDTH,
1245
- height: DEFAULT_NODE_HEIGHT,
1246
- };
1319
+ const newStepSize = getWorkflowSize(newStep) || {
1320
+ width: DEFAULT_NODE_WIDTH,
1321
+ height: DEFAULT_NODE_HEIGHT,
1322
+ };
1247
1323
  const sourceSnap = snap[String(afterStepId)];
1248
1324
  const resolvedTargetId =
1249
1325
  targetId || freshSteps.find((s) => s.name === targetName)?.id;
1250
- const targetSnap = resolvedTargetId ? snap[String(resolvedTargetId)] : null;
1326
+ const targetSnap = resolvedTargetId
1327
+ ? snap[String(resolvedTargetId)]
1328
+ : null;
1251
1329
  const shiftAmount = newStepSize.height + V_GAP;
1252
1330
  const fallbackY = sourceSnap?.y ?? 0;
1253
1331
  const fallbackX = sourceSnap?.x ?? targetSnap?.x ?? 0;
@@ -1255,7 +1333,7 @@ const WorkflowEditor = ({ data }) => {
1255
1333
  const newX =
1256
1334
  sourceSnap && targetSnap
1257
1335
  ? (sourceSnap.x + targetSnap.x) / 2
1258
- : targetSnap?.x ?? sourceSnap?.x ?? fallbackX;
1336
+ : (targetSnap?.x ?? sourceSnap?.x ?? fallbackX);
1259
1337
 
1260
1338
  const newHalfWidth = (newStepSize.width || DEFAULT_NODE_WIDTH) / 2;
1261
1339
  const bandPadding = H_GAP / 2;
@@ -1280,12 +1358,14 @@ const WorkflowEditor = ({ data }) => {
1280
1358
  const nodeMaxX = nodeSnap.x + nodeHalfWidth;
1281
1359
  const overlapsBand = !(nodeMaxX < bandMinX || nodeMinX > bandMaxX);
1282
1360
  const shouldShift =
1283
- nodeSnap.y >= newY &&
1284
- idStr !== String(afterStepId) &&
1285
- overlapsBand;
1361
+ nodeSnap.y >= newY && idStr !== String(afterStepId) && overlapsBand;
1286
1362
 
1287
1363
  if (shouldShift) {
1288
- updates.push({ id: idStr, x: nodeSnap.x, y: nodeSnap.y + shiftAmount });
1364
+ updates.push({
1365
+ id: idStr,
1366
+ x: nodeSnap.x,
1367
+ y: nodeSnap.y + shiftAmount,
1368
+ });
1289
1369
  } else {
1290
1370
  updates.push({ id: idStr, x: nodeSnap.x, y: nodeSnap.y });
1291
1371
  }
@@ -1367,6 +1447,7 @@ const WorkflowEditor = ({ data }) => {
1367
1447
  const base = `${data.urls.stepForm}${stepId ? `/${stepId}` : ""}`;
1368
1448
  const url = new URL(base, window.location.origin);
1369
1449
  url.searchParams.set("render", "dialog");
1450
+ url.searchParams.set("vw", window.innerWidth);
1370
1451
  if (initial_step) url.searchParams.set("initial_step", "true");
1371
1452
  if (after_step) url.searchParams.set("after_step", after_step);
1372
1453
  const res = await fetchJson(url.toString());
@@ -1431,8 +1512,7 @@ const WorkflowEditor = ({ data }) => {
1431
1512
  const scheduleAutoSave = allowAutoSave
1432
1513
  ? () => {
1433
1514
  if (autoSavingRef.current) return;
1434
- if (autoSaveTimerRef.current)
1435
- clearTimeout(autoSaveTimerRef.current);
1515
+ if (autoSaveTimerRef.current) clearTimeout(autoSaveTimerRef.current);
1436
1516
  autoSaveTimerRef.current = setTimeout(async () => {
1437
1517
  autoSavingRef.current = true;
1438
1518
  try {
@@ -1463,8 +1543,7 @@ const WorkflowEditor = ({ data }) => {
1463
1543
  formEl.addEventListener("change", scheduleAutoSave);
1464
1544
  }
1465
1545
  return () => {
1466
- if (autoSaveTimerRef.current)
1467
- clearTimeout(autoSaveTimerRef.current);
1546
+ if (autoSaveTimerRef.current) clearTimeout(autoSaveTimerRef.current);
1468
1547
  formEl.removeEventListener("submit", submitHandler);
1469
1548
  if (scheduleAutoSave) {
1470
1549
  formEl.removeEventListener("input", scheduleAutoSave);
@@ -1601,7 +1680,11 @@ const WorkflowEditor = ({ data }) => {
1601
1680
 
1602
1681
  const onEdgesChange = useCallback(
1603
1682
  (changes) => {
1604
- const removed = changes.filter((c) => c.type === "remove");
1683
+ // Block edge deletions while a modal is open so ReactFlow cannot remove links under dialogs
1684
+ const safeChanges = modal
1685
+ ? changes.filter((c) => c.type !== "remove")
1686
+ : changes;
1687
+ const removed = safeChanges.filter((c) => c.type === "remove");
1605
1688
  if (removed.length) {
1606
1689
  const updates = [];
1607
1690
  removed.forEach((chg) => {
@@ -1630,13 +1713,18 @@ const WorkflowEditor = ({ data }) => {
1630
1713
  .then(() => reload())
1631
1714
  .catch((e) => setError(e.message));
1632
1715
  }
1633
- rfOnEdgesChange(changes);
1716
+ rfOnEdgesChange(safeChanges);
1634
1717
  },
1635
- [edges, reload, rfOnEdgesChange, updateConnection]
1718
+ [edges, modal, reload, rfOnEdgesChange, updateConnection]
1636
1719
  );
1637
1720
 
1638
1721
  const onNodesChangeWrapped = useCallback(
1639
1722
  (changes) => {
1723
+ // Prevent node deletion by reactflow while drawer is open
1724
+ const safeChanges = modal
1725
+ ? changes.filter((c) => c.type !== "remove")
1726
+ : changes;
1727
+
1640
1728
  const finishedPositions = changes
1641
1729
  .filter((c) => c.type === "position" && c.position && c.dragging)
1642
1730
  .map((c) => ({ id: c.id, x: c.position.x, y: c.position.y }));
@@ -1664,9 +1752,15 @@ const WorkflowEditor = ({ data }) => {
1664
1752
  );
1665
1753
  if (stepPositions.length) schedulePersistPositions(stepPositions);
1666
1754
  }
1667
- onNodesChange(changes);
1755
+ onNodesChange(safeChanges);
1668
1756
  },
1669
- [onNodesChange, schedulePersistPositions, setAddPositions, setStartPosition]
1757
+ [
1758
+ modal,
1759
+ onNodesChange,
1760
+ schedulePersistPositions,
1761
+ setAddPositions,
1762
+ setStartPosition,
1763
+ ]
1670
1764
  );
1671
1765
 
1672
1766
  const onAddAfter = useCallback(
@@ -1703,9 +1797,10 @@ const WorkflowEditor = ({ data }) => {
1703
1797
  nodesRef.current
1704
1798
  .filter((n) => n.type === "step")
1705
1799
  .forEach((n) => {
1706
- const size =
1707
- sizeById.get(String(n.id)) ||
1708
- { width: DEFAULT_NODE_WIDTH, height: DEFAULT_NODE_HEIGHT };
1800
+ const size = sizeById.get(String(n.id)) || {
1801
+ width: DEFAULT_NODE_WIDTH,
1802
+ height: DEFAULT_NODE_HEIGHT,
1803
+ };
1709
1804
  positionsSnapshot[String(n.id)] = {
1710
1805
  x: n.position?.x ?? 0,
1711
1806
  y: n.position?.y ?? 0,
@@ -1723,9 +1818,7 @@ const WorkflowEditor = ({ data }) => {
1723
1818
  positionsSnapshot,
1724
1819
  },
1725
1820
  };
1726
- openStepForm(
1727
- fromStart ? { initial_step: true } : { after_step: source }
1728
- );
1821
+ openStepForm(fromStart ? { initial_step: true } : { after_step: source });
1729
1822
  },
1730
1823
  [nameById, openStepForm, steps]
1731
1824
  );
@@ -1754,7 +1847,7 @@ const WorkflowEditor = ({ data }) => {
1754
1847
  await reload();
1755
1848
  return;
1756
1849
  }
1757
-
1850
+
1758
1851
  const copiedStep = res.step;
1759
1852
  const copiedName = copiedStep.name;
1760
1853
 
@@ -1763,23 +1856,19 @@ const WorkflowEditor = ({ data }) => {
1763
1856
  const freshSteps = (await reload()) || [];
1764
1857
  if (!freshSteps.length) return;
1765
1858
 
1766
- const sourceStep = freshSteps.find(
1767
- (s) => String(s.id) === String(id)
1768
- );
1859
+ const sourceStep = freshSteps.find((s) => String(s.id) === String(id));
1769
1860
  const newStep = freshSteps.find((s) => s.name === copiedName);
1770
1861
 
1771
1862
  if (!sourceStep || !newStep || !newStep.id) return;
1772
1863
 
1773
- const sourceSize =
1774
- getWorkflowSize(sourceStep) || {
1775
- width: DEFAULT_NODE_WIDTH,
1776
- height: DEFAULT_NODE_HEIGHT,
1777
- };
1778
- const newSize =
1779
- getWorkflowSize(newStep) || {
1780
- width: DEFAULT_NODE_WIDTH,
1781
- height: DEFAULT_NODE_HEIGHT,
1782
- };
1864
+ const sourceSize = getWorkflowSize(sourceStep) || {
1865
+ width: DEFAULT_NODE_WIDTH,
1866
+ height: DEFAULT_NODE_HEIGHT,
1867
+ };
1868
+ const newSize = getWorkflowSize(newStep) || {
1869
+ width: DEFAULT_NODE_WIDTH,
1870
+ height: DEFAULT_NODE_HEIGHT,
1871
+ };
1783
1872
 
1784
1873
  const sourcePos = sourceStep.configuration?.workflow_position || {
1785
1874
  x: 0,
@@ -1795,12 +1884,25 @@ const WorkflowEditor = ({ data }) => {
1795
1884
  // Push steps that are to the right of the insertion band
1796
1885
  const bandMinX = targetX - newSize.width * 0.25;
1797
1886
  const shiftX = newSize.width + ADD_GAP;
1887
+ const newHalfHeight = (newSize.height || DEFAULT_NODE_HEIGHT) / 2;
1888
+ const bandPaddingY = V_GAP / 2;
1889
+ const bandMinY = targetY - newHalfHeight - bandPaddingY;
1890
+ const bandMaxY = targetY + newHalfHeight + bandPaddingY;
1798
1891
 
1799
1892
  freshSteps.forEach((s) => {
1800
1893
  const cfgPos = s.configuration?.workflow_position;
1801
1894
  if (!cfgPos) return;
1802
1895
  if (String(s.id) === String(newStep.id)) return;
1803
- if (cfgPos.x >= bandMinX) {
1896
+ const stepSize = getWorkflowSize(s) || {
1897
+ width: DEFAULT_NODE_WIDTH,
1898
+ height: DEFAULT_NODE_HEIGHT,
1899
+ };
1900
+ const halfHeight = (stepSize.height || DEFAULT_NODE_HEIGHT) / 2;
1901
+ const nodeMinY = cfgPos.y - halfHeight;
1902
+ const nodeMaxY = cfgPos.y + halfHeight;
1903
+ const overlapsBandY = !(nodeMaxY < bandMinY || nodeMinY > bandMaxY);
1904
+
1905
+ if (cfgPos.x >= bandMinX && overlapsBandY) {
1804
1906
  updates.push({
1805
1907
  id: String(s.id),
1806
1908
  x: cfgPos.x + shiftX,
@@ -1963,7 +2065,11 @@ const WorkflowEditor = ({ data }) => {
1963
2065
 
1964
2066
  const hasSteps = steps.length > 0;
1965
2067
  const shellClass = modal ? "wf-shell wf-shell--drawer-open" : "wf-shell";
1966
- const shellStyle = { "--wf-drawer-width": `${drawerWidth}px` };
2068
+ const shellStyle = {
2069
+ "--wf-drawer-width": `${drawerWidth}px`,
2070
+ height: shellHeight ? `${shellHeight}px` : undefined,
2071
+ maxHeight: shellHeight ? `${shellHeight}px` : undefined,
2072
+ };
1967
2073
  const showMiniMap = !modal;
1968
2074
  const shouldFitView = !pendingViewportRef.current;
1969
2075
 
@@ -2060,85 +2166,132 @@ const WorkflowEditor = ({ data }) => {
2060
2166
  };
2061
2167
  }, [drawerWidth, modal]);
2062
2168
 
2063
- return (
2064
- <div className={shellClass} style={shellStyle} ref={shellRef}>
2065
- <div className="wf-toolbar">
2066
- <div className="wf-toolbar__left">
2067
- <strong>{strings.configure}</strong>
2068
- {message ? (
2069
- <span className="text-success ms-2">{message}</span>
2070
- ) : null}
2071
- {error ? <span className="text-danger ms-2">{error}</span> : null}
2072
- {/* {savingPositions ? (
2073
- <span className="text-muted ms-2">
2074
- {strings.loading || "Saving positions..."}
2075
- </span>
2076
- ) : null} */}
2077
- </div>
2078
- <div className="wf-toolbar__actions">
2079
- <button
2080
- className="btn btn-sm btn-outline-primary"
2081
- onClick={() => openStepForm({ initial_step: !hasSteps })}
2082
- >
2083
- {strings.addStep}
2084
- </button>
2085
- <button
2086
- className="btn btn-sm btn-outline-secondary"
2087
- onClick={reload}
2088
- disabled={loading}
2089
- >
2090
- {loading ? strings.loading : strings.refresh}
2091
- </button>
2092
- <a className="btn btn-sm btn-outline-info" href={data.urls.runs}>
2093
- {strings.runs}
2094
- </a>
2095
- </div>
2169
+ useEffect(() => {
2170
+ const shellEl = shellRef.current;
2171
+ if (!shellEl) return;
2172
+ const cardEl = shellEl.closest(".card");
2173
+ const headerEl = cardEl?.querySelector?.(".card-header");
2174
+ if (headerEl && headerEl !== headerHost) {
2175
+ headerHtmlRef.current = headerEl.innerHTML || "";
2176
+ headerEl.dataset.wfHeaderInjected = "true";
2177
+ headerEl.classList.add("d-flex", "align-items-center");
2178
+ headerEl.innerHTML = "";
2179
+ setHeaderHost(headerEl);
2180
+ }
2181
+ // recalc height once we know our real position
2182
+ requestAnimationFrame(() => recomputeShellHeight());
2183
+ }, [headerHost, recomputeShellHeight]);
2184
+
2185
+ useLayoutEffect(() => {
2186
+ recomputeShellHeight();
2187
+
2188
+ // Debounce resize events to avoid jumpiness
2189
+ const handleResize = () => {
2190
+ if (heightDebounceRef.current) clearTimeout(heightDebounceRef.current);
2191
+ heightDebounceRef.current = setTimeout(() => {
2192
+ recomputeShellHeight();
2193
+ }, 100);
2194
+ };
2195
+
2196
+ window.addEventListener("resize", handleResize);
2197
+ return () => {
2198
+ window.removeEventListener("resize", handleResize);
2199
+ if (heightDebounceRef.current) clearTimeout(heightDebounceRef.current);
2200
+ };
2201
+ }, [recomputeShellHeight]);
2202
+
2203
+ useLayoutEffect(() => {
2204
+ requestAnimationFrame(() => recomputeShellHeight());
2205
+ }, [steps.length, recomputeShellHeight]);
2206
+
2207
+ const headerContent = (
2208
+ <div className="d-flex align-items-center w-100">
2209
+ <div className="d-flex align-items-center flex-wrap">
2210
+ {headerHtmlRef.current ? (
2211
+ <span
2212
+ className="d-flex align-items-center flex-wrap text-muted"
2213
+ dangerouslySetInnerHTML={{ __html: headerHtmlRef.current }}
2214
+ />
2215
+ ) : (
2216
+ <h5 className="m-0 fw-bold text-primary d-inline">
2217
+ {strings.configure}
2218
+ </h5>
2219
+ )}
2220
+ {message ? <span className="text-success ms-2">{message}</span> : null}
2221
+ {error ? <span className="text-danger ms-2">{error}</span> : null}
2096
2222
  </div>
2097
- {!hasSteps ? <div className="wf-empty">{strings.empty}</div> : null}
2098
- <div className="wf-canvas">
2099
- <ReactFlow
2100
- nodesFocusable={true}
2101
- edgesFocusable={true}
2102
- disableKeyboardA11y={false}
2103
- nodes={nodes}
2104
- edges={edges}
2105
- onNodesChange={onNodesChangeWrapped}
2106
- onEdgesChange={onEdgesChange}
2107
- onConnect={onConnect}
2108
- onConnectStart={onConnectStart}
2109
- onConnectEnd={onConnectEnd}
2110
- onEdgeClick={onEdgeClick}
2111
- onSelectionChange={handleSelectionChange}
2112
- nodeTypes={nodeTypes}
2113
- edgeTypes={edgeTypes}
2114
- onInit={onInit}
2115
- defaultViewport={pendingViewportRef.current || undefined}
2116
- fitView={shouldFitView}
2117
- proOptions={{ hideAttribution: true }}
2118
- defaultEdgeOptions={{ animated: true }}
2119
- colorMode={window._sc_lightmode || "light"}
2120
- onMoveEnd={(_, viewport) => persistViewport(viewport)}
2223
+ <div className="wf-toolbar__actions ms-auto d-flex flex-wrap">
2224
+ <button
2225
+ className="btn btn-sm btn-outline-primary"
2226
+ onClick={() => openStepForm({ initial_step: !hasSteps })}
2121
2227
  >
2122
- {showMiniMap && <MiniMap pannable zoomable />}
2123
- <Controls />
2124
- <Background gap={16} />
2125
- </ReactFlow>
2228
+ {strings.addStep}
2229
+ </button>
2230
+ <button
2231
+ className="btn btn-sm btn-outline-secondary ms-2"
2232
+ onClick={reload}
2233
+ disabled={loading}
2234
+ >
2235
+ {loading ? strings.loading : strings.refresh}
2236
+ </button>
2126
2237
  </div>
2127
- <StepDrawer
2128
- modal={modal}
2129
- innerRef={modalRef}
2130
- outerRef={drawerRef}
2131
- onClose={closeModal}
2132
- submitting={savingModal}
2133
- error={error && modal ? error : ""}
2134
- data={data}
2135
- onDelete={onDelete}
2136
- onCopy={onCopy}
2137
- drawerWidth={drawerWidth}
2138
- onResizeStart={startResize}
2139
- />
2140
2238
  </div>
2141
2239
  );
2240
+
2241
+ return (
2242
+ <>
2243
+ {headerHost ? (
2244
+ createPortal(headerContent, headerHost)
2245
+ ) : (
2246
+ <div className="wf-toolbar">{headerContent}</div>
2247
+ )}
2248
+ <div className={shellClass} style={shellStyle} ref={shellRef}>
2249
+ {!hasSteps ? <div className="wf-empty">{strings.empty}</div> : null}
2250
+ <div className="wf-canvas">
2251
+ <ReactFlow
2252
+ nodesFocusable={true}
2253
+ edgesFocusable={true}
2254
+ disableKeyboardA11y={false}
2255
+ nodes={nodes}
2256
+ edges={edges}
2257
+ onNodesChange={onNodesChangeWrapped}
2258
+ onEdgesChange={onEdgesChange}
2259
+ onConnect={onConnect}
2260
+ onConnectStart={onConnectStart}
2261
+ onConnectEnd={onConnectEnd}
2262
+ onEdgeClick={onEdgeClick}
2263
+ onSelectionChange={handleSelectionChange}
2264
+ nodeTypes={nodeTypes}
2265
+ edgeTypes={edgeTypes}
2266
+ onInit={onInit}
2267
+ defaultViewport={pendingViewportRef.current || undefined}
2268
+ fitView={shouldFitView}
2269
+ proOptions={{ hideAttribution: true }}
2270
+ defaultEdgeOptions={{ animated: true }}
2271
+ colorMode={window._sc_lightmode || "light"}
2272
+ onMoveEnd={(_, viewport) => persistViewport(viewport)}
2273
+ >
2274
+ {showMiniMap && <MiniMap pannable zoomable />}
2275
+ <Controls />
2276
+ <Background gap={16} />
2277
+ </ReactFlow>
2278
+ </div>
2279
+ <StepDrawer
2280
+ modal={modal}
2281
+ innerRef={modalRef}
2282
+ outerRef={drawerRef}
2283
+ onClose={closeModal}
2284
+ submitting={savingModal}
2285
+ error={error && modal ? error : ""}
2286
+ data={data}
2287
+ onDelete={onDelete}
2288
+ onCopy={onCopy}
2289
+ drawerWidth={drawerWidth}
2290
+ onResizeStart={startResize}
2291
+ />
2292
+ </div>
2293
+ </>
2294
+ );
2142
2295
  };
2143
2296
 
2144
2297
  export default WorkflowEditor;