@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/dist/workflow_bundle.js +1 -1
- package/package.json +1 -1
- package/src/WorkflowEditor.jsx +294 -141
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,
|
|
@@ -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:
|
|
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
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
e
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
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
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
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
|
-
}, [
|
|
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 (
|
|
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
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
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
|
|
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({
|
|
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
|
-
|
|
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(
|
|
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(
|
|
1755
|
+
onNodesChange(safeChanges);
|
|
1668
1756
|
},
|
|
1669
|
-
[
|
|
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
|
-
|
|
1708
|
-
|
|
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
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
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
|
-
|
|
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 = {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)}
|
|
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
|
-
{
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
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;
|