@saltcorn/workflow-editor 1.6.0-alpha.11 → 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/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.12",
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,
@@ -758,15 +759,17 @@ const StepDrawer = ({
758
759
  ) : null}
759
760
  </div>
760
761
  <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>
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
+ )}
770
773
  {modal.stepId ? (
771
774
  <button
772
775
  className="btn btn-sm btn-outline-secondary"
@@ -781,19 +784,21 @@ const StepDrawer = ({
781
784
  <button className="btn btn-secondary" onClick={onClose}>
782
785
  Close
783
786
  </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>
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
+ )}
797
802
  </div>
798
803
  </>
799
804
  ) : null}
@@ -836,6 +841,10 @@ const WorkflowEditor = ({ data }) => {
836
841
  const autoSaveTimerRef = useRef(null);
837
842
  const autoSavingRef = useRef(false);
838
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);
839
848
 
840
849
  const drawerStorageKey = useMemo(
841
850
  () => `wf-drawer:${data?.trigger?.id || "default"}`,
@@ -848,6 +857,57 @@ const WorkflowEditor = ({ data }) => {
848
857
  );
849
858
 
850
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
+ }, []);
851
911
 
852
912
  const persistDrawerState = useCallback(
853
913
  (payload) => {
@@ -1166,7 +1226,13 @@ const WorkflowEditor = ({ data }) => {
1166
1226
  } finally {
1167
1227
  setLoading(false);
1168
1228
  }
1169
- }, [applyInsertBetweenLayout, data.urls.data, fetchJson, strings.refresh, updateConnection]);
1229
+ }, [
1230
+ applyInsertBetweenLayout,
1231
+ data.urls.data,
1232
+ fetchJson,
1233
+ strings.refresh,
1234
+ updateConnection,
1235
+ ]);
1170
1236
 
1171
1237
  const persistPositions = useCallback(
1172
1238
  async (positions = []) => {
@@ -1236,18 +1302,22 @@ const WorkflowEditor = ({ data }) => {
1236
1302
  );
1237
1303
 
1238
1304
  const applyInsertBetweenLayout = useCallback(
1239
- async (freshSteps, { newStep, afterStepId, targetName, targetId, snapshot }) => {
1305
+ async (
1306
+ freshSteps,
1307
+ { newStep, afterStepId, targetName, targetId, snapshot }
1308
+ ) => {
1240
1309
  if (!newStep || !snapshot) return null;
1241
1310
  const snap = snapshot || {};
1242
- const newStepSize =
1243
- getWorkflowSize(newStep) || {
1244
- width: DEFAULT_NODE_WIDTH,
1245
- height: DEFAULT_NODE_HEIGHT,
1246
- };
1311
+ const newStepSize = getWorkflowSize(newStep) || {
1312
+ width: DEFAULT_NODE_WIDTH,
1313
+ height: DEFAULT_NODE_HEIGHT,
1314
+ };
1247
1315
  const sourceSnap = snap[String(afterStepId)];
1248
1316
  const resolvedTargetId =
1249
1317
  targetId || freshSteps.find((s) => s.name === targetName)?.id;
1250
- const targetSnap = resolvedTargetId ? snap[String(resolvedTargetId)] : null;
1318
+ const targetSnap = resolvedTargetId
1319
+ ? snap[String(resolvedTargetId)]
1320
+ : null;
1251
1321
  const shiftAmount = newStepSize.height + V_GAP;
1252
1322
  const fallbackY = sourceSnap?.y ?? 0;
1253
1323
  const fallbackX = sourceSnap?.x ?? targetSnap?.x ?? 0;
@@ -1255,7 +1325,7 @@ const WorkflowEditor = ({ data }) => {
1255
1325
  const newX =
1256
1326
  sourceSnap && targetSnap
1257
1327
  ? (sourceSnap.x + targetSnap.x) / 2
1258
- : targetSnap?.x ?? sourceSnap?.x ?? fallbackX;
1328
+ : (targetSnap?.x ?? sourceSnap?.x ?? fallbackX);
1259
1329
 
1260
1330
  const newHalfWidth = (newStepSize.width || DEFAULT_NODE_WIDTH) / 2;
1261
1331
  const bandPadding = H_GAP / 2;
@@ -1280,12 +1350,14 @@ const WorkflowEditor = ({ data }) => {
1280
1350
  const nodeMaxX = nodeSnap.x + nodeHalfWidth;
1281
1351
  const overlapsBand = !(nodeMaxX < bandMinX || nodeMinX > bandMaxX);
1282
1352
  const shouldShift =
1283
- nodeSnap.y >= newY &&
1284
- idStr !== String(afterStepId) &&
1285
- overlapsBand;
1353
+ nodeSnap.y >= newY && idStr !== String(afterStepId) && overlapsBand;
1286
1354
 
1287
1355
  if (shouldShift) {
1288
- updates.push({ id: idStr, x: nodeSnap.x, y: nodeSnap.y + shiftAmount });
1356
+ updates.push({
1357
+ id: idStr,
1358
+ x: nodeSnap.x,
1359
+ y: nodeSnap.y + shiftAmount,
1360
+ });
1289
1361
  } else {
1290
1362
  updates.push({ id: idStr, x: nodeSnap.x, y: nodeSnap.y });
1291
1363
  }
@@ -1367,6 +1439,7 @@ const WorkflowEditor = ({ data }) => {
1367
1439
  const base = `${data.urls.stepForm}${stepId ? `/${stepId}` : ""}`;
1368
1440
  const url = new URL(base, window.location.origin);
1369
1441
  url.searchParams.set("render", "dialog");
1442
+ url.searchParams.set("vw", window.innerWidth);
1370
1443
  if (initial_step) url.searchParams.set("initial_step", "true");
1371
1444
  if (after_step) url.searchParams.set("after_step", after_step);
1372
1445
  const res = await fetchJson(url.toString());
@@ -1431,8 +1504,7 @@ const WorkflowEditor = ({ data }) => {
1431
1504
  const scheduleAutoSave = allowAutoSave
1432
1505
  ? () => {
1433
1506
  if (autoSavingRef.current) return;
1434
- if (autoSaveTimerRef.current)
1435
- clearTimeout(autoSaveTimerRef.current);
1507
+ if (autoSaveTimerRef.current) clearTimeout(autoSaveTimerRef.current);
1436
1508
  autoSaveTimerRef.current = setTimeout(async () => {
1437
1509
  autoSavingRef.current = true;
1438
1510
  try {
@@ -1463,8 +1535,7 @@ const WorkflowEditor = ({ data }) => {
1463
1535
  formEl.addEventListener("change", scheduleAutoSave);
1464
1536
  }
1465
1537
  return () => {
1466
- if (autoSaveTimerRef.current)
1467
- clearTimeout(autoSaveTimerRef.current);
1538
+ if (autoSaveTimerRef.current) clearTimeout(autoSaveTimerRef.current);
1468
1539
  formEl.removeEventListener("submit", submitHandler);
1469
1540
  if (scheduleAutoSave) {
1470
1541
  formEl.removeEventListener("input", scheduleAutoSave);
@@ -1601,7 +1672,11 @@ const WorkflowEditor = ({ data }) => {
1601
1672
 
1602
1673
  const onEdgesChange = useCallback(
1603
1674
  (changes) => {
1604
- const removed = changes.filter((c) => c.type === "remove");
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");
1605
1680
  if (removed.length) {
1606
1681
  const updates = [];
1607
1682
  removed.forEach((chg) => {
@@ -1630,13 +1705,18 @@ const WorkflowEditor = ({ data }) => {
1630
1705
  .then(() => reload())
1631
1706
  .catch((e) => setError(e.message));
1632
1707
  }
1633
- rfOnEdgesChange(changes);
1708
+ rfOnEdgesChange(safeChanges);
1634
1709
  },
1635
- [edges, reload, rfOnEdgesChange, updateConnection]
1710
+ [edges, modal, reload, rfOnEdgesChange, updateConnection]
1636
1711
  );
1637
1712
 
1638
1713
  const onNodesChangeWrapped = useCallback(
1639
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
+
1640
1720
  const finishedPositions = changes
1641
1721
  .filter((c) => c.type === "position" && c.position && c.dragging)
1642
1722
  .map((c) => ({ id: c.id, x: c.position.x, y: c.position.y }));
@@ -1664,9 +1744,15 @@ const WorkflowEditor = ({ data }) => {
1664
1744
  );
1665
1745
  if (stepPositions.length) schedulePersistPositions(stepPositions);
1666
1746
  }
1667
- onNodesChange(changes);
1747
+ onNodesChange(safeChanges);
1668
1748
  },
1669
- [onNodesChange, schedulePersistPositions, setAddPositions, setStartPosition]
1749
+ [
1750
+ modal,
1751
+ onNodesChange,
1752
+ schedulePersistPositions,
1753
+ setAddPositions,
1754
+ setStartPosition,
1755
+ ]
1670
1756
  );
1671
1757
 
1672
1758
  const onAddAfter = useCallback(
@@ -1703,9 +1789,10 @@ const WorkflowEditor = ({ data }) => {
1703
1789
  nodesRef.current
1704
1790
  .filter((n) => n.type === "step")
1705
1791
  .forEach((n) => {
1706
- const size =
1707
- sizeById.get(String(n.id)) ||
1708
- { width: DEFAULT_NODE_WIDTH, height: DEFAULT_NODE_HEIGHT };
1792
+ const size = sizeById.get(String(n.id)) || {
1793
+ width: DEFAULT_NODE_WIDTH,
1794
+ height: DEFAULT_NODE_HEIGHT,
1795
+ };
1709
1796
  positionsSnapshot[String(n.id)] = {
1710
1797
  x: n.position?.x ?? 0,
1711
1798
  y: n.position?.y ?? 0,
@@ -1723,9 +1810,7 @@ const WorkflowEditor = ({ data }) => {
1723
1810
  positionsSnapshot,
1724
1811
  },
1725
1812
  };
1726
- openStepForm(
1727
- fromStart ? { initial_step: true } : { after_step: source }
1728
- );
1813
+ openStepForm(fromStart ? { initial_step: true } : { after_step: source });
1729
1814
  },
1730
1815
  [nameById, openStepForm, steps]
1731
1816
  );
@@ -1754,7 +1839,7 @@ const WorkflowEditor = ({ data }) => {
1754
1839
  await reload();
1755
1840
  return;
1756
1841
  }
1757
-
1842
+
1758
1843
  const copiedStep = res.step;
1759
1844
  const copiedName = copiedStep.name;
1760
1845
 
@@ -1763,23 +1848,19 @@ const WorkflowEditor = ({ data }) => {
1763
1848
  const freshSteps = (await reload()) || [];
1764
1849
  if (!freshSteps.length) return;
1765
1850
 
1766
- const sourceStep = freshSteps.find(
1767
- (s) => String(s.id) === String(id)
1768
- );
1851
+ const sourceStep = freshSteps.find((s) => String(s.id) === String(id));
1769
1852
  const newStep = freshSteps.find((s) => s.name === copiedName);
1770
1853
 
1771
1854
  if (!sourceStep || !newStep || !newStep.id) return;
1772
1855
 
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
- };
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
+ };
1783
1864
 
1784
1865
  const sourcePos = sourceStep.configuration?.workflow_position || {
1785
1866
  x: 0,
@@ -1795,12 +1876,25 @@ const WorkflowEditor = ({ data }) => {
1795
1876
  // Push steps that are to the right of the insertion band
1796
1877
  const bandMinX = targetX - newSize.width * 0.25;
1797
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;
1798
1883
 
1799
1884
  freshSteps.forEach((s) => {
1800
1885
  const cfgPos = s.configuration?.workflow_position;
1801
1886
  if (!cfgPos) return;
1802
1887
  if (String(s.id) === String(newStep.id)) return;
1803
- if (cfgPos.x >= bandMinX) {
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) {
1804
1898
  updates.push({
1805
1899
  id: String(s.id),
1806
1900
  x: cfgPos.x + shiftX,
@@ -1963,7 +2057,11 @@ const WorkflowEditor = ({ data }) => {
1963
2057
 
1964
2058
  const hasSteps = steps.length > 0;
1965
2059
  const shellClass = modal ? "wf-shell wf-shell--drawer-open" : "wf-shell";
1966
- const shellStyle = { "--wf-drawer-width": `${drawerWidth}px` };
2060
+ const shellStyle = {
2061
+ "--wf-drawer-width": `${drawerWidth}px`,
2062
+ height: shellHeight ? `${shellHeight}px` : undefined,
2063
+ maxHeight: shellHeight ? `${shellHeight}px` : undefined,
2064
+ };
1967
2065
  const showMiniMap = !modal;
1968
2066
  const shouldFitView = !pendingViewportRef.current;
1969
2067
 
@@ -2060,85 +2158,132 @@ const WorkflowEditor = ({ data }) => {
2060
2158
  };
2061
2159
  }, [drawerWidth, modal]);
2062
2160
 
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>
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}
2096
2214
  </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)}
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 })}
2121
2219
  >
2122
- {showMiniMap && <MiniMap pannable zoomable />}
2123
- <Controls />
2124
- <Background gap={16} />
2125
- </ReactFlow>
2220
+ {strings.addStep}
2221
+ </button>
2222
+ <button
2223
+ className="btn btn-sm btn-outline-secondary ms-2"
2224
+ onClick={reload}
2225
+ disabled={loading}
2226
+ >
2227
+ {loading ? strings.loading : strings.refresh}
2228
+ </button>
2126
2229
  </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
2230
  </div>
2141
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
+ );
2142
2287
  };
2143
2288
 
2144
2289
  export default WorkflowEditor;