@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/dist/workflow_bundle.js +1 -1
- package/package.json +1 -1
- package/src/WorkflowEditor.jsx +285 -140
package/package.json
CHANGED
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,
|
|
@@ -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
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
e
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
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
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
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
|
-
}, [
|
|
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 (
|
|
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
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
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
|
|
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({
|
|
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
|
-
|
|
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(
|
|
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(
|
|
1747
|
+
onNodesChange(safeChanges);
|
|
1668
1748
|
},
|
|
1669
|
-
[
|
|
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
|
-
|
|
1708
|
-
|
|
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
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
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
|
-
|
|
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 = {
|
|
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
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
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
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
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
|
-
{
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
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;
|