@saltcorn/workflow-editor 1.6.0-alpha.13 → 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.13",
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,21 +801,13 @@ const StepDrawer = ({
792
801
  <button className="btn btn-secondary" onClick={onClose}>
793
802
  Close
794
803
  </button>
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
- )}
804
+ <button
805
+ className="btn btn-primary"
806
+ onClick={handleSaveClick}
807
+ disabled={submitting}
808
+ >
809
+ {submitting ? "Saving..." : "Save"}
810
+ </button>
810
811
  </div>
811
812
  </>
812
813
  ) : null}
@@ -848,6 +849,8 @@ const WorkflowEditor = ({ data }) => {
848
849
  const [selectedEdgeId, setSelectedEdgeId] = useState(null);
849
850
  const autoSaveTimerRef = useRef(null);
850
851
  const autoSavingRef = useRef(false);
852
+ const lastAutosaveSnapshotRef = useRef("");
853
+ const pendingAutosaveSnapshotRef = useRef("");
851
854
  const nodesRef = useRef([]);
852
855
  const [headerHost, setHeaderHost] = useState(null);
853
856
  const [shellHeight, setShellHeight] = useState(null);
@@ -985,6 +988,24 @@ const WorkflowEditor = ({ data }) => {
985
988
  [viewportStorageKey]
986
989
  );
987
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
+
988
1009
  const onInit = useCallback((instance) => {
989
1010
  rfInstanceRef.current = instance;
990
1011
  }, []);
@@ -1491,6 +1512,8 @@ const WorkflowEditor = ({ data }) => {
1491
1512
  if (!modal) return undefined;
1492
1513
  const formEl = modalRef.current?.querySelector("form");
1493
1514
  if (!formEl) return undefined;
1515
+ lastAutosaveSnapshotRef.current = serializeForm(formEl);
1516
+ pendingAutosaveSnapshotRef.current = "";
1494
1517
  // Re-run show-if logic so action-specific fields render correctly
1495
1518
  setTimeout(() => {
1496
1519
  if (typeof window !== "undefined") {
@@ -1502,21 +1525,44 @@ const WorkflowEditor = ({ data }) => {
1502
1525
 
1503
1526
  const actionSelect = formEl.querySelector('[name="wf_action_name"]');
1504
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
+
1505
1539
  const handleActionChange = () => {
1506
1540
  if (typeof window !== "undefined" && window.apply_showif)
1507
1541
  window.apply_showif();
1542
+ if (!shouldAutoSaveForAction()) cancelPendingAutoSave();
1508
1543
  };
1509
1544
  if (actionSelect)
1510
1545
  actionSelect.addEventListener("change", handleActionChange);
1511
1546
 
1512
1547
  const scheduleAutoSave = allowAutoSave
1513
1548
  ? () => {
1549
+ if (!shouldAutoSaveForAction()) return;
1514
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;
1515
1555
  if (autoSaveTimerRef.current) clearTimeout(autoSaveTimerRef.current);
1556
+ pendingAutosaveSnapshotRef.current = snapshot;
1516
1557
  autoSaveTimerRef.current = setTimeout(async () => {
1558
+ pendingAutosaveSnapshotRef.current = "";
1559
+ const currentSnapshot = serializeForm(formEl);
1560
+ if (!currentSnapshot) return;
1561
+ if (currentSnapshot === lastAutosaveSnapshotRef.current) return;
1517
1562
  autoSavingRef.current = true;
1518
1563
  try {
1519
1564
  await submitStepForm(formEl, { closeOnSuccess: false });
1565
+ lastAutosaveSnapshotRef.current = currentSnapshot;
1520
1566
  } catch (err) {
1521
1567
  setError(err.message);
1522
1568
  } finally {
@@ -1529,8 +1575,23 @@ const WorkflowEditor = ({ data }) => {
1529
1575
 
1530
1576
  const submitHandler = async (e) => {
1531
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
+
1532
1591
  try {
1592
+ cancelPendingAutoSave();
1533
1593
  await submitStepForm(formEl);
1594
+ lastAutosaveSnapshotRef.current = currentSnapshot;
1534
1595
  } catch (err) {
1535
1596
  setError(err.message);
1536
1597
  } finally {
@@ -1543,7 +1604,7 @@ const WorkflowEditor = ({ data }) => {
1543
1604
  formEl.addEventListener("change", scheduleAutoSave);
1544
1605
  }
1545
1606
  return () => {
1546
- if (autoSaveTimerRef.current) clearTimeout(autoSaveTimerRef.current);
1607
+ cancelPendingAutoSave();
1547
1608
  formEl.removeEventListener("submit", submitHandler);
1548
1609
  if (scheduleAutoSave) {
1549
1610
  formEl.removeEventListener("input", scheduleAutoSave);
@@ -1551,8 +1612,9 @@ const WorkflowEditor = ({ data }) => {
1551
1612
  }
1552
1613
  if (actionSelect)
1553
1614
  actionSelect.removeEventListener("change", handleActionChange);
1615
+ lastAutosaveSnapshotRef.current = "";
1554
1616
  };
1555
- }, [modal, submitStepForm]);
1617
+ }, [modal, serializeForm, submitStepForm, clearDrawerState, setModal]);
1556
1618
 
1557
1619
  useEffect(() => {
1558
1620
  if (typeof window === "undefined") return undefined;