@saltcorn/workflow-editor 1.6.0-alpha.14 → 1.6.0-alpha.15

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.14",
3
+ "version": "1.6.0-alpha.15",
4
4
  "description": "React Flow-based workflow editor for Saltcorn",
5
5
  "main": "index.js",
6
6
  "homepage": "https://saltcorn.com",
@@ -732,6 +732,15 @@ const StepDrawer = ({
732
732
  if (e?.nativeEvent?.stopImmediatePropagation)
733
733
  e.nativeEvent.stopImmediatePropagation();
734
734
  }, []);
735
+ const handleSaveClick = useCallback(() => {
736
+ const form = innerRef?.current?.querySelector("form");
737
+ if (!form) return;
738
+ if (typeof form.requestSubmit === "function") form.requestSubmit();
739
+ else
740
+ form.dispatchEvent(
741
+ new Event("submit", { cancelable: true, bubbles: true })
742
+ );
743
+ }, [innerRef]);
735
744
  return (
736
745
  <div
737
746
  ref={outerRef}
@@ -792,16 +801,9 @@ const StepDrawer = ({
792
801
  <button className="btn btn-secondary" onClick={onClose}>
793
802
  Close
794
803
  </button>
795
-
796
804
  <button
797
805
  className="btn btn-primary"
798
- onClick={() =>
799
- innerRef.current
800
- ?.querySelector("form")
801
- ?.dispatchEvent(
802
- new Event("submit", { cancelable: true, bubbles: true })
803
- )
804
- }
806
+ onClick={handleSaveClick}
805
807
  disabled={submitting}
806
808
  >
807
809
  {submitting ? "Saving..." : "Save"}
@@ -847,6 +849,8 @@ const WorkflowEditor = ({ data }) => {
847
849
  const [selectedEdgeId, setSelectedEdgeId] = useState(null);
848
850
  const autoSaveTimerRef = useRef(null);
849
851
  const autoSavingRef = useRef(false);
852
+ const lastAutosaveSnapshotRef = useRef("");
853
+ const pendingAutosaveSnapshotRef = useRef("");
850
854
  const nodesRef = useRef([]);
851
855
  const [headerHost, setHeaderHost] = useState(null);
852
856
  const [shellHeight, setShellHeight] = useState(null);
@@ -984,6 +988,24 @@ const WorkflowEditor = ({ data }) => {
984
988
  [viewportStorageKey]
985
989
  );
986
990
 
991
+ const serializeForm = useCallback((formEl) => {
992
+ if (!formEl) return "";
993
+ const fd = new FormData(formEl);
994
+ const entries = [];
995
+ for (const [key, value] of fd.entries()) {
996
+ if (value instanceof File) {
997
+ entries.push([key, `file:${value.name}:${value.size}`]);
998
+ } else {
999
+ entries.push([key, `${value}`]);
1000
+ }
1001
+ }
1002
+ entries.sort((a, b) => {
1003
+ if (a[0] === b[0]) return a[1].localeCompare(b[1]);
1004
+ return a[0].localeCompare(b[0]);
1005
+ });
1006
+ return JSON.stringify(entries);
1007
+ }, []);
1008
+
987
1009
  const onInit = useCallback((instance) => {
988
1010
  rfInstanceRef.current = instance;
989
1011
  }, []);
@@ -1490,6 +1512,8 @@ const WorkflowEditor = ({ data }) => {
1490
1512
  if (!modal) return undefined;
1491
1513
  const formEl = modalRef.current?.querySelector("form");
1492
1514
  if (!formEl) return undefined;
1515
+ lastAutosaveSnapshotRef.current = serializeForm(formEl);
1516
+ pendingAutosaveSnapshotRef.current = "";
1493
1517
  // Re-run show-if logic so action-specific fields render correctly
1494
1518
  setTimeout(() => {
1495
1519
  if (typeof window !== "undefined") {
@@ -1501,22 +1525,45 @@ const WorkflowEditor = ({ data }) => {
1501
1525
 
1502
1526
  const actionSelect = formEl.querySelector('[name="wf_action_name"]');
1503
1527
  const allowAutoSave = !!modal?.stepId; // avoid auto-save for brand new steps to prevent insert errors
1528
+
1529
+ const shouldAutoSaveForAction = () => !!allowAutoSave;
1530
+
1531
+ const cancelPendingAutoSave = () => {
1532
+ if (autoSaveTimerRef.current) {
1533
+ clearTimeout(autoSaveTimerRef.current);
1534
+ autoSaveTimerRef.current = null;
1535
+ }
1536
+ pendingAutosaveSnapshotRef.current = "";
1537
+ };
1538
+
1504
1539
  const handleActionChange = () => {
1505
1540
  if (typeof window !== "undefined" && window.apply_showif)
1506
1541
  window.apply_showif();
1542
+ if (!shouldAutoSaveForAction()) cancelPendingAutoSave();
1507
1543
  };
1508
1544
  if (actionSelect)
1509
1545
  actionSelect.addEventListener("change", handleActionChange);
1510
1546
 
1511
1547
  const scheduleAutoSave = allowAutoSave
1512
1548
  ? () => {
1549
+ if (!shouldAutoSaveForAction()) return;
1513
1550
  if (autoSavingRef.current) return;
1551
+ const snapshot = serializeForm(formEl);
1552
+ if (!snapshot) return;
1553
+ if (snapshot === lastAutosaveSnapshotRef.current) return;
1554
+ if (snapshot === pendingAutosaveSnapshotRef.current) return;
1514
1555
  if (autoSaveTimerRef.current) clearTimeout(autoSaveTimerRef.current);
1556
+ pendingAutosaveSnapshotRef.current = snapshot;
1515
1557
  autoSaveTimerRef.current = setTimeout(async () => {
1558
+ pendingAutosaveSnapshotRef.current = "";
1559
+ const currentSnapshot = serializeForm(formEl);
1560
+ if (!currentSnapshot) return;
1561
+ if (currentSnapshot === lastAutosaveSnapshotRef.current) return;
1516
1562
  autoSavingRef.current = true;
1517
1563
  try {
1518
1564
  await submitStepForm(formEl, { closeOnSuccess: false });
1519
- } catch (err) {
1565
+ lastAutosaveSnapshotRef.current = currentSnapshot;
1566
+ } catch (err) {
1520
1567
  setError(err.message);
1521
1568
  } finally {
1522
1569
  autoSavingRef.current = false;
@@ -1528,8 +1575,23 @@ const WorkflowEditor = ({ data }) => {
1528
1575
 
1529
1576
  const submitHandler = async (e) => {
1530
1577
  e.preventDefault();
1578
+ const currentSnapshot = serializeForm(formEl);
1579
+ const alreadySaved =
1580
+ shouldAutoSaveForAction() &&
1581
+ !!lastAutosaveSnapshotRef.current &&
1582
+ currentSnapshot === lastAutosaveSnapshotRef.current;
1583
+
1584
+ if (alreadySaved) {
1585
+ setModal(null);
1586
+ clearDrawerState();
1587
+ setSavingModal(false);
1588
+ return;
1589
+ }
1590
+
1531
1591
  try {
1592
+ cancelPendingAutoSave();
1532
1593
  await submitStepForm(formEl);
1594
+ lastAutosaveSnapshotRef.current = currentSnapshot;
1533
1595
  } catch (err) {
1534
1596
  setError(err.message);
1535
1597
  } finally {
@@ -1542,7 +1604,7 @@ const WorkflowEditor = ({ data }) => {
1542
1604
  formEl.addEventListener("change", scheduleAutoSave);
1543
1605
  }
1544
1606
  return () => {
1545
- if (autoSaveTimerRef.current) clearTimeout(autoSaveTimerRef.current);
1607
+ cancelPendingAutoSave();
1546
1608
  formEl.removeEventListener("submit", submitHandler);
1547
1609
  if (scheduleAutoSave) {
1548
1610
  formEl.removeEventListener("input", scheduleAutoSave);
@@ -1550,8 +1612,9 @@ const WorkflowEditor = ({ data }) => {
1550
1612
  }
1551
1613
  if (actionSelect)
1552
1614
  actionSelect.removeEventListener("change", handleActionChange);
1615
+ lastAutosaveSnapshotRef.current = "";
1553
1616
  };
1554
- }, [modal, submitStepForm]);
1617
+ }, [modal, serializeForm, submitStepForm, clearDrawerState, setModal]);
1555
1618
 
1556
1619
  useEffect(() => {
1557
1620
  if (typeof window === "undefined") return undefined;