diff --git a/src/actions/sponsor-actions.js b/src/actions/sponsor-actions.js
index b32b68b40..13d6614fa 100644
--- a/src/actions/sponsor-actions.js
+++ b/src/actions/sponsor-actions.js
@@ -611,7 +611,11 @@ export const saveSponsor = (entity) => async (dispatch, getState) => {
const { currentSummit } = currentSummitState;
const params = {
- access_token: accessToken
+ access_token: accessToken,
+ expand:
+ "company,members,sponsorships,sponsorships.type,sponsorships.type.type,featured_event,extra_questions,extra_questions.values,lead_report_setting",
+ fields:
+ "featured_event.id,featured_event.title,sponsorships.id,sponsorships.type.id,sponsorships.type.type.id,sponsorships.type.type.name"
};
dispatch(startLoading());
@@ -619,40 +623,42 @@ export const saveSponsor = (entity) => async (dispatch, getState) => {
const normalizedEntity = normalizeSponsor(entity);
if (entity.id) {
- putRequest(
+ return putRequest(
createAction(UPDATE_SPONSOR),
createAction(SPONSOR_UPDATED),
`${window.API_BASE_URL}/api/v1/summits/${currentSummit.id}/sponsors/${entity.id}`,
normalizedEntity,
- authErrorHandler,
+ snackbarErrorHandler,
entity
- )(params)(dispatch).then(() => {
- dispatch(showSuccessMessage(T.translate("edit_sponsor.sponsor_saved")));
- });
- } else {
- const success_message = {
- title: T.translate("general.done"),
- html: T.translate("edit_sponsor.sponsor_created"),
- type: "success"
- };
+ )(params)(dispatch)
+ .then(() => {
+ dispatch(
+ snackbarSuccessHandler({
+ title: T.translate("general.success"),
+ html: T.translate("edit_sponsor.sponsor_saved")
+ })
+ );
+ })
+ .finally(() => dispatch(stopLoading()));
+ }
- postRequest(
- createAction(UPDATE_SPONSOR),
- createAction(SPONSOR_ADDED),
- `${window.API_BASE_URL}/api/v1/summits/${currentSummit.id}/sponsors`,
- normalizedEntity,
- authErrorHandler,
- entity
- )(params)(dispatch).then((payload) => {
+ return postRequest(
+ createAction(UPDATE_SPONSOR),
+ createAction(SPONSOR_ADDED),
+ `${window.API_BASE_URL}/api/v1/summits/${currentSummit.id}/sponsors`,
+ normalizedEntity,
+ snackbarErrorHandler,
+ entity
+ )(params)(dispatch)
+ .then(() => {
dispatch(
- showMessage(success_message, () => {
- history.push(
- `/app/summits/${currentSummit.id}/sponsors/${payload.response.id}`
- );
+ snackbarSuccessHandler({
+ title: T.translate("general.success"),
+ html: T.translate("edit_sponsor.sponsor_created")
})
);
- });
- }
+ })
+ .finally(() => dispatch(stopLoading()));
};
export const addMemberToSponsor =
@@ -746,16 +752,21 @@ export const updateSponsorOrder =
const normalizeSponsor = (entity) => {
const normalizedEntity = { ...entity };
- normalizedEntity.company_id = normalizedEntity.company?.id || 0;
- normalizedEntity.sponsorship_id = normalizedEntity.sponsorship?.id || 0;
- normalizedEntity.featured_event_id =
- normalizedEntity.featured_event && normalizedEntity.featured_event.id
- ? normalizedEntity.featured_event.id
- : 0;
-
- delete normalizedEntity.featured_event;
- delete normalizedEntity.company;
- delete normalizedEntity.sponsorship;
+ if (normalizedEntity.hasOwnProperty("company")) {
+ normalizedEntity.company_id = normalizedEntity.company?.id || 0;
+ delete normalizedEntity.company;
+ }
+
+ if (normalizedEntity.hasOwnProperty("sponsorship")) {
+ normalizedEntity.sponsorship_id = normalizedEntity.sponsorship?.id || 0;
+ delete normalizedEntity.sponsorship;
+ }
+
+ if (normalizedEntity.hasOwnProperty("featured_event")) {
+ normalizedEntity.featured_event_id =
+ normalizedEntity.featured_event?.id || 0;
+ delete normalizedEntity.featured_event;
+ }
return normalizedEntity;
};
diff --git a/src/pages/sponsors/sponsor-page/tabs/sponsor-general-form/__tests__/sponsor-header.test.js b/src/pages/sponsors/sponsor-page/tabs/sponsor-general-form/__tests__/sponsor-header.test.js
new file mode 100644
index 000000000..0f0505f3f
--- /dev/null
+++ b/src/pages/sponsors/sponsor-page/tabs/sponsor-general-form/__tests__/sponsor-header.test.js
@@ -0,0 +1,74 @@
+import React from "react";
+import { render, screen, waitFor, fireEvent } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import SponsorHeader from "../sponsor-header";
+
+const buildSponsor = (overrides = {}) => ({
+ id: 5,
+ is_published: false,
+ company: {
+ name: "Acme Corp",
+ city: "Austin",
+ state: "TX",
+ country: "USA",
+ contact_email: "contact@acme.com"
+ },
+ ...overrides
+});
+
+const deferred = () => {
+ let resolve;
+ const promise = new Promise((res) => {
+ resolve = res;
+ });
+ return { promise, resolve };
+};
+
+describe("SponsorHeader", () => {
+ it("renders sponsor info and the current publication state", () => {
+ const sponsor = buildSponsor({ is_published: true });
+
+ render();
+
+ expect(screen.getByText("Acme Corp")).toBeInTheDocument();
+ expect(screen.getByText("edit_sponsor.is_published")).toBeInTheDocument();
+ expect(screen.getByRole("checkbox")).toBeChecked();
+ });
+
+ it("saves the toggled value, disables controls while pending, blocks a second submit, and re-enables on resolve", async () => {
+ const { promise, resolve } = deferred();
+ const onSave = jest.fn(() => promise);
+ const sponsor = buildSponsor({ id: 5, is_published: false });
+
+ render();
+
+ await userEvent.click(screen.getByRole("checkbox"));
+ const saveButton = screen.getByRole("button", { name: "general.save" });
+ await userEvent.click(saveButton);
+
+ expect(onSave).toHaveBeenCalledWith({ id: 5, is_published: true });
+ expect(saveButton).toBeDisabled();
+ expect(screen.getByRole("checkbox")).toBeDisabled();
+
+ // the button is disabled now, so a second click can't invoke the handler again
+ fireEvent.click(saveButton);
+ expect(onSave).toHaveBeenCalledTimes(1);
+
+ resolve();
+ await waitFor(() => expect(saveButton).toBeEnabled());
+ expect(screen.getByRole("checkbox")).toBeEnabled();
+ });
+
+ it("re-enables controls and does not blow up when the save fails", async () => {
+ const onSave = jest.fn().mockRejectedValue(new Error("save failed"));
+ const sponsor = buildSponsor({ is_published: false });
+
+ render();
+
+ const saveButton = screen.getByRole("button", { name: "general.save" });
+ await userEvent.click(saveButton);
+
+ await waitFor(() => expect(saveButton).toBeEnabled());
+ expect(screen.getByRole("checkbox")).toBeEnabled();
+ });
+});
diff --git a/src/pages/sponsors/sponsor-page/tabs/sponsor-general-form/index.js b/src/pages/sponsors/sponsor-page/tabs/sponsor-general-form/index.js
index ee86dd44f..9ad9868a9 100644
--- a/src/pages/sponsors/sponsor-page/tabs/sponsor-general-form/index.js
+++ b/src/pages/sponsors/sponsor-page/tabs/sponsor-general-form/index.js
@@ -28,6 +28,7 @@ import {
removeTierFromSponsor,
resetSponsorExtraQuestionForm,
saveAddonsToSponsorship,
+ saveSponsor,
saveSponsorExtraQuestion,
saveSponsorExtraQuestionValue,
setSelectedSponsorship,
@@ -52,7 +53,8 @@ const SponsorGeneralForm = ({
saveSponsorExtraQuestionValue,
resetSponsorExtraQuestionForm,
deleteExtraQuestion,
- updateExtraQuestionOrder
+ updateExtraQuestionOrder,
+ saveSponsor
}) => {
const handleSponsorshipPaginate = (page, perPage, order, orderDir) => {
getSponsorSponsorships(sponsor.id, page, perPage, order, orderDir);
@@ -60,7 +62,7 @@ const SponsorGeneralForm = ({
return (
-
+
(
-
-
-
-
- {T.translate("edit_sponsor.general_information")}
-
+const SponsorHeader = ({ sponsor, onSave }) => {
+ const [isPublished, setIsPublished] = useState(sponsor.is_published);
+ const [isSaving, setIsSaving] = useState(false);
+
+ useEffect(() => {
+ setIsPublished(sponsor.is_published);
+ }, [sponsor.id, sponsor.is_published]);
+
+ const handlePublishSave = () => {
+ const entity = { id: sponsor.id, is_published: isPublished };
+ setIsSaving(true);
+ Promise.resolve(onSave(entity))
+ .catch(() => {})
+ .finally(() => setIsSaving(false));
+ };
+
+ return (
+
+
+
+
+ {T.translate("edit_sponsor.general_information")}
+
+
-
-
-
-
-
- {T.translate("edit_sponsor.sponsor_name")}
-
+
+
+
+
+ {T.translate("edit_sponsor.sponsor_name")}
+
+
+ {sponsor.company?.name}
- {sponsor.company?.name}
-
-
-
-
-
- {T.translate("edit_sponsor.sponsor_address")}
-
+
+
+
+
+ {T.translate("edit_sponsor.sponsor_address")}
+
+
+
+ {[
+ sponsor.company?.city,
+ sponsor.company?.state,
+ sponsor.company?.country
+ ]
+ // filter empty or undefined fields
+ .filter(Boolean)
+ .join(" - ")}
+ {sponsor.company?.contact_email && (
+ <>
+
+
+ {sponsor.company.contact_email}
+
+ >
+ )}
+
-
- {[
- sponsor.company?.city,
- sponsor.company?.state,
- sponsor.company?.country
- ]
- // filter empty or undefined fields
- .filter(Boolean)
- .join(" - ")}
- {sponsor.company?.contact_email && (
- <>
-
-
- {sponsor.company.contact_email}
-
- >
- )}
+
+
+
+
+
+
+ setIsPublished(!isPublished)}
+ inputProps={{ "aria-labelledby": "sponsor-is-published-label" }}
+ />
+
+
-
-
-);
+
+ );
+};
export default SponsorHeader;
diff --git a/src/reducers/sponsors/__tests__/sponsor-reducer.test.js b/src/reducers/sponsors/__tests__/sponsor-reducer.test.js
new file mode 100644
index 000000000..9901d373d
--- /dev/null
+++ b/src/reducers/sponsors/__tests__/sponsor-reducer.test.js
@@ -0,0 +1,100 @@
+import sponsorReducer, { DEFAULT_STATE } from "../sponsor-reducer";
+import {
+ UPDATE_SPONSOR,
+ SPONSOR_UPDATED,
+ SPONSOR_ADDED,
+ RECEIVE_SPONSOR
+} from "../../../actions/sponsor-actions";
+
+jest.mock("i18n-react/dist/i18n-react", () => ({
+ translate: jest.fn((key) => key)
+}));
+
+jest.mock("openstack-uicore-foundation/lib/security/actions", () => ({
+ LOGOUT_USER: "LOGOUT_USER"
+}));
+
+function createState(overrides = {}) {
+ return {
+ ...DEFAULT_STATE,
+ entity: { ...DEFAULT_STATE.entity, ...overrides }
+ };
+}
+
+describe("sponsorReducer", () => {
+ describe("UPDATE_SPONSOR", () => {
+ it("merges the payload into the existing entity instead of replacing it", () => {
+ const state = createState({
+ id: 5,
+ is_published: false,
+ company: { id: 9, name: "Acme" },
+ header_image: "https://cdn.example.com/header.png"
+ });
+
+ const result = sponsorReducer(state, {
+ type: UPDATE_SPONSOR,
+ payload: { id: 5, is_published: true }
+ });
+
+ expect(result.entity.is_published).toBe(true);
+ // fields not present on the payload (e.g. from a partial save) survive
+ expect(result.entity.company).toEqual({ id: 9, name: "Acme" });
+ expect(result.entity.header_image).toBe(
+ "https://cdn.example.com/header.png"
+ );
+ expect(result.errors).toEqual({});
+ });
+ });
+
+ describe.each([
+ ["SPONSOR_UPDATED", SPONSOR_UPDATED],
+ ["SPONSOR_ADDED", SPONSOR_ADDED],
+ ["RECEIVE_SPONSOR", RECEIVE_SPONSOR]
+ ])("%s", (_name, actionType) => {
+ it("merges the server response into the existing entity", () => {
+ const state = createState({
+ id: 5,
+ is_published: false,
+ header_image: "https://cdn.example.com/header.png"
+ });
+
+ const result = sponsorReducer(state, {
+ type: actionType,
+ payload: {
+ response: {
+ id: 5,
+ is_published: true,
+ sponsorships: null,
+ lead_report_setting: null
+ }
+ }
+ });
+
+ expect(result.entity.is_published).toBe(true);
+ // untouched fields from the previous entity are preserved
+ expect(result.entity.header_image).toBe(
+ "https://cdn.example.com/header.png"
+ );
+ });
+
+ it("normalizes null response fields to empty defaults", () => {
+ const state = createState({ id: 5 });
+
+ const result = sponsorReducer(state, {
+ type: actionType,
+ payload: {
+ response: {
+ id: 5,
+ intro: null,
+ sponsorships: null,
+ lead_report_setting: null
+ }
+ }
+ });
+
+ expect(result.entity.intro).toBe("");
+ expect(result.entity.sponsorships).toEqual([]);
+ expect(result.entity.lead_report_setting).toEqual({});
+ });
+ });
+});
diff --git a/src/reducers/sponsors/sponsor-reducer.js b/src/reducers/sponsors/sponsor-reducer.js
index 9736a35db..a26dd8705 100644
--- a/src/reducers/sponsors/sponsor-reducer.js
+++ b/src/reducers/sponsors/sponsor-reducer.js
@@ -153,8 +153,9 @@ const sponsorReducer = (state = DEFAULT_STATE, action) => {
return { ...state, entity: { ...DEFAULT_ENTITY }, errors: {} };
case UPDATE_SPONSOR:
- return { ...state, entity: { ...payload }, errors: {} };
+ return { ...state, entity: { ...state.entity, ...payload }, errors: {} };
case SPONSOR_ADDED:
+ case SPONSOR_UPDATED:
case RECEIVE_SPONSOR: {
const entity = { ...payload.response };
@@ -175,8 +176,6 @@ const sponsorReducer = (state = DEFAULT_STATE, action) => {
entity: { ...state.entity, ...entity }
};
}
- case SPONSOR_UPDATED:
- return state;
case MEMBER_ADDED_TO_SPONSOR: {
const { member } = payload;
return {