diff --git a/src/actions/sponsor-forms-actions.js b/src/actions/sponsor-forms-actions.js
index 78c4e3079..4705fa1ab 100644
--- a/src/actions/sponsor-forms-actions.js
+++ b/src/actions/sponsor-forms-actions.js
@@ -368,7 +368,9 @@ export const cloneGlobalTemplate =
})
);
})
- .catch(() => {}); // need to catch promise reject
+ .finally(() => {
+ dispatch(stopLoading());
+ });
};
export const saveFormTemplate = (entity) => async (dispatch, getState) => {
@@ -403,7 +405,6 @@ export const saveFormTemplate = (entity) => async (dispatch, getState) => {
})
);
})
- .catch(() => {}) // need to catch promise reject
.finally(() => {
dispatch(stopLoading());
});
@@ -443,7 +444,6 @@ export const updateFormTemplate = (entity) => async (dispatch, getState) => {
})
);
})
- .catch(() => {}) // need to catch promise reject
.finally(() => {
dispatch(stopLoading());
});
diff --git a/src/pages/sponsors/sponsor-forms-list-page/components/form-template/__tests__/form-template-popup.test.js b/src/pages/sponsors/sponsor-forms-list-page/components/form-template/__tests__/form-template-popup.test.js
new file mode 100644
index 000000000..062fd01eb
--- /dev/null
+++ b/src/pages/sponsors/sponsor-forms-list-page/components/form-template/__tests__/form-template-popup.test.js
@@ -0,0 +1,110 @@
+import React from "react";
+import { act, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { renderWithRedux } from "utils/test-utils";
+import { saveFormTemplate } from "actions/sponsor-forms-actions";
+import FormTemplatePopup from "../form-template-popup";
+
+jest.mock("actions/sponsor-forms-actions", () => ({
+ saveFormTemplate: jest.fn(),
+ updateFormTemplate: jest.fn(),
+ getSponsorships: jest.fn(() => ({ type: "GET_SPONSORSHIPS" })),
+ resetFormTemplate: jest.fn(() => ({ type: "RESET_TEMPLATE_FORM" }))
+}));
+
+jest.mock("../form-template-form", () => ({
+ __esModule: true,
+ default: ({ onSubmit, isSaving }) => (
+
+ )
+}));
+
+describe("FormTemplatePopup", () => {
+ const initialState = {
+ sponsorFormsListState: {
+ sponsorships: { items: [] },
+ formTemplate: {}
+ },
+ currentSummitState: {
+ currentSummit: {
+ time_zone_id: "UTC"
+ }
+ }
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it("keeps modal open when save fails", async () => {
+ const onClose = jest.fn();
+ saveFormTemplate.mockReturnValue(() => Promise.reject(new Error("dup")));
+
+ renderWithRedux(, {
+ initialState
+ });
+
+ await act(async () => {
+ await userEvent.click(
+ screen.getByRole("button", { name: "submit-form-template" })
+ );
+ await Promise.resolve();
+ });
+
+ expect(saveFormTemplate).toHaveBeenCalledTimes(1);
+ expect(onClose).not.toHaveBeenCalled();
+ });
+
+ it("prevents duplicate save requests while saving", async () => {
+ const onClose = jest.fn();
+ let resolveSave;
+ const pendingPromise = new Promise((resolve) => {
+ resolveSave = resolve;
+ });
+
+ saveFormTemplate.mockReturnValue(() => pendingPromise);
+
+ renderWithRedux(, {
+ initialState
+ });
+
+ const button = screen.getByRole("button", { name: "submit-form-template" });
+
+ await act(async () => {
+ await userEvent.click(button);
+ await userEvent.click(button);
+ });
+
+ expect(saveFormTemplate).toHaveBeenCalledTimes(1);
+
+ await act(async () => {
+ resolveSave();
+ await Promise.resolve();
+ });
+ });
+
+ it("closes modal after successful save", async () => {
+ const onClose = jest.fn();
+ saveFormTemplate.mockReturnValue(() => Promise.resolve());
+
+ renderWithRedux(, {
+ initialState
+ });
+
+ await act(async () => {
+ await userEvent.click(
+ screen.getByRole("button", { name: "submit-form-template" })
+ );
+ await Promise.resolve();
+ });
+
+ expect(saveFormTemplate).toHaveBeenCalledTimes(1);
+ expect(onClose).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/src/pages/sponsors/sponsor-forms-list-page/components/form-template/form-template-form.js b/src/pages/sponsors/sponsor-forms-list-page/components/form-template/form-template-form.js
index 9f70f4ee4..e26f9afab 100644
--- a/src/pages/sponsors/sponsor-forms-list-page/components/form-template/form-template-form.js
+++ b/src/pages/sponsors/sponsor-forms-list-page/components/form-template/form-template-form.js
@@ -46,6 +46,7 @@ const FormTemplateForm = ({
initialValues,
sponsorships,
summitTZ,
+ isSaving = false,
onSubmit
}) => {
const formik = useFormik({
@@ -150,7 +151,12 @@ const FormTemplateForm = ({
-
diff --git a/src/pages/sponsors/sponsor-forms-list-page/components/form-template/form-template-popup.js b/src/pages/sponsors/sponsor-forms-list-page/components/form-template/form-template-popup.js
index 160c03546..952002057 100644
--- a/src/pages/sponsors/sponsor-forms-list-page/components/form-template/form-template-popup.js
+++ b/src/pages/sponsors/sponsor-forms-list-page/components/form-template/form-template-popup.js
@@ -1,4 +1,4 @@
-import React, { useEffect } from "react";
+import React, { useEffect, useState } from "react";
import PropTypes from "prop-types";
import T from "i18n-react/dist/i18n-react";
import { connect } from "react-redux";
@@ -31,33 +31,52 @@ const FormTemplatePopup = ({
updateFormTemplate,
edit
}) => {
+ const [isSaving, setIsSaving] = useState(false);
+
useEffect(() => {
getSponsorships(1, MAX_PER_PAGE);
}, []);
- const handleClose = () => {
- // clear form from reducer
+ const closePopup = () => {
resetFormTemplate();
onClose();
};
+ const handleClose = () => {
+ if (isSaving) return;
+ closePopup();
+ };
+
const handleOnSave = (values) => {
+ if (isSaving) return;
+
const save = values.id ? updateFormTemplate : saveFormTemplate;
+ setIsSaving(true);
- save(values).finally(() => {
- handleClose();
- });
+ save(values)
+ .then(() => {
+ closePopup();
+ })
+ .catch(() => {
+ // keep dialog open on save error to preserve user input
+ })
+ .finally(() => {
+ setIsSaving(false);
+ });
};
return (
diff --git a/src/pages/sponsors/sponsor-forms-list-page/components/global-template/__tests__/global-template-popup.test.js b/src/pages/sponsors/sponsor-forms-list-page/components/global-template/__tests__/global-template-popup.test.js
new file mode 100644
index 000000000..3b6c2c58c
--- /dev/null
+++ b/src/pages/sponsors/sponsor-forms-list-page/components/global-template/__tests__/global-template-popup.test.js
@@ -0,0 +1,143 @@
+import React from "react";
+import { act, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { renderWithRedux } from "utils/test-utils";
+import { cloneGlobalTemplate } from "actions/sponsor-forms-actions";
+import GlobalTemplatePopup from "../global-template-popup";
+
+jest.mock("@mui/material", () => {
+ const originalModule = jest.requireActual("@mui/material");
+ return {
+ ...originalModule,
+ Dialog: ({ open, onClose, children }) =>
+ open ? (
+
+ onClose({}, "backdropClick")}>
+ dialog-onclose
+
+ {children}
+
+ ) : null
+ };
+});
+
+jest.mock("actions/sponsor-forms-actions", () => ({
+ cloneGlobalTemplate: jest.fn()
+}));
+
+jest.mock("../select-templates-dialog", () => ({
+ __esModule: true,
+ default: ({ onSave }) => (
+
+ onSave([1])}>
+ go-sponsorships
+
+
+ )
+}));
+
+jest.mock("../select-sponsorships-dialog", () => ({
+ __esModule: true,
+ default: ({ onSave, isSaving }) => (
+
+ sponsorships-step
+ onSave([10], false)}
+ disabled={isSaving}
+ >
+ apply-sponsorships
+
+
+ )
+}));
+
+describe("GlobalTemplatePopup", () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it("keeps popup open on clone error", async () => {
+ const onClose = jest.fn();
+ cloneGlobalTemplate.mockReturnValue(() => Promise.reject(new Error("dup")));
+
+ renderWithRedux(, {
+ initialState: {}
+ });
+
+ await act(async () => {
+ await userEvent.click(
+ screen.getByRole("button", { name: "go-sponsorships" })
+ );
+ await userEvent.click(
+ screen.getByRole("button", { name: "apply-sponsorships" })
+ );
+ await Promise.resolve();
+ });
+
+ expect(cloneGlobalTemplate).toHaveBeenCalledTimes(1);
+ expect(onClose).not.toHaveBeenCalled();
+ expect(screen.getByText("sponsorships-step")).toBeTruthy();
+ });
+
+ it("resets stage to templates on dialog onClose", async () => {
+ const onClose = jest.fn();
+
+ renderWithRedux(, {
+ initialState: {}
+ });
+
+ await act(async () => {
+ await userEvent.click(
+ screen.getByRole("button", { name: "go-sponsorships" })
+ );
+ });
+ expect(screen.getByText("sponsorships-step")).toBeTruthy();
+
+ await act(async () => {
+ await userEvent.click(
+ screen.getByRole("button", { name: "dialog-onclose" })
+ );
+ await Promise.resolve();
+ });
+
+ expect(onClose).toHaveBeenCalledTimes(1);
+ expect(
+ screen.queryByRole("button", { name: "go-sponsorships" })
+ ).not.toBeInTheDocument();
+ });
+
+ it("prevents duplicate clone requests while saving", async () => {
+ const onClose = jest.fn();
+ let resolveClone;
+ const pendingPromise = new Promise((resolve) => {
+ resolveClone = resolve;
+ });
+ cloneGlobalTemplate.mockReturnValue(() => pendingPromise);
+
+ renderWithRedux(, {
+ initialState: {}
+ });
+
+ await act(async () => {
+ await userEvent.click(
+ screen.getByRole("button", { name: "go-sponsorships" })
+ );
+ });
+
+ const applyButton = screen.getByRole("button", {
+ name: "apply-sponsorships"
+ });
+ await act(async () => {
+ await userEvent.click(applyButton);
+ await userEvent.click(applyButton);
+ });
+
+ expect(cloneGlobalTemplate).toHaveBeenCalledTimes(1);
+
+ await act(async () => {
+ resolveClone();
+ await Promise.resolve();
+ });
+ });
+});
diff --git a/src/pages/sponsors/sponsor-forms-list-page/components/global-template/global-template-popup.js b/src/pages/sponsors/sponsor-forms-list-page/components/global-template/global-template-popup.js
index 184dead62..7e3f87c35 100644
--- a/src/pages/sponsors/sponsor-forms-list-page/components/global-template/global-template-popup.js
+++ b/src/pages/sponsors/sponsor-forms-list-page/components/global-template/global-template-popup.js
@@ -2,36 +2,60 @@ import React, { useState } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { Dialog } from "@mui/material";
+import { cloneGlobalTemplate } from "../../../../../actions/sponsor-forms-actions";
import SelectTemplatesDialog from "./select-templates-dialog";
import SelectSponsorshipsDialog from "./select-sponsorships-dialog";
-import { cloneGlobalTemplate } from "../../../../../actions/sponsor-forms-actions";
const GlobalTemplatePopup = ({ open, onClose, cloneGlobalTemplate }) => {
const [stage, setStage] = useState("templates");
const [selectedTemplates, setSelectedTemplates] = useState([]);
+ const [isSaving, setIsSaving] = useState(false);
const dialogSize = stage === "templates" ? "md" : "sm";
const handleClose = () => {
+ if (isSaving) return;
setSelectedTemplates([]);
setStage("templates");
onClose();
};
+ const handleDismiss = () => {
+ if (isSaving) return;
+ onClose();
+ };
+
const handleOnSelectTemplates = (templates) => {
setSelectedTemplates(templates);
setStage("sponsorships");
};
const handleOnSave = (selectedTiers, allTiers) => {
- cloneGlobalTemplate(selectedTemplates, selectedTiers, allTiers).finally(
- () => {
- handleClose();
- }
- );
+ if (isSaving) return;
+
+ setIsSaving(true);
+
+ cloneGlobalTemplate(selectedTemplates, selectedTiers, allTiers)
+ .then(() => {
+ setSelectedTemplates([]);
+ setStage("templates");
+ onClose();
+ })
+ .catch(() => {
+ // keep dialog open on save error to preserve user progress
+ })
+ .finally(() => {
+ setIsSaving(false);
+ });
};
return (
-