@saltcorn/workflow-editor 1.6.0-alpha.7 → 1.6.0-alpha.9
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 +173 -22
- package/dist/workflow_bundle.js.LICENSE.txt +61 -0
- package/dist/workflow_bundle.js.map +1 -0
- package/package.json +1 -1
- package/src/WorkflowEditor.jsx +185 -18
- package/src/workflow.css +7 -5
package/package.json
CHANGED
package/src/WorkflowEditor.jsx
CHANGED
|
@@ -4,6 +4,7 @@ import React, {
|
|
|
4
4
|
useRef,
|
|
5
5
|
useState,
|
|
6
6
|
useLayoutEffect,
|
|
7
|
+
useMemo,
|
|
7
8
|
} from "react";
|
|
8
9
|
import {
|
|
9
10
|
Background,
|
|
@@ -699,6 +700,7 @@ const buildGraph = (
|
|
|
699
700
|
const StepDrawer = ({
|
|
700
701
|
modal,
|
|
701
702
|
innerRef,
|
|
703
|
+
outerRef,
|
|
702
704
|
onClose,
|
|
703
705
|
submitting,
|
|
704
706
|
error,
|
|
@@ -710,6 +712,7 @@ const StepDrawer = ({
|
|
|
710
712
|
const isOpen = !!modal;
|
|
711
713
|
return (
|
|
712
714
|
<div
|
|
715
|
+
ref={outerRef}
|
|
713
716
|
className={`wf-drawer${isOpen ? " wf-drawer--open" : ""}`}
|
|
714
717
|
role="dialog"
|
|
715
718
|
aria-modal="false"
|
|
@@ -799,17 +802,99 @@ const WorkflowEditor = ({ data }) => {
|
|
|
799
802
|
return 600;
|
|
800
803
|
});
|
|
801
804
|
const modalRef = useRef(null);
|
|
805
|
+
const drawerRef = useRef(null);
|
|
806
|
+
const shellRef = useRef(null);
|
|
802
807
|
const resizingRef = useRef(false);
|
|
803
808
|
const connectSourceRef = useRef(null);
|
|
804
809
|
const [selectedEdgeId, setSelectedEdgeId] = useState(null);
|
|
805
810
|
|
|
811
|
+
const drawerStorageKey = useMemo(
|
|
812
|
+
() => `wf-drawer:${data?.trigger?.id || "default"}`,
|
|
813
|
+
[data?.trigger?.id]
|
|
814
|
+
);
|
|
815
|
+
|
|
816
|
+
const viewportStorageKey = useMemo(
|
|
817
|
+
() => `wf-viewport:${data?.trigger?.id || "default"}`,
|
|
818
|
+
[data?.trigger?.id]
|
|
819
|
+
);
|
|
820
|
+
|
|
806
821
|
const rfInstanceRef = useRef(null);
|
|
807
822
|
|
|
823
|
+
const persistDrawerState = useCallback(
|
|
824
|
+
(payload) => {
|
|
825
|
+
try {
|
|
826
|
+
if (typeof window === "undefined") return;
|
|
827
|
+
window.sessionStorage.setItem(
|
|
828
|
+
drawerStorageKey,
|
|
829
|
+
JSON.stringify(payload || {})
|
|
830
|
+
);
|
|
831
|
+
} catch (e) {
|
|
832
|
+
// ignore storage errors
|
|
833
|
+
}
|
|
834
|
+
},
|
|
835
|
+
[drawerStorageKey]
|
|
836
|
+
);
|
|
837
|
+
|
|
838
|
+
const clearDrawerState = useCallback(() => {
|
|
839
|
+
try {
|
|
840
|
+
if (typeof window === "undefined") return;
|
|
841
|
+
window.sessionStorage.removeItem(drawerStorageKey);
|
|
842
|
+
} catch (e) {
|
|
843
|
+
// ignore storage errors
|
|
844
|
+
}
|
|
845
|
+
}, [drawerStorageKey]);
|
|
846
|
+
|
|
847
|
+
const loadViewport = useCallback(() => {
|
|
848
|
+
if (typeof window === "undefined") return null;
|
|
849
|
+
try {
|
|
850
|
+
const raw = window.sessionStorage.getItem(viewportStorageKey);
|
|
851
|
+
if (!raw) return null;
|
|
852
|
+
const parsed = JSON.parse(raw);
|
|
853
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
854
|
+
const { x, y, zoom } = parsed;
|
|
855
|
+
if (
|
|
856
|
+
Number.isFinite(Number(x)) &&
|
|
857
|
+
Number.isFinite(Number(y)) &&
|
|
858
|
+
Number.isFinite(Number(zoom))
|
|
859
|
+
) {
|
|
860
|
+
return { x: Number(x), y: Number(y), zoom: Number(zoom) };
|
|
861
|
+
}
|
|
862
|
+
} catch (e) {
|
|
863
|
+
// ignore storage errors
|
|
864
|
+
}
|
|
865
|
+
return null;
|
|
866
|
+
}, [viewportStorageKey]);
|
|
867
|
+
|
|
868
|
+
const persistViewport = useCallback(
|
|
869
|
+
(viewport) => {
|
|
870
|
+
if (!viewport) return;
|
|
871
|
+
const { x, y, zoom } = viewport;
|
|
872
|
+
if (
|
|
873
|
+
!Number.isFinite(Number(x)) ||
|
|
874
|
+
!Number.isFinite(Number(y)) ||
|
|
875
|
+
!Number.isFinite(Number(zoom))
|
|
876
|
+
)
|
|
877
|
+
return;
|
|
878
|
+
try {
|
|
879
|
+
if (typeof window === "undefined") return;
|
|
880
|
+
window.sessionStorage.setItem(
|
|
881
|
+
viewportStorageKey,
|
|
882
|
+
JSON.stringify({ x: Number(x), y: Number(y), zoom: Number(zoom) })
|
|
883
|
+
);
|
|
884
|
+
} catch (e) {
|
|
885
|
+
// ignore storage errors
|
|
886
|
+
}
|
|
887
|
+
},
|
|
888
|
+
[viewportStorageKey]
|
|
889
|
+
);
|
|
890
|
+
|
|
808
891
|
const onInit = useCallback((instance) => {
|
|
809
892
|
rfInstanceRef.current = instance;
|
|
810
893
|
}, []);
|
|
811
894
|
|
|
812
895
|
const [selectedNodes, setSelectedNodes] = useState([]);
|
|
896
|
+
const pendingViewportRef = useRef(loadViewport());
|
|
897
|
+
const appliedViewportRef = useRef(false);
|
|
813
898
|
|
|
814
899
|
const handleSelectionChange = useCallback(({ nodes = [], edges = [] }) => {
|
|
815
900
|
setSelectedNodes(nodes);
|
|
@@ -868,6 +953,16 @@ const WorkflowEditor = ({ data }) => {
|
|
|
868
953
|
refreshGraph(steps);
|
|
869
954
|
}, [steps, refreshGraph]);
|
|
870
955
|
|
|
956
|
+
useEffect(() => {
|
|
957
|
+
if (!rfInstanceRef.current) return;
|
|
958
|
+
if (!pendingViewportRef.current) return;
|
|
959
|
+
if (appliedViewportRef.current) return;
|
|
960
|
+
rfInstanceRef.current.setViewport(pendingViewportRef.current, {
|
|
961
|
+
duration: 0,
|
|
962
|
+
});
|
|
963
|
+
appliedViewportRef.current = true;
|
|
964
|
+
}, [nodes]);
|
|
965
|
+
|
|
871
966
|
useEffect(() => {
|
|
872
967
|
window.addEventListener("keydown", handleKeyDelete);
|
|
873
968
|
return () => window.removeEventListener("keydown", handleKeyDelete);
|
|
@@ -972,7 +1067,6 @@ const WorkflowEditor = ({ data }) => {
|
|
|
972
1067
|
|
|
973
1068
|
const reload = useCallback(async () => {
|
|
974
1069
|
setLoading(true);
|
|
975
|
-
setModal(null);
|
|
976
1070
|
setError("");
|
|
977
1071
|
try {
|
|
978
1072
|
let fresh = await fetchJson(data.urls.data);
|
|
@@ -1117,6 +1211,12 @@ const WorkflowEditor = ({ data }) => {
|
|
|
1117
1211
|
async ({ stepId, initial_step, after_step } = {}) => {
|
|
1118
1212
|
try {
|
|
1119
1213
|
setError("");
|
|
1214
|
+
const descriptor = {
|
|
1215
|
+
stepId: stepId || null,
|
|
1216
|
+
initial_step: !!initial_step,
|
|
1217
|
+
after_step: after_step || null,
|
|
1218
|
+
};
|
|
1219
|
+
persistDrawerState(descriptor);
|
|
1120
1220
|
const base = `${data.urls.stepForm}${stepId ? `/${stepId}` : ""}`;
|
|
1121
1221
|
const url = new URL(base, window.location.origin);
|
|
1122
1222
|
url.searchParams.set("render", "dialog");
|
|
@@ -1125,10 +1225,11 @@ const WorkflowEditor = ({ data }) => {
|
|
|
1125
1225
|
const res = await fetchJson(url.toString());
|
|
1126
1226
|
setModal({ title: res.title, body: res.form, stepId: stepId || null });
|
|
1127
1227
|
} catch (e) {
|
|
1228
|
+
clearDrawerState();
|
|
1128
1229
|
setError(e.message);
|
|
1129
1230
|
}
|
|
1130
1231
|
},
|
|
1131
|
-
[data.urls.stepForm, fetchJson]
|
|
1232
|
+
[clearDrawerState, data.urls.stepForm, fetchJson, persistDrawerState]
|
|
1132
1233
|
);
|
|
1133
1234
|
|
|
1134
1235
|
const handleModalSubmit = useCallback(
|
|
@@ -1149,9 +1250,10 @@ const WorkflowEditor = ({ data }) => {
|
|
|
1149
1250
|
const json = await res.json();
|
|
1150
1251
|
if (json.error) throw new Error(json.error);
|
|
1151
1252
|
setModal(null);
|
|
1253
|
+
clearDrawerState();
|
|
1152
1254
|
await reload();
|
|
1153
1255
|
},
|
|
1154
|
-
[data.csrfToken, data.urls.stepForm, reload]
|
|
1256
|
+
[clearDrawerState, data.csrfToken, data.urls.stepForm, reload]
|
|
1155
1257
|
);
|
|
1156
1258
|
|
|
1157
1259
|
useEffect(() => {
|
|
@@ -1193,6 +1295,19 @@ const WorkflowEditor = ({ data }) => {
|
|
|
1193
1295
|
};
|
|
1194
1296
|
}, [handleModalSubmit, modal]);
|
|
1195
1297
|
|
|
1298
|
+
useEffect(() => {
|
|
1299
|
+
if (typeof window === "undefined") return undefined;
|
|
1300
|
+
const raw = window.sessionStorage.getItem(drawerStorageKey);
|
|
1301
|
+
if (!raw) return undefined;
|
|
1302
|
+
try {
|
|
1303
|
+
const parsed = JSON.parse(raw);
|
|
1304
|
+
if (parsed && typeof parsed === "object") openStepForm(parsed);
|
|
1305
|
+
} catch (e) {
|
|
1306
|
+
clearDrawerState();
|
|
1307
|
+
}
|
|
1308
|
+
return undefined;
|
|
1309
|
+
}, [clearDrawerState, drawerStorageKey, openStepForm]);
|
|
1310
|
+
|
|
1196
1311
|
const updateConnection = useCallback(
|
|
1197
1312
|
async (payload) => {
|
|
1198
1313
|
const body = new URLSearchParams();
|
|
@@ -1243,7 +1358,9 @@ const WorkflowEditor = ({ data }) => {
|
|
|
1243
1358
|
: "";
|
|
1244
1359
|
const simpleNextId = rawNext && idByName[rawNext];
|
|
1245
1360
|
const targetHasNext = !!(
|
|
1246
|
-
tgtStep &&
|
|
1361
|
+
tgtStep &&
|
|
1362
|
+
tgtStep.next_step &&
|
|
1363
|
+
String(tgtStep.next_step).trim()
|
|
1247
1364
|
);
|
|
1248
1365
|
|
|
1249
1366
|
if (
|
|
@@ -1409,15 +1526,22 @@ const WorkflowEditor = ({ data }) => {
|
|
|
1409
1526
|
const onDelete = useCallback(
|
|
1410
1527
|
async (id) => {
|
|
1411
1528
|
if (!window.confirm(strings.confirmDelete)) return;
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1529
|
+
try {
|
|
1530
|
+
const formData = new FormData();
|
|
1531
|
+
formData.append("_csrf", data.csrfToken || "");
|
|
1532
|
+
await fetchJson(`${data.urls.deleteStep}/${id}`, {
|
|
1533
|
+
method: "POST",
|
|
1534
|
+
body: formData,
|
|
1535
|
+
});
|
|
1536
|
+
setModal(null);
|
|
1537
|
+
setError("");
|
|
1538
|
+
clearDrawerState();
|
|
1539
|
+
await reload();
|
|
1540
|
+
} catch (e) {
|
|
1541
|
+
setError(e.message);
|
|
1542
|
+
}
|
|
1419
1543
|
},
|
|
1420
|
-
[data.csrfToken, fetchJson, reload, strings.confirmDelete]
|
|
1544
|
+
[clearDrawerState, data.csrfToken, fetchJson, reload, strings.confirmDelete]
|
|
1421
1545
|
);
|
|
1422
1546
|
|
|
1423
1547
|
useEffect(() => {
|
|
@@ -1485,6 +1609,7 @@ const WorkflowEditor = ({ data }) => {
|
|
|
1485
1609
|
const shellClass = modal ? "wf-shell wf-shell--drawer-open" : "wf-shell";
|
|
1486
1610
|
const shellStyle = { "--wf-drawer-width": `${drawerWidth}px` };
|
|
1487
1611
|
const showMiniMap = !modal;
|
|
1612
|
+
const shouldFitView = !pendingViewportRef.current;
|
|
1488
1613
|
|
|
1489
1614
|
const onEdgeClick = useCallback((_, edge) => {
|
|
1490
1615
|
setSelectedEdgeId(edge.id);
|
|
@@ -1537,8 +1662,50 @@ const WorkflowEditor = ({ data }) => {
|
|
|
1537
1662
|
|
|
1538
1663
|
useEffect(() => () => stopResize(), [stopResize]);
|
|
1539
1664
|
|
|
1665
|
+
const closeModal = useCallback(() => {
|
|
1666
|
+
setModal(null);
|
|
1667
|
+
setError("");
|
|
1668
|
+
clearDrawerState();
|
|
1669
|
+
}, [clearDrawerState]);
|
|
1670
|
+
|
|
1671
|
+
useLayoutEffect(() => {
|
|
1672
|
+
const shellEl = shellRef.current;
|
|
1673
|
+
const drawerEl = drawerRef.current;
|
|
1674
|
+
if (!shellEl) return undefined;
|
|
1675
|
+
const hasResizeObserver = typeof ResizeObserver !== "undefined";
|
|
1676
|
+
|
|
1677
|
+
const computePadding = () => {
|
|
1678
|
+
if (!drawerEl || !modal) {
|
|
1679
|
+
shellEl.style.removeProperty("--wf-shell-padding-right");
|
|
1680
|
+
return;
|
|
1681
|
+
}
|
|
1682
|
+
const shellRect = shellEl.getBoundingClientRect();
|
|
1683
|
+
const drawerRect = drawerEl.getBoundingClientRect();
|
|
1684
|
+
// Space already available to the right of the shell (e.g. due to sidebars)
|
|
1685
|
+
const naturalGap = Math.max(0, window.innerWidth - shellRect.right);
|
|
1686
|
+
const needed = Math.max(0, drawerRect.width - naturalGap) + 4;
|
|
1687
|
+
shellEl.style.setProperty("--wf-shell-padding-right", `${needed}px`);
|
|
1688
|
+
};
|
|
1689
|
+
|
|
1690
|
+
computePadding();
|
|
1691
|
+
|
|
1692
|
+
const resizeObserver = hasResizeObserver
|
|
1693
|
+
? new ResizeObserver(() => computePadding())
|
|
1694
|
+
: null;
|
|
1695
|
+
if (resizeObserver) {
|
|
1696
|
+
resizeObserver.observe(shellEl);
|
|
1697
|
+
if (drawerEl) resizeObserver.observe(drawerEl);
|
|
1698
|
+
}
|
|
1699
|
+
window.addEventListener("resize", computePadding);
|
|
1700
|
+
|
|
1701
|
+
return () => {
|
|
1702
|
+
if (resizeObserver) resizeObserver.disconnect();
|
|
1703
|
+
window.removeEventListener("resize", computePadding);
|
|
1704
|
+
};
|
|
1705
|
+
}, [drawerWidth, modal]);
|
|
1706
|
+
|
|
1540
1707
|
return (
|
|
1541
|
-
<div className={shellClass} style={shellStyle}>
|
|
1708
|
+
<div className={shellClass} style={shellStyle} ref={shellRef}>
|
|
1542
1709
|
<div className="wf-toolbar">
|
|
1543
1710
|
<div className="wf-toolbar__left">
|
|
1544
1711
|
<strong>{strings.configure}</strong>
|
|
@@ -1589,10 +1756,12 @@ const WorkflowEditor = ({ data }) => {
|
|
|
1589
1756
|
nodeTypes={nodeTypes}
|
|
1590
1757
|
edgeTypes={edgeTypes}
|
|
1591
1758
|
onInit={onInit}
|
|
1592
|
-
|
|
1759
|
+
defaultViewport={pendingViewportRef.current || undefined}
|
|
1760
|
+
fitView={shouldFitView}
|
|
1593
1761
|
proOptions={{ hideAttribution: true }}
|
|
1594
1762
|
defaultEdgeOptions={{ animated: true }}
|
|
1595
1763
|
colorMode={window._sc_lightmode || "light"}
|
|
1764
|
+
onMoveEnd={(_, viewport) => persistViewport(viewport)}
|
|
1596
1765
|
>
|
|
1597
1766
|
{showMiniMap && <MiniMap pannable zoomable />}
|
|
1598
1767
|
<Controls />
|
|
@@ -1602,10 +1771,8 @@ const WorkflowEditor = ({ data }) => {
|
|
|
1602
1771
|
<StepDrawer
|
|
1603
1772
|
modal={modal}
|
|
1604
1773
|
innerRef={modalRef}
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
setError("");
|
|
1608
|
-
}}
|
|
1774
|
+
outerRef={drawerRef}
|
|
1775
|
+
onClose={closeModal}
|
|
1609
1776
|
submitting={savingModal}
|
|
1610
1777
|
error={error && modal ? error : ""}
|
|
1611
1778
|
data={data}
|
package/src/workflow.css
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
.wf-shell {
|
|
2
2
|
padding: 0;
|
|
3
|
+
padding-right: var(--wf-shell-padding-right, 0px);
|
|
3
4
|
height: 100%;
|
|
4
5
|
min-height: 0;
|
|
5
6
|
display: flex;
|
|
@@ -14,10 +15,7 @@
|
|
|
14
15
|
--wf-handle-bg: #fff;
|
|
15
16
|
--wf-handle-source-bg: #5a5a5a;
|
|
16
17
|
--wf-drawer-width: 50vw;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
.wf-shell--drawer-open {
|
|
20
|
-
padding-right: calc(var(--wf-drawer-width) - 44px);
|
|
18
|
+
--wf-drawer-offset: -4px;
|
|
21
19
|
}
|
|
22
20
|
|
|
23
21
|
.wf-toolbar {
|
|
@@ -49,6 +47,10 @@
|
|
|
49
47
|
.wf-shell .react-flow__edge-labels {
|
|
50
48
|
z-index: 2;
|
|
51
49
|
}
|
|
50
|
+
.react-flow .react-flow__edges svg {
|
|
51
|
+
overflow: visible !important;
|
|
52
|
+
z-index: 1040;
|
|
53
|
+
}
|
|
52
54
|
|
|
53
55
|
.wf-empty {
|
|
54
56
|
padding: 12px;
|
|
@@ -256,7 +258,7 @@
|
|
|
256
258
|
|
|
257
259
|
@media (max-width: 768px) {
|
|
258
260
|
.wf-shell--drawer-open {
|
|
259
|
-
padding-right:
|
|
261
|
+
padding-right: var(--wf-shell-padding-right, 0px);
|
|
260
262
|
}
|
|
261
263
|
|
|
262
264
|
.wf-drawer {
|