diff --git a/src/actions/__tests__/selection-plan-actions.test.js b/src/actions/__tests__/selection-plan-actions.test.js
new file mode 100644
index 000000000..a67e63e66
--- /dev/null
+++ b/src/actions/__tests__/selection-plan-actions.test.js
@@ -0,0 +1,107 @@
+/**
+ * @jest-environment jsdom
+ */
+import configureStore from "redux-mock-store";
+import thunk from "redux-thunk";
+import flushPromises from "flush-promises";
+import {
+ postRequest,
+ putRequest
+} from "openstack-uicore-foundation/lib/utils/actions";
+import { saveSelectionPlan } from "../selection-plan-actions";
+import * as methods from "../../utils/methods";
+
+jest.mock("openstack-uicore-foundation/lib/utils/actions", () => ({
+ __esModule: true,
+ ...jest.requireActual("openstack-uicore-foundation/lib/utils/actions"),
+ postRequest: jest.fn(),
+ putRequest: jest.fn()
+}));
+
+jest.mock("../marketing-actions", () => ({
+ saveMarketingSetting: jest.fn()
+}));
+
+const requestMock =
+ (requestActionCreator, receiveActionCreator) => () => (dispatch) => {
+ if (requestActionCreator && typeof requestActionCreator === "function") {
+ dispatch(requestActionCreator({}));
+ }
+ return new Promise((resolve) => {
+ if (typeof receiveActionCreator === "function") {
+ dispatch(receiveActionCreator({ response: { id: 1 } }));
+ } else {
+ dispatch(receiveActionCreator);
+ }
+ resolve({ response: { id: 1 } });
+ });
+ };
+
+const storeState = {
+ currentSummitState: { currentSummit: { id: 1 } }
+};
+
+describe("saveSelectionPlan", () => {
+ const middlewares = [thunk];
+ const mockStore = configureStore(middlewares);
+
+ beforeEach(() => {
+ jest.spyOn(methods, "getAccessTokenSafely").mockResolvedValue("TOKEN");
+ postRequest.mockImplementation(requestMock);
+ putRequest.mockImplementation(requestMock);
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ describe("create path (entity has no id)", () => {
+ it("returns a Promise that resolves with the response payload", async () => {
+ const store = mockStore(storeState);
+ const result = store.dispatch(
+ saveSelectionPlan({ name: "CFP 2026", is_enabled: true })
+ );
+ expect(result).toBeInstanceOf(Promise);
+ await expect(result).resolves.toEqual({ id: 1 });
+ });
+
+ it("dispatches SELECTION_PLAN_ADDED then STOP_LOADING on success", async () => {
+ const store = mockStore(storeState);
+ store.dispatch(saveSelectionPlan({ name: "CFP 2026", is_enabled: true }));
+ await flushPromises();
+
+ const actionTypes = store.getActions().map((a) => a.type);
+ expect(actionTypes).toContain("SELECTION_PLAN_ADDED");
+ expect(actionTypes).toContain("STOP_LOADING");
+ expect(actionTypes.indexOf("STOP_LOADING")).toBeGreaterThan(
+ actionTypes.indexOf("SELECTION_PLAN_ADDED")
+ );
+ });
+ });
+
+ describe("update path (entity has id)", () => {
+ it("returns a Promise that resolves with the response payload", async () => {
+ const store = mockStore(storeState);
+ const result = store.dispatch(
+ saveSelectionPlan({ id: 1, name: "CFP 2026", is_enabled: true })
+ );
+ expect(result).toBeInstanceOf(Promise);
+ await expect(result).resolves.toEqual({ id: 1 });
+ });
+
+ it("dispatches SELECTION_PLAN_UPDATED then STOP_LOADING on success", async () => {
+ const store = mockStore(storeState);
+ store.dispatch(
+ saveSelectionPlan({ id: 1, name: "CFP 2026", is_enabled: true })
+ );
+ await flushPromises();
+
+ const actionTypes = store.getActions().map((a) => a.type);
+ expect(actionTypes).toContain("SELECTION_PLAN_UPDATED");
+ expect(actionTypes).toContain("STOP_LOADING");
+ expect(actionTypes.indexOf("STOP_LOADING")).toBeGreaterThan(
+ actionTypes.indexOf("SELECTION_PLAN_UPDATED")
+ );
+ });
+ });
+});
diff --git a/src/actions/selection-plan-actions.js b/src/actions/selection-plan-actions.js
index e0e48ae77..f41aeb1ad 100644
--- a/src/actions/selection-plan-actions.js
+++ b/src/actions/selection-plan-actions.js
@@ -12,7 +12,7 @@
* */
import T from "i18n-react/dist/i18n-react";
-import debounce from "lodash/debounce"
+import debounce from "lodash/debounce";
import {
getRequest,
putRequest,
@@ -22,8 +22,8 @@ import {
stopLoading,
startLoading,
showMessage,
- showSuccessMessage,
authErrorHandler,
+ snackbarErrorHandler,
postFile
} from "openstack-uicore-foundation/lib/utils/actions";
import URI from "urijs";
@@ -35,7 +35,13 @@ import {
fetchErrorHandler
} from "../utils/methods";
import { saveMarketingSetting } from "./marketing-actions";
-import { DEBOUNCE_WAIT, DEFAULT_PER_PAGE } from "../utils/constants";
+import { snackbarSuccessHandler } from "./base-actions";
+import {
+ DEBOUNCE_WAIT,
+ DEFAULT_CURRENT_PAGE,
+ DEFAULT_ORDER_DIR,
+ DEFAULT_PER_PAGE
+} from "../utils/constants";
URI.escapeQuerySpace = false;
@@ -66,7 +72,13 @@ export const SELECTION_PLAN_PROGRESS_FLAG_ORDER_UPDATED =
"SELECTION_PLAN_PROGRESS_FLAG_ORDER_UPDATED";
export const getSelectionPlans =
- (term = "", page = 1, order = "id", orderDir = 1) =>
+ (
+ term = "",
+ page = DEFAULT_CURRENT_PAGE,
+ perPage = DEFAULT_PER_PAGE,
+ order = "id",
+ orderDir = DEFAULT_ORDER_DIR
+ ) =>
async (dispatch, getState) => {
const { currentSummitState } = getState();
const accessToken = await getAccessTokenSafely();
@@ -84,7 +96,7 @@ export const getSelectionPlans =
access_token: accessToken,
relations: "none",
page,
- per_page: DEFAULT_PER_PAGE,
+ per_page: perPage,
order: `${orderDir === 1 ? "" : "-"}${order}`
};
@@ -93,10 +105,11 @@ export const getSelectionPlans =
}
return getRequest(
- null,
+ createAction(REQUEST_SELECTION_PLANS),
createAction(RECEIVE_SELECTION_PLANS),
`${window.API_BASE_URL}/api/v1/summits/${currentSummit.id}/selection-plans`,
- authErrorHandler
+ authErrorHandler,
+ { order, orderDir, page, perPage, term }
)(params)(dispatch).then(async () => {
dispatch(stopLoading());
});
@@ -120,7 +133,7 @@ export const getSelectionPlan =
null,
createAction(RECEIVE_SELECTION_PLAN),
`${window.API_BASE_URL}/api/v1/summits/${currentSummit.id}/selection-plans/${selectionPlanId}`,
- authErrorHandler
+ snackbarErrorHandler
)(params)(dispatch).then(async () => {
await dispatch(getAllowedMembers(selectionPlanId));
await dispatch(
@@ -149,34 +162,42 @@ export const saveSelectionPlan = (entity) => async (dispatch, getState) => {
createAction(SELECTION_PLAN_UPDATED),
`${window.API_BASE_URL}/api/v1/summits/${currentSummit.id}/selection-plans/${entity.id}?access_token=${accessToken}`,
normalizedEntity,
- authErrorHandler,
+ snackbarErrorHandler,
entity
- )({})(dispatch).then((payload) => {
- dispatch(stopLoading());
- dispatch(
- showSuccessMessage(
- T.translate("edit_selection_plan.selection_plan_saved")
- )
- );
- return payload.response;
- });
+ )({})(dispatch)
+ .then((payload) => {
+ dispatch(
+ snackbarSuccessHandler({
+ title: T.translate("general.done"),
+ html: T.translate("edit_selection_plan.selection_plan_saved")
+ })
+ );
+ return payload.response;
+ })
+ .finally(() => {
+ dispatch(stopLoading());
+ });
}
return postRequest(
createAction(UPDATE_SELECTION_PLAN),
createAction(SELECTION_PLAN_ADDED),
`${window.API_BASE_URL}/api/v1/summits/${currentSummit.id}/selection-plans?access_token=${accessToken}`,
normalizedEntity,
- authErrorHandler,
+ snackbarErrorHandler,
entity
- )({})(dispatch).then((payload) => {
- dispatch(stopLoading());
- dispatch(
- showSuccessMessage(
- T.translate("edit_selection_plan.selection_plan_created")
- )
- );
- return payload.response;
- });
+ )({})(dispatch)
+ .then((payload) => {
+ dispatch(
+ snackbarSuccessHandler({
+ title: T.translate("general.done"),
+ html: T.translate("edit_selection_plan.selection_plan_created")
+ })
+ );
+ return payload.response;
+ })
+ .finally(() => {
+ dispatch(stopLoading());
+ });
};
export const deleteSelectionPlan =
@@ -194,7 +215,7 @@ export const deleteSelectionPlan =
createAction(SELECTION_PLAN_DELETED)({ selectionPlanId }),
`${window.API_BASE_URL}/api/v1/summits/${currentSummit.id}/selection-plans/${selectionPlanId}`,
null,
- authErrorHandler
+ snackbarErrorHandler
)(params)(dispatch).then(() => {
dispatch(stopLoading());
});
@@ -240,7 +261,7 @@ export const removeTrackGroupFromSelectionPlan =
createAction(TRACK_GROUP_REMOVED)({ trackGroupId }),
`${window.API_BASE_URL}/api/v1/summits/${currentSummit.id}/selection-plans/${selectionPlanId}/track-groups/${trackGroupId}`,
null,
- authErrorHandler
+ snackbarErrorHandler
)(params)(dispatch).then(() => {
dispatch(stopLoading());
});
@@ -404,7 +425,7 @@ export const saveSelectionPlanExtraQuestion =
createAction(SELECTION_PLAN_EXTRA_QUESTION_UPDATED),
`${window.API_BASE_URL}/api/v1/summits/${currentSummit.id}/selection-plans/${selectionPlanId}/extra-questions/${entity.id}`,
normalizedEntity,
- authErrorHandler,
+ snackbarErrorHandler,
entity
)(params)(dispatch).then((payload) => {
dispatch(stopLoading());
@@ -440,7 +461,7 @@ export const saveSelectionPlanExtraQuestion =
createAction(SELECTION_PLAN_EXTRA_QUESTION_ADDED),
`${window.API_BASE_URL}/api/v1/summits/${currentSummit.id}/selection-plans/${selectionPlanId}/extra-questions`,
normalizedEntity,
- authErrorHandler,
+ snackbarErrorHandler,
entity
)(params)(dispatch).then((payload) => {
dispatch(stopLoading());
@@ -469,7 +490,7 @@ export const deleteSelectionPlanExtraQuestion =
createAction(SELECTION_PLAN_EXTRA_QUESTION_DELETED)({ questionId }),
`${window.API_BASE_URL}/api/v1/summits/${currentSummit.id}/selection-plans/${selectionPlanId}/extra-questions/${questionId}`,
null,
- authErrorHandler
+ snackbarErrorHandler
)(params)(dispatch).then(() => {
dispatch(stopLoading());
});
@@ -491,7 +512,7 @@ export const updateSelectionPlanExtraQuestionOrder =
createAction(SELECTION_PLAN_EXTRA_QUESTION_ORDER_UPDATED)(questions),
`${window.API_BASE_URL}/api/v1/summits/${currentSummit.id}/selection-plans/${selectionPlanId}/extra-questions/${questionId}`,
{ order: newOrder },
- authErrorHandler
+ snackbarErrorHandler
)(params)(dispatch).then(() => {
dispatch(stopLoading());
});
@@ -515,7 +536,7 @@ export const saveSelectionPlanExtraQuestionValue =
createAction(SELECTION_PLAN_EXTRA_QUESTION_VALUE_UPDATED),
`${window.API_BASE_URL}/api/v1/summits/${currentSummit.id}/selection-plans/${selectionPlanId}/extra-questions/${questionId}/values/${entity.id}`,
entity,
- authErrorHandler,
+ snackbarErrorHandler,
entity
)(params)(dispatch).then(() => {
dispatch(stopLoading());
@@ -527,7 +548,7 @@ export const saveSelectionPlanExtraQuestionValue =
createAction(SELECTION_PLAN_EXTRA_QUESTION_VALUE_ADDED),
`${window.API_BASE_URL}/api/v1/summits/${currentSummit.id}/selection-plans/${selectionPlanId}/extra-questions/${questionId}/values`,
entity,
- authErrorHandler,
+ snackbarErrorHandler,
entity
)(params)(dispatch).then(() => {
dispatch(stopLoading());
@@ -559,7 +580,7 @@ export const updateSelectionPlanExtraQuestionValueOrder =
createAction(SELECTION_PLAN_EXTRA_QUESTION_VALUE_UPDATED),
`${window.API_BASE_URL}/api/v1/summits/${summit_id}/selection-plans/${selection_plan_id}/extra-questions/${id}/values/${valueId}`,
{ order: newOrder },
- authErrorHandler,
+ snackbarErrorHandler,
{ order: newOrder, id: valueId }
)(params)(dispatch).then(() => {
dispatch(stopLoading());
@@ -581,7 +602,7 @@ export const deleteSelectionPlanExtraQuestionValue =
createAction(SELECTION_PLAN_EXTRA_QUESTION_VALUE_DELETED)({ valueId }),
`${window.API_BASE_URL}/api/v1/summits/${currentSummit.id}/selection-plans/${selectionPlanId}/extra-questions/${questionId}/values/${valueId}`,
null,
- authErrorHandler
+ snackbarErrorHandler
)(params)(dispatch).then(() => {
dispatch(stopLoading());
});
@@ -611,7 +632,7 @@ export const addEventTypeSelectionPlan =
createAction(SELECTION_PLAN_EVENT_TYPE_ADDED)({ eventType }),
`${window.API_BASE_URL}/api/v1/summits/${currentSummit.id}/selection-plans/${selectionPlanId}/event-types/${eventType.id}`,
{},
- authErrorHandler
+ snackbarErrorHandler
)(params)(dispatch).then(() => {
dispatch(stopLoading());
});
@@ -633,7 +654,7 @@ export const deleteEventTypeSelectionPlan =
createAction(SELECTION_PLAN_EVENT_TYPE_REMOVED)({ eventTypeId }),
`${window.API_BASE_URL}/api/v1/summits/${currentSummit.id}/selection-plans/${selectionPlanId}/event-types/${eventTypeId}`,
null,
- authErrorHandler
+ snackbarErrorHandler
)(params)(dispatch).then(() => {
dispatch(stopLoading());
});
@@ -671,7 +692,7 @@ export const updateRatingTypeOrder =
createAction(SELECTION_PLAN_RATING_TYPE_ORDER_UPDATED)(ratingTypes),
`${window.API_BASE_URL}/api/v1/summits/${currentSummit.id}/selection-plans/${selectionPlanId}/track-chair-rating-types/${ratingTypeId}`,
ratingType,
- authErrorHandler
+ snackbarErrorHandler
)(params)(dispatch).then(() => {
dispatch(stopLoading());
});
@@ -692,7 +713,7 @@ export const deleteRatingType =
createAction(SELECTION_PLAN_RATING_TYPE_REMOVED)({ ratingTypeId }),
`${window.API_BASE_URL}/api/v1/summits/${currentSummit.id}/selection-plans/${selectionPlanId}/track-chair-rating-types/${ratingTypeId}`,
null,
- authErrorHandler
+ snackbarErrorHandler
)(params)(dispatch).then(() => {
dispatch(stopLoading());
});
@@ -724,18 +745,19 @@ export const assignExtraQuestion2SelectionPlan =
(summitId, selectionPlanId, questionId) => async (dispatch) => {
const accessToken = await getAccessTokenSafely();
dispatch(startLoading());
- postRequest(
+ return postRequest(
null,
createAction(SELECTION_PLAN_ASSIGNED_EXTRA_QUESTION),
`${window.API_BASE_URL}/api/v1/summits/${summitId}/selection-plans/${selectionPlanId}/extra-questions/${questionId}?access_token=${accessToken}`,
{},
- authErrorHandler
+ snackbarErrorHandler
)({})(dispatch).then(() => {
dispatch(stopLoading());
dispatch(
- showSuccessMessage(
- T.translate("edit_selection_plan.selection_plan_saved")
- )
+ snackbarSuccessHandler({
+ title: T.translate("general.done"),
+ html: T.translate("edit_selection_plan.selection_plan_saved")
+ })
);
});
};
@@ -763,7 +785,7 @@ export const getAllowedMembers =
createAction(REQUEST_ALLOWED_MEMBERS),
createAction(RECEIVE_ALLOWED_MEMBERS),
`${window.API_BASE_URL}/api/v1/summits/${currentSummit.id}/selection-plans/${selectionPlanId}/allowed-members`,
- authErrorHandler
+ snackbarErrorHandler
)(params)(dispatch).then(() => {
dispatch(stopLoading());
});
@@ -786,7 +808,7 @@ export const addAllowedMemberToSelectionPlan =
createAction(ALLOWED_MEMBER_ADDED),
`${window.API_BASE_URL}/api/v1/summits/${currentSummit.id}/selection-plans/${selectionPlanId}/allowed-members`,
{ email },
- authErrorHandler
+ snackbarErrorHandler
)(params)(dispatch).then(() => {
dispatch(stopLoading());
});
@@ -809,7 +831,7 @@ export const removeAllowedMemberFromSelectionPlan =
createAction(ALLOWED_MEMBER_REMOVED)({ emailId }),
`${window.API_BASE_URL}/api/v1/summits/${currentSummit.id}/selection-plans/${selectionPlanId}/allowed-members/${emailId}`,
null,
- authErrorHandler
+ snackbarErrorHandler
)(params)(dispatch).then(() => {
dispatch(stopLoading());
});
@@ -827,19 +849,22 @@ export const importAllowedMembersCSV =
dispatch(startLoading());
- postFile(
+ return postFile(
null,
createAction(ALLOWED_MEMBERS_IMPORTED),
`${window.API_BASE_URL}/api/v1/summits/${currentSummit.id}/selection-plans/${selectionPlanId}/allowed-members/csv`,
file,
{},
- authErrorHandler
+ snackbarErrorHandler
)(params)(dispatch).then(() => {
dispatch(stopLoading());
dispatch(
- showSuccessMessage(
- T.translate("edit_selection_plan.import_allowed_members_success")
- )
+ snackbarSuccessHandler({
+ title: T.translate("general.done"),
+ html: T.translate(
+ "edit_selection_plan.import_allowed_members_success"
+ )
+ })
);
});
};
@@ -863,7 +888,7 @@ export const getSelectionPlanProgressFlags =
null,
createAction(RECEIVE_SELECTION_PLAN_PROGRESS_FLAGS),
`${window.API_BASE_URL}/api/v1/summits/${summitId}/selection-plans/${selectionPlanId}/allowed-presentation-action-types`,
- authErrorHandler
+ snackbarErrorHandler
)(params)(dispatch).then(() => {
dispatch(stopLoading());
});
@@ -873,18 +898,19 @@ export const assignProgressFlag2SelectionPlan =
(summitId, selectionPlanId, progressFlagId) => async (dispatch) => {
const accessToken = await getAccessTokenSafely();
dispatch(startLoading());
- postRequest(
+ return postRequest(
null,
createAction(SELECTION_PLAN_ASSIGNED_PROGRESS_FLAG),
`${window.API_BASE_URL}/api/v1/summits/${summitId}/selection-plans/${selectionPlanId}/allowed-presentation-action-types/${progressFlagId}?access_token=${accessToken}`,
{},
- authErrorHandler
+ snackbarErrorHandler
)({})(dispatch).then(() => {
dispatch(stopLoading());
dispatch(
- showSuccessMessage(
- T.translate("edit_selection_plan.selection_plan_saved")
- )
+ snackbarSuccessHandler({
+ title: T.translate("general.done"),
+ html: T.translate("edit_selection_plan.selection_plan_saved")
+ })
);
});
};
@@ -911,7 +937,7 @@ export const updateProgressFlagOrder =
createAction(SELECTION_PLAN_PROGRESS_FLAG_ORDER_UPDATED)(progressFlags),
`${window.API_BASE_URL}/api/v1/summits/${currentSummit.id}/selection-plans/${selectionPlanId}/allowed-presentation-action-types/${progressFlagId}`,
progressFlag,
- authErrorHandler
+ snackbarErrorHandler
)(params)(dispatch).then(() => {
dispatch(stopLoading());
});
@@ -932,7 +958,7 @@ export const unassignProgressFlagFromSelectionPlan =
createAction(SELECTION_PLAN_PROGRESS_FLAG_REMOVED)({ progressFlagId }),
`${window.API_BASE_URL}/api/v1/summits/${currentSummit.id}/selection-plans/${selectionPlanId}/allowed-presentation-action-types/${progressFlagId}`,
null,
- authErrorHandler
+ snackbarErrorHandler
)(params)(dispatch).then(() => {
dispatch(stopLoading());
});
diff --git a/src/components/forms/__tests__/selection-plan-form.test.js b/src/components/forms/__tests__/selection-plan-form.test.js
new file mode 100644
index 000000000..35bce9f58
--- /dev/null
+++ b/src/components/forms/__tests__/selection-plan-form.test.js
@@ -0,0 +1,603 @@
+import React from "react";
+import { render, screen, within, waitFor } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import "@testing-library/jest-dom";
+import SelectionPlanForm from "../selection-plan-form";
+
+jest.mock("i18n-react/dist/i18n-react", () => ({
+ __esModule: true,
+ default: { translate: (key) => key }
+}));
+
+jest.mock(
+ "openstack-uicore-foundation/lib/components/inputs/editor-input-v3",
+ () => ({ __esModule: true, default: () => null })
+);
+
+jest.mock("openstack-uicore-foundation/lib/components/mui/table", () => ({
+ __esModule: true,
+ default: ({ data, onDelete }) => (
+
+ {(data || []).map((row) => (
+
+ {row.name || row.email || row.label}
+ onDelete && onDelete(row.id)}>
+ delete
+
+
+ ))}
+
+ )
+}));
+
+jest.mock(
+ "openstack-uicore-foundation/lib/components/mui/sortable-table",
+ () => ({
+ __esModule: true,
+ default: ({ data, onEdit, onDelete }) => (
+
+ {(data || []).map((row) => (
+
+ {row.name || row.label}
+ {onEdit && (
+ onEdit(row)}>
+ edit
+
+ )}
+ {onDelete && (
+ onDelete(row.id)}>
+ delete
+
+ )}
+
+ ))}
+
+ )
+ })
+);
+
+jest.mock("openstack-uicore-foundation/lib/utils/query-actions", () => ({
+ queryTrackGroups: jest.fn(),
+ queryEventTypes: jest.fn()
+}));
+
+jest.mock("../../mui/formik-inputs/mui-formik-datetimepicker", () => ({
+ __esModule: true,
+ default: ({ name }) =>
+}));
+
+jest.mock("../../inputs/email-template-input", () => ({
+ __esModule: true,
+ default: ({ id }) =>
+}));
+
+jest.mock("../../inputs/import-modal", () => ({
+ __esModule: true,
+ default: ({ show, onIngest }) =>
+ show ? (
+
+ onIngest(new File([""], "test.csv"))}
+ >
+ ingest
+
+
+ ) : null
+}));
+
+jest.mock("../../inputs/many-2-many-dropdown", () => ({
+ __esModule: true,
+ default: () => null
+}));
+
+jest.mock("../../../actions/selection-plan-actions", () => ({
+ querySelectionPlanExtraQuestions: jest.fn()
+}));
+
+jest.mock("../../../actions/track-chair-actions", () => ({
+ querySummitProgressFlags: jest.fn()
+}));
+
+jest.mock("../../../reducers/selection_plans/selection-plan-reducer", () => ({
+ DEFAULT_ALLOWED_EDITABLE_QUESTIONS: [],
+ DEFAULT_ALLOWED_QUESTIONS: [],
+ DEFAULT_CFP_PRESENTATION_EDITION_TABS: []
+}));
+
+// Base entity for a new plan (no tabs shown)
+const newEntity = {
+ id: 0,
+ name: "",
+ is_enabled: false,
+ is_hidden: false,
+ allow_proposed_schedules: false,
+ allow_new_presentations: false,
+ submission_begin_date: null,
+ submission_end_date: null,
+ submission_lock_down_presentation_status_date: null,
+ voting_begin_date: null,
+ voting_end_date: null,
+ selection_begin_date: null,
+ selection_end_date: null,
+ submission_period_disclaimer: "",
+ max_submission_allowed_per_user: 0,
+ presentation_creator_notification_email_template: "",
+ presentation_moderator_notification_email_template: "",
+ presentation_speaker_notification_email_template: "",
+ track_chair_rating_types: [],
+ allow_track_change_requests: true,
+ track_groups: [],
+ event_types: [],
+ extra_questions: [],
+ allowed_presentation_action_types: [],
+ allowed_presentation_questions: [],
+ allowed_presentation_editable_questions: [],
+ marketing_settings: {}
+};
+
+// Existing plan entity (tabs shown)
+const existingEntity = { ...newEntity, id: 42, name: "Spring CFP" };
+
+const baseProps = {
+ entity: newEntity,
+ errors: {},
+ currentSummit: { id: 1, time_zone_id: "UTC", slug: "test-summit" },
+ extraQuestionsOrder: "id",
+ extraQuestionsOrderDir: 1,
+ actionTypesOrder: "id",
+ actionTypesOrderDir: 1,
+ allowedMembers: { data: [], currentPage: 1, lastPage: 1 },
+ onSave: jest.fn(() => Promise.resolve()),
+ onTrackGroupLink: jest.fn(),
+ onTrackGroupUnLink: jest.fn(),
+ onAddEventType: jest.fn(),
+ onDeleteEventType: jest.fn(),
+ onAddRatingType: jest.fn(),
+ onEditRatingType: jest.fn(),
+ onDeleteRatingType: jest.fn(),
+ onEditExtraQuestion: jest.fn(),
+ onDeleteExtraQuestion: jest.fn(),
+ onAddNewExtraQuestion: jest.fn(),
+ onAssignExtraQuestion2SelectionPlan: jest.fn(),
+ onAssignProgressFlag2SelectionPlan: jest.fn(),
+ onUnassignProgressFlag: jest.fn(),
+ onUpdateProgressFlagOrder: jest.fn(),
+ onUpdateRatingTypeOrder: jest.fn(),
+ updateExtraQuestionOrder: jest.fn(),
+ onImportAllowedMembers: jest.fn(),
+ onAllowedMemberAdd: jest.fn(),
+ onAllowedMemberDelete: jest.fn(),
+ onAllowedMembersPageChange: jest.fn()
+};
+
+// Mirrors the popup - external submit button via form attribute
+const FormWithButton = (props) => (
+ <>
+ {/* eslint-disable-next-line react/jsx-props-no-spreading */}
+
+
+ general.save
+
+ >
+);
+
+const renderForm = (overrides = {}) => {
+ const merged = { ...baseProps, ...overrides };
+ // eslint-disable-next-line react/jsx-props-no-spreading
+ return render( );
+};
+
+const renderExistingForm = (overrides = {}) =>
+ renderForm({ entity: existingEntity, ...overrides });
+
+const clickTab = async (label) => {
+ await userEvent.click(screen.getByRole("tab", { name: label }));
+};
+
+beforeEach(() => jest.clearAllMocks());
+
+// ---------------------------------------------------------------------------
+// Save behaviour
+// ---------------------------------------------------------------------------
+
+describe("SelectionPlanForm - save behaviour", () => {
+ it("calls onSave when the form is submitted", async () => {
+ const onSave = jest.fn(() => Promise.resolve());
+ renderForm({ onSave });
+ await userEvent.click(screen.getByRole("button", { name: "general.save" }));
+ await waitFor(() => expect(onSave).toHaveBeenCalledTimes(1));
+ });
+
+ it("normalizes null dates to 0 before calling onSave", async () => {
+ const onSave = jest.fn(() => Promise.resolve());
+ renderForm({ onSave });
+ await userEvent.click(screen.getByRole("button", { name: "general.save" }));
+ await waitFor(() => expect(onSave).toHaveBeenCalled());
+ const [payload] = onSave.mock.calls[0];
+ expect(payload.submission_begin_date).toBe(0);
+ expect(payload.voting_begin_date).toBe(0);
+ });
+});
+
+// ---------------------------------------------------------------------------
+// Tab navigation
+// ---------------------------------------------------------------------------
+
+describe("SelectionPlanForm - tab navigation", () => {
+ it("hides tabs for a new plan (id=0)", () => {
+ renderForm();
+ expect(screen.queryByRole("tab")).toBeNull();
+ });
+
+ it("shows tab bar for an existing plan (id>0)", () => {
+ renderExistingForm();
+ expect(screen.getByRole("tab", { name: "Main" })).toBeInTheDocument();
+ expect(
+ screen.getByRole("tab", { name: "edit_selection_plan.track_groups" })
+ ).toBeInTheDocument();
+ });
+
+ it("hides the allowed_members tab when is_hidden is true", () => {
+ renderExistingForm({ entity: { ...existingEntity, is_hidden: true } });
+ expect(
+ screen.queryByRole("tab", { name: "edit_selection_plan.allowed_members" })
+ ).toBeNull();
+ });
+
+ it("shows the allowed_members tab when is_hidden is false", () => {
+ renderExistingForm({ entity: { ...existingEntity, is_hidden: false } });
+ expect(
+ screen.getByRole("tab", { name: "edit_selection_plan.allowed_members" })
+ ).toBeInTheDocument();
+ });
+});
+
+// ---------------------------------------------------------------------------
+// Main tab
+// ---------------------------------------------------------------------------
+
+describe("SelectionPlanForm - main tab", () => {
+ it("renders the name field", () => {
+ renderForm();
+ expect(screen.getByRole("textbox")).toBeInTheDocument();
+ });
+
+ it("renders enabled and hidden checkboxes", () => {
+ renderForm();
+ expect(
+ screen.getByLabelText("edit_selection_plan.enabled")
+ ).toBeInTheDocument();
+ expect(
+ screen.getByLabelText("edit_selection_plan.hidden")
+ ).toBeInTheDocument();
+ });
+
+ it("renders all date pickers", () => {
+ renderForm();
+ expect(screen.getByTestId("submission_begin_date")).toBeInTheDocument();
+ expect(screen.getByTestId("submission_end_date")).toBeInTheDocument();
+ expect(screen.getByTestId("voting_begin_date")).toBeInTheDocument();
+ expect(screen.getByTestId("selection_begin_date")).toBeInTheDocument();
+ });
+});
+
+// ---------------------------------------------------------------------------
+// Track groups tab
+// ---------------------------------------------------------------------------
+
+describe("SelectionPlanForm - track_groups tab", () => {
+ const goToTab = async () => {
+ renderExistingForm();
+ await clickTab("edit_selection_plan.track_groups");
+ };
+
+ it("shows empty state when no track groups", async () => {
+ await goToTab();
+ const panel = document.getElementById("tabpanel-track_groups");
+ expect(
+ within(panel).getByText("edit_selection_plan.no_track_groups")
+ ).toBeInTheDocument();
+ });
+
+ it("renders linked track groups in table", async () => {
+ renderExistingForm({
+ entity: {
+ ...existingEntity,
+ track_groups: [{ id: 1, name: "Group A", description: "" }]
+ }
+ });
+ await clickTab("edit_selection_plan.track_groups");
+ const panel = document.getElementById("tabpanel-track_groups");
+ expect(within(panel).getByText("Group A")).toBeInTheDocument();
+ });
+
+ it("calls onTrackGroupUnLink when delete is clicked", async () => {
+ const onTrackGroupUnLink = jest.fn();
+ renderExistingForm({
+ entity: {
+ ...existingEntity,
+ track_groups: [{ id: 7, name: "G", description: "" }]
+ },
+ onTrackGroupUnLink
+ });
+ await clickTab("edit_selection_plan.track_groups");
+ await userEvent.click(screen.getByRole("button", { name: "delete" }));
+ expect(onTrackGroupUnLink).toHaveBeenCalledWith(existingEntity.id, 7);
+ });
+});
+
+// ---------------------------------------------------------------------------
+// Event types tab
+// ---------------------------------------------------------------------------
+
+describe("SelectionPlanForm - event_types tab", () => {
+ const goToTab = async () => {
+ renderExistingForm();
+ await clickTab("edit_selection_plan.event_types");
+ };
+
+ it("shows empty state when no event types", async () => {
+ await goToTab();
+ const panel = document.getElementById("tabpanel-event_types");
+ expect(
+ within(panel).getByText("edit_selection_plan.no_event_types")
+ ).toBeInTheDocument();
+ });
+
+ it("renders linked event types in table", async () => {
+ renderExistingForm({
+ entity: {
+ ...existingEntity,
+ event_types: [{ id: 3, name: "Presentation" }]
+ }
+ });
+ await clickTab("edit_selection_plan.event_types");
+ const panel = document.getElementById("tabpanel-event_types");
+ expect(within(panel).getByText("Presentation")).toBeInTheDocument();
+ });
+
+ it("calls onDeleteEventType when delete is clicked", async () => {
+ const onDeleteEventType = jest.fn();
+ renderExistingForm({
+ entity: { ...existingEntity, event_types: [{ id: 5, name: "Talk" }] },
+ onDeleteEventType
+ });
+ await clickTab("edit_selection_plan.event_types");
+ await userEvent.click(screen.getByRole("button", { name: "delete" }));
+ expect(onDeleteEventType).toHaveBeenCalledWith(existingEntity.id, 5);
+ });
+});
+
+// ---------------------------------------------------------------------------
+// Extra questions tab
+// ---------------------------------------------------------------------------
+
+describe("SelectionPlanForm - extra_questions tab", () => {
+ const goToTab = async () => {
+ renderExistingForm();
+ await clickTab("edit_selection_plan.extra_questions");
+ };
+
+ it("shows empty state when no extra questions", async () => {
+ await goToTab();
+ const panel = document.getElementById("tabpanel-extra_questions");
+ expect(
+ within(panel).getByText("edit_selection_plan.no_extra_questions")
+ ).toBeInTheDocument();
+ });
+
+ it("calls onAddNewExtraQuestion when Add button is clicked", async () => {
+ const onAddNewExtraQuestion = jest.fn();
+ renderExistingForm({ onAddNewExtraQuestion });
+ await clickTab("edit_selection_plan.extra_questions");
+ await userEvent.click(
+ screen.getByRole("button", {
+ name: "edit_selection_plan.add_extra_questions"
+ })
+ );
+ expect(onAddNewExtraQuestion).toHaveBeenCalledTimes(1);
+ });
+
+ it("renders extra questions and calls onEditExtraQuestion on edit", async () => {
+ const onEditExtraQuestion = jest.fn();
+ renderExistingForm({
+ entity: {
+ ...existingEntity,
+ extra_questions: [{ id: 10, name: "q1", label: "Q One", type: "text" }]
+ },
+ onEditExtraQuestion
+ });
+ await clickTab("edit_selection_plan.extra_questions");
+ await userEvent.click(screen.getByRole("button", { name: "edit" }));
+ expect(onEditExtraQuestion).toHaveBeenCalledWith(10);
+ });
+});
+
+// ---------------------------------------------------------------------------
+// Email templates tab
+// ---------------------------------------------------------------------------
+
+describe("SelectionPlanForm - email_templates tab", () => {
+ it("renders the three email template inputs", async () => {
+ renderExistingForm();
+ await clickTab("edit_selection_plan.email_templates");
+ const panel = document.getElementById("tabpanel-email_templates");
+ expect(
+ within(panel).getByTestId(
+ "presentation_creator_notification_email_template"
+ )
+ ).toBeInTheDocument();
+ expect(
+ within(panel).getByTestId(
+ "presentation_moderator_notification_email_template"
+ )
+ ).toBeInTheDocument();
+ expect(
+ within(panel).getByTestId(
+ "presentation_speaker_notification_email_template"
+ )
+ ).toBeInTheDocument();
+ });
+});
+
+// ---------------------------------------------------------------------------
+// Track chair settings tab
+// ---------------------------------------------------------------------------
+
+describe("SelectionPlanForm - track_chair_settings tab", () => {
+ const goToTab = async () => {
+ renderExistingForm();
+ await clickTab("track_chair_settings.title");
+ };
+
+ it("renders the allow track change requests checkbox", async () => {
+ await goToTab();
+ expect(
+ screen.getByLabelText("track_chair_settings.allow_change_requests")
+ ).toBeInTheDocument();
+ });
+
+ it("calls onAddRatingType when Add Rating Type is clicked", async () => {
+ const onAddRatingType = jest.fn();
+ renderExistingForm({ onAddRatingType });
+ await clickTab("track_chair_settings.title");
+ await userEvent.click(
+ screen.getByRole("button", {
+ name: "track_chair_settings.add_rating_type"
+ })
+ );
+ expect(onAddRatingType).toHaveBeenCalledTimes(1);
+ });
+
+ it("renders rating types and calls onEditRatingType on edit", async () => {
+ const onEditRatingType = jest.fn();
+ renderExistingForm({
+ entity: {
+ ...existingEntity,
+ track_chair_rating_types: [{ id: 20, name: "Excellent", weight: 10 }]
+ },
+ onEditRatingType
+ });
+ await clickTab("track_chair_settings.title");
+ await userEvent.click(screen.getByRole("button", { name: "edit" }));
+ expect(onEditRatingType).toHaveBeenCalledWith(20);
+ });
+});
+
+// ---------------------------------------------------------------------------
+// Presentation action types tab
+// ---------------------------------------------------------------------------
+
+describe("SelectionPlanForm - presentation_action_types tab", () => {
+ const goToTab = async () => {
+ renderExistingForm();
+ await clickTab("edit_selection_plan.presentation_action_types");
+ };
+
+ it("shows empty state when no action types", async () => {
+ await goToTab();
+ const panel = document.getElementById("tabpanel-presentation_action_types");
+ expect(
+ within(panel).getByText(
+ "edit_selection_plan.no_presentation_action_types"
+ )
+ ).toBeInTheDocument();
+ });
+
+ it("renders action types and calls onUnassignProgressFlag on delete", async () => {
+ const onUnassignProgressFlag = jest.fn();
+ renderExistingForm({
+ entity: {
+ ...existingEntity,
+ allowed_presentation_action_types: [{ id: 30, label: "Approve" }]
+ },
+ onUnassignProgressFlag
+ });
+ await clickTab("edit_selection_plan.presentation_action_types");
+ await userEvent.click(screen.getByRole("button", { name: "delete" }));
+ expect(onUnassignProgressFlag).toHaveBeenCalledWith(30);
+ });
+});
+
+// ---------------------------------------------------------------------------
+// Allowed members tab
+// ---------------------------------------------------------------------------
+
+describe("SelectionPlanForm - allowed_members tab", () => {
+ const membersProps = {
+ entity: { ...existingEntity, is_hidden: false },
+ allowedMembers: {
+ data: [{ id: 1, email: "user@example.com" }],
+ currentPage: 1,
+ lastPage: 2
+ }
+ };
+
+ it("renders members and calls onAllowedMemberDelete on delete", async () => {
+ const onAllowedMemberDelete = jest.fn();
+ renderExistingForm({ ...membersProps, onAllowedMemberDelete });
+ await clickTab("edit_selection_plan.allowed_members");
+ await userEvent.click(screen.getByRole("button", { name: "delete" }));
+ expect(onAllowedMemberDelete).toHaveBeenCalledWith(existingEntity.id, 1);
+ });
+
+ it("calls onAllowedMemberAdd when Add is clicked with an email", async () => {
+ const onAllowedMemberAdd = jest.fn();
+ renderExistingForm({ ...membersProps, onAllowedMemberAdd });
+ await clickTab("edit_selection_plan.allowed_members");
+ const panel = document.getElementById("tabpanel-allowed_members");
+ const emailInput = within(panel).getByRole("textbox");
+ await userEvent.type(emailInput, "new@test.com");
+ await userEvent.click(
+ within(panel).getByRole("button", { name: "general.add" })
+ );
+ expect(onAllowedMemberAdd).toHaveBeenCalledWith(
+ existingEntity.id,
+ "new@test.com"
+ );
+ });
+
+ it("calls onImportAllowedMembers when import modal is confirmed", async () => {
+ const onImportAllowedMembers = jest.fn();
+ renderExistingForm({ ...membersProps, onImportAllowedMembers });
+ await clickTab("edit_selection_plan.allowed_members");
+ const panel = document.getElementById("tabpanel-allowed_members");
+ await userEvent.click(
+ within(panel).getByRole("button", { name: "edit_selection_plan.import" })
+ );
+ await userEvent.click(screen.getByRole("button", { name: "ingest" }));
+ expect(onImportAllowedMembers).toHaveBeenCalledWith(
+ existingEntity.id,
+ expect.any(File)
+ );
+ });
+});
+
+// ---------------------------------------------------------------------------
+// CFP settings tab
+// ---------------------------------------------------------------------------
+
+describe("SelectionPlanForm - cfp_settings tab", () => {
+ it("renders the allowed_presentation_questions autocomplete", async () => {
+ renderExistingForm();
+ await clickTab("edit_selection_plan.cfp_settings");
+ const panel = document.getElementById("tabpanel-cfp_settings");
+ expect(
+ within(panel).getByPlaceholderText(
+ "edit_selection_plan.placeholders.allowed_presentation_questions"
+ )
+ ).toBeInTheDocument();
+ });
+
+ it("renders the allowed_presentation_editable_questions autocomplete", async () => {
+ renderExistingForm();
+ await clickTab("edit_selection_plan.cfp_settings");
+ const panel = document.getElementById("tabpanel-cfp_settings");
+ expect(
+ within(panel).getByPlaceholderText(
+ "edit_selection_plan.placeholders.allowed_presentation_editable_questions"
+ )
+ ).toBeInTheDocument();
+ });
+});
diff --git a/src/components/forms/selection-plan-form.js b/src/components/forms/selection-plan-form.js
index eae94c098..416062d9e 100644
--- a/src/components/forms/selection-plan-form.js
+++ b/src/components/forms/selection-plan-form.js
@@ -11,1466 +11,340 @@
* limitations under the License.
* */
-import React from "react";
+import React, { useState, useEffect } from "react";
+import PropTypes from "prop-types";
import T from "i18n-react/dist/i18n-react";
-import "awesome-bootstrap-checkbox/awesome-bootstrap-checkbox.css";
+import { useFormik, FormikProvider } from "formik";
+import moment from "moment-timezone";
import { epochToMomentTimeZone } from "openstack-uicore-foundation/lib/utils/methods";
-import {
- queryTrackGroups,
- queryEventTypes,
- queryMembers
-} from "openstack-uicore-foundation/lib/utils/query-actions";
-import {
- Input,
- DateTimePicker,
- SimpleLinkList,
- SortableTable,
- Panel,
- Table,
- Dropdown
-} from "openstack-uicore-foundation/lib/components";
-import TextEditorV3 from "openstack-uicore-foundation/lib/components/inputs/editor-input-v3";
-import Switch from "react-switch";
-import { Pagination } from "react-bootstrap";
-import {
- isEmpty,
- scrollToError,
- shallowEqual,
- stripTags
-} from "../../utils/methods";
-import EmailTemplateInput from "../inputs/email-template-input";
-import ImportModal from "../inputs/import-modal";
-import {
- MILLISECONDS_TO_SECONDS,
- PresentationTypeClassName
-} from "../../utils/constants";
-import Many2ManyDropDown from "../inputs/many-2-many-dropdown";
-import { querySelectionPlanExtraQuestions } from "../../actions/selection-plan-actions";
-import { querySummitProgressFlags } from "../../actions/track-chair-actions";
-import {
- DEFAULT_ALLOWED_EDITABLE_QUESTIONS,
- DEFAULT_ALLOWED_QUESTIONS,
- DEFAULT_CFP_PRESENTATION_EDITION_TABS
-} from "../../reducers/selection_plans/selection-plan-reducer";
-import history from "../../history";
-
-class SelectionPlanForm extends React.Component {
- constructor(props) {
- super(props);
-
- this.state = {
- entity: { ...props.entity },
- errors: props.errors,
- showSection: "main",
- newMemberEmail: "",
- showImportModal: false,
- importFile: null
- };
-
- this.handleTrackGroupLink = this.handleTrackGroupLink.bind(this);
- this.handleTrackGroupUnLink = this.handleTrackGroupUnLink.bind(this);
- this.handleChange = this.handleChange.bind(this);
- this.handleSubmit = this.handleSubmit.bind(this);
- this.handleEditExtraQuestion = this.handleEditExtraQuestion.bind(this);
- this.handleDeleteExtraQuestion = this.handleDeleteExtraQuestion.bind(this);
- this.handleNewExtraQuestion = this.handleNewExtraQuestion.bind(this);
- this.handleDeleteEventType = this.handleDeleteEventType.bind(this);
- this.handleAddEventType = this.handleAddEventType.bind(this);
- this.handleAddRatingType = this.handleAddRatingType.bind(this);
- this.handleDeleteRatingType = this.handleDeleteRatingType.bind(this);
- this.handleEditRatingType = this.handleEditRatingType.bind(this);
- this.handleRemoveProgressFlag = this.handleRemoveProgressFlag.bind(this);
- this.toggleSection = this.toggleSection.bind(this);
- this.handleNotificationEmailTemplateChange =
- this.handleNotificationEmailTemplateChange.bind(this);
- this.fetchSummitSelectionPlanExtraQuestions =
- this.fetchSummitSelectionPlanExtraQuestions.bind(this);
- this.fetchMembers = this.fetchMembers.bind(this);
- this.linkSummitSelectionPlanExtraQuestion =
- this.linkSummitSelectionPlanExtraQuestion.bind(this);
- this.fetchSummitPresentationActionTypes =
- this.fetchSummitPresentationActionTypes.bind(this);
- this.linkSummitProgressFlag = this.linkSummitProgressFlag.bind(this);
- this.handleAddAllowedMember = this.handleAddAllowedMember.bind(this);
- this.handleImportAllowedMembers =
- this.handleImportAllowedMembers.bind(this);
- this.handleDeleteAllowedMember = this.handleDeleteAllowedMember.bind(this);
- this.handleAllowedMembersPageChange =
- this.handleAllowedMembersPageChange.bind(this);
- this.handleOnSwitchChange = this.handleOnSwitchChange.bind(this);
- }
-
- fetchSummitSelectionPlanExtraQuestions(input, callback) {
- const { currentSummit } = this.props;
-
- if (!input) {
- return Promise.resolve({ options: [] });
- }
- querySelectionPlanExtraQuestions(currentSummit.id, input, callback);
- }
-
- fetchMembers(input, callback) {
- if (!input) {
- return Promise.resolve({ options: [] });
- }
- queryMembers(input, callback);
- }
-
- linkSummitSelectionPlanExtraQuestion(question) {
- const { currentSummit } = this.props;
- this.props.onAssignExtraQuestion2SelectionPlan(
- currentSummit.id,
- this.state.entity.id,
- question.id
- );
- }
-
- handleEditExtraQuestion(questionId) {
- this.props.onEditExtraQuestion(questionId);
- }
-
- handleDeleteExtraQuestion(questionId) {
- this.props.onDeleteExtraQuestion(questionId);
- }
-
- handleNewExtraQuestion() {
- this.props.onAddNewExtraQuestion();
- }
-
- componentDidUpdate(prevProps) {
- const state = {};
- scrollToError(this.props.errors);
-
- if (!shallowEqual(prevProps.entity, this.props.entity)) {
- state.entity = { ...this.props.entity };
- state.errors = {};
- }
-
- if (!shallowEqual(prevProps.errors, this.props.errors)) {
- state.errors = { ...this.props.errors };
- }
-
- if (!isEmpty(state)) {
- this.setState({ ...this.state, ...state });
- }
- }
-
- handleChange(ev) {
- const newEntity = { ...this.state.entity };
- const newErrors = { ...this.state.errors };
- let { value, id } = ev.target;
-
- if (ev.target.type === "checkbox") {
- value = ev.target.checked;
- }
-
- if (ev.target.type === "datetime") {
- value = value.valueOf() / MILLISECONDS_TO_SECONDS;
- }
-
- if (id.startsWith("cfp_")) {
- if (!newEntity.marketing_settings.hasOwnProperty(id)) {
- newEntity.marketing_settings[id] = { value: "" };
- }
- newEntity.marketing_settings[id].value = value;
- } else {
- newErrors[id] = "";
- newEntity[id] = value;
- }
-
- this.setState({ entity: newEntity, errors: newErrors });
- }
-
- handleNotificationEmailTemplateChange(ev) {
- const newEntity = { ...this.state.entity };
- const newErrors = { ...this.state.errors };
- const { value, id } = ev.target;
-
- newErrors[id] = "";
- newEntity[id] = value;
- this.setState({ ...this.state, entity: newEntity, errors: newErrors });
- }
-
- handleSubmit(ev) {
- ev.preventDefault();
-
- const entity = { ...this.state.entity };
- const { currentSummit } = this.props;
-
- this.props.onSubmit(this.state.entity).then((e) => {
- this.props
- .saveSelectionPlanSettings(entity.marketing_settings, e.id)
- .then(() => {
- if (!entity.id)
- history.push(
- `/app/summits/${currentSummit.id}/selection-plans/${e.id}`
- );
- });
+import Box from "@mui/material/Box";
+import Tab from "@mui/material/Tab";
+import Tabs from "@mui/material/Tabs";
+import { scrollToError } from "../../utils/methods";
+import MainTab from "./selection-plan-form/main-tab";
+import TrackGroupsTab from "./selection-plan-form/track-groups-tab";
+import EventTypesTab from "./selection-plan-form/event-types-tab";
+import ExtraQuestionsTab from "./selection-plan-form/extra-questions-tab";
+import EmailTemplatesTab from "./selection-plan-form/email-templates-tab";
+import TrackChairSettingsTab from "./selection-plan-form/track-chair-settings-tab";
+import PresentationActionTypesTab from "./selection-plan-form/presentation-action-types-tab";
+import AllowedMembersTab from "./selection-plan-form/allowed-members-tab";
+import CfpSettingsTab from "./selection-plan-form/cfp-settings-tab";
+
+const DATE_FIELDS = [
+ "submission_begin_date",
+ "submission_end_date",
+ "submission_lock_down_presentation_status_date",
+ "voting_begin_date",
+ "voting_end_date",
+ "selection_begin_date",
+ "selection_end_date"
+];
+
+const buildInitialValues = (entity, timezone) => {
+ const values = { ...entity };
+ DATE_FIELDS.forEach((field) => {
+ values[field] = entity[field]
+ ? epochToMomentTimeZone(entity[field], timezone)
+ : null;
+ });
+ return values;
+};
+
+const TAB_SX = {
+ fontSize: "1.4rem",
+ lineHeight: "1.8rem",
+ minHeight: "36px",
+ px: 2,
+ py: 1
+};
+
+const SelectionPlanForm = (props) => {
+ const {
+ entity: propsEntity,
+ errors: propsErrors,
+ currentSummit,
+ extraQuestionsOrderDir,
+ extraQuestionsOrder,
+ actionTypesOrderDir,
+ actionTypesOrder,
+ allowedMembers,
+ onSave,
+ onTrackGroupLink,
+ onTrackGroupUnLink,
+ onAddEventType,
+ onDeleteEventType,
+ onAddRatingType,
+ onEditRatingType,
+ onDeleteRatingType,
+ onEditExtraQuestion,
+ onDeleteExtraQuestion,
+ onAddNewExtraQuestion,
+ onAssignExtraQuestion2SelectionPlan,
+ onAssignProgressFlag2SelectionPlan,
+ onUnassignProgressFlag,
+ onUpdateProgressFlagOrder,
+ onUpdateRatingTypeOrder,
+ updateExtraQuestionOrder,
+ onImportAllowedMembers,
+ onAllowedMemberAdd,
+ onAllowedMemberDelete,
+ onAllowedMembersPageChange
+ } = props;
+
+ const [activeTab, setActiveTab] = useState("main");
+
+ const handleFormikSubmit = (values) => {
+ const normalized = { ...values };
+ DATE_FIELDS.forEach((field) => {
+ normalized[field] = values[field]
+ ? moment.tz(values[field], currentSummit.time_zone_id).unix()
+ : 0;
});
- }
-
- hasErrors(field) {
- const { errors } = this.state;
- if (field in errors) {
- return errors[field];
- }
-
- return "";
- }
-
- handleTrackGroupLink(value) {
- const { entity } = this.state;
- this.props.onTrackGroupLink(entity.id, value);
- }
-
- handleTrackGroupUnLink(valueId) {
- const { entity } = this.state;
- this.props.onTrackGroupUnLink(entity.id, valueId);
- }
-
- handleAddEventType(value) {
- const { entity } = this.state;
- this.props.onAddEventType(entity.id, value);
- }
-
- handleDeleteEventType(valueId) {
- const { entity } = this.state;
- this.props.onDeleteEventType(entity.id, valueId);
- }
-
- handleAddRatingType() {
- this.props.onAddRatingType();
- }
-
- handleEditRatingType(ratingTypeId) {
- this.props.onEditRatingType(ratingTypeId);
- }
-
- handleDeleteRatingType(ratingTypeId) {
- this.props.onDeleteRatingType(ratingTypeId);
- }
-
- fetchSummitPresentationActionTypes(input, callback) {
- const { currentSummit } = this.props;
-
- if (!input) {
- return Promise.resolve({ options: [] });
- }
- querySummitProgressFlags(currentSummit.id, input, callback);
- }
-
- linkSummitProgressFlag(progressFlag) {
- const { currentSummit } = this.props;
- this.props.onAssignProgressFlag2SelectionPlan(
- currentSummit.id,
- this.state.entity.id,
- progressFlag.id
+ return onSave(normalized);
+ };
+
+ const formik = useFormik({
+ initialValues: buildInitialValues(propsEntity, currentSummit.time_zone_id),
+ onSubmit: handleFormikSubmit,
+ validateOnChange: false
+ });
+
+ useEffect(() => {
+ scrollToError(propsErrors);
+ formik.setErrors(
+ propsErrors && Object.keys(propsErrors).length > 0 ? propsErrors : {}
);
- }
-
- handleRemoveProgressFlag(progressFlagId) {
- this.props.onUnassignProgressFlag(progressFlagId);
- }
-
- handleImportAllowedMembers(importFile) {
- if (importFile) {
- this.props.onImportAllowedMembers(this.state.entity.id, importFile);
+ }, [propsErrors]);
+
+ // Sync sub-resource arrays from Redux without resetting user-editable main tab fields
+ useEffect(() => {
+ formik.setValues((current) => ({
+ ...current,
+ track_groups: propsEntity.track_groups ?? [],
+ event_types: propsEntity.event_types ?? [],
+ extra_questions: propsEntity.extra_questions ?? [],
+ track_chair_rating_types: propsEntity.track_chair_rating_types ?? [],
+ allowed_presentation_action_types:
+ propsEntity.allowed_presentation_action_types ?? []
+ }));
+ }, [
+ propsEntity.track_groups,
+ propsEntity.event_types,
+ propsEntity.extra_questions,
+ propsEntity.track_chair_rating_types,
+ propsEntity.allowed_presentation_action_types
+ ]);
+
+ // Reset tab if allowed_members becomes unavailable
+ useEffect(() => {
+ if (formik.values.is_hidden && activeTab === "allowed_members") {
+ setActiveTab("main");
}
- this.setState({ ...this.state, showImportModal: false });
- }
-
- handleAddAllowedMember() {
- const { entity, newMemberEmail } = this.state;
- this.props.onAllowedMemberAdd(entity.id, newMemberEmail);
- }
-
- handleDeleteAllowedMember(valueId) {
- const { entity } = this.state;
- this.props.onAllowedMemberDelete(entity.id, valueId);
- }
-
- handleAllowedMembersPageChange(page) {
- const { entity } = this.state;
- this.props.onAllowedMembersPageChange(entity.id, page);
- }
-
- toggleSection(section) {
- const { showSection } = this.state;
- const newShowSection = showSection === section ? "main" : section;
- this.setState({ showSection: newShowSection });
- }
-
- handleOnSwitchChange(setting, value) {
- const newEntity = { ...this.state.entity };
- const newErrors = { ...this.state.errors };
-
- if (!newEntity.marketing_settings.hasOwnProperty(setting)) {
- newEntity.marketing_settings[setting] = { value: "" };
+ }, [formik.values.is_hidden]);
+
+ const isNewPlan = formik.values.id === 0;
+
+ const tabs = [
+ { value: "main", label: "Main" },
+ {
+ value: "track_groups",
+ label: T.translate("edit_selection_plan.track_groups")
+ },
+ {
+ value: "event_types",
+ label: T.translate("edit_selection_plan.event_types")
+ },
+ {
+ value: "extra_questions",
+ label: T.translate("edit_selection_plan.extra_questions")
+ },
+ {
+ value: "email_templates",
+ label: T.translate("edit_selection_plan.email_templates")
+ },
+ {
+ value: "track_chair_settings",
+ label: T.translate("track_chair_settings.title")
+ },
+ {
+ value: "presentation_action_types",
+ label: T.translate("edit_selection_plan.presentation_action_types")
+ },
+ ...(!formik.values.is_hidden
+ ? [
+ {
+ value: "allowed_members",
+ label: T.translate("edit_selection_plan.allowed_members")
+ }
+ ]
+ : []),
+ {
+ value: "cfp_settings",
+ label: T.translate("edit_selection_plan.cfp_settings")
}
+ ];
+
+ return (
+
+
+
+
+ {!isNewPlan && (
+
+ setActiveTab(val)}
+ variant="scrollable"
+ scrollButtons="auto"
+ sx={{
+ "& .MuiTabScrollButton-root.Mui-disabled": { display: "none" }
+ }}
+ >
+ {tabs.map((tab) => (
+
+ ))}
+
+
+ )}
- newEntity.marketing_settings[setting].value = value;
-
- this.setState({ entity: newEntity, errors: newErrors });
- }
-
- render() {
- const { entity, showSection, newMemberEmail, showImportModal } = this.state;
- const {
- currentSummit,
- extraQuestionsOrderDir,
- extraQuestionsOrder,
- actionTypesOrderDir,
- actionTypesOrder,
- allowedMembers
- } = this.props;
-
- const trackGroupsColumns = [
- { columnKey: "name", value: T.translate("edit_selection_plan.name") },
- {
- columnKey: "description",
- value: T.translate("edit_selection_plan.description")
- }
- ];
-
- const trackGroupsOptions = {
- valueKey: "name",
- labelKey: "name",
- defaultOptions: true,
- actions: {
- search: (input, callback) => {
- queryTrackGroups(currentSummit.id, input, callback);
- },
- delete: { onClick: this.handleTrackGroupUnLink },
- add: { onClick: this.handleTrackGroupLink }
- }
- };
-
- const eventTypesColumns = [
- { columnKey: "name", value: T.translate("edit_selection_plan.name") }
- ];
-
- const eventTypesOptions = {
- valueKey: "name",
- labelKey: "name",
- defaultOptions: true,
- actions: {
- search: (input, callback) => {
- queryEventTypes(
- currentSummit.id,
- input,
- callback,
- PresentationTypeClassName
- );
- },
- delete: { onClick: this.handleDeleteEventType },
- add: { onClick: this.handleAddEventType }
- }
- };
-
- const extraQuestionColumns = [
- {
- columnKey: "type",
- value: T.translate("order_extra_question_list.question_type")
- },
- {
- columnKey: "label",
- value: T.translate("order_extra_question_list.visible_question")
- },
- {
- columnKey: "name",
- value: T.translate("order_extra_question_list.question_id")
- }
- ];
-
- const extraQuestionsOptions = {
- sortCol: extraQuestionsOrder,
- sortDir: extraQuestionsOrderDir,
- actions: {
- edit: { onClick: this.handleEditExtraQuestion },
- delete: { onClick: this.handleDeleteExtraQuestion }
- }
- };
-
- const ratingTypesColumns = [
- { columnKey: "name", value: T.translate("rating_type_list.name") },
- { columnKey: "weight", value: T.translate("rating_type_list.weight") }
- ];
-
- const ratingTypesOptions = {
- actions: {
- edit: { onClick: this.handleEditRatingType },
- delete: { onClick: this.handleDeleteRatingType }
- }
- };
-
- const actionTypesColumns = [
- { columnKey: "label", value: T.translate("progress_flags.label") }
- ];
-
- const actionTypesOptions = {
- sortCol: actionTypesOrder,
- sortDir: actionTypesOrderDir,
- actions: {
- delete: { onClick: this.handleRemoveProgressFlag }
- }
- };
-
- const allowedMembersColumns = [
- { columnKey: "id", value: T.translate("edit_selection_plan.id") },
- { columnKey: "email", value: T.translate("edit_selection_plan.email") }
- ];
-
- const allowedMembersOptions = {
- sortCol: "email",
- sortDir: 1,
- actions: {
- delete: { onClick: this.handleDeleteAllowedMember }
- }
- };
-
- console.log("CHECK...", entity, currentSummit);
-
- return (
-
- );
- }
-}
+
+
+ );
+};
+
+SelectionPlanForm.propTypes = {
+ entity: PropTypes.shape({
+ id: PropTypes.number,
+ track_groups: PropTypes.arrayOf(PropTypes.shape({})),
+ event_types: PropTypes.arrayOf(PropTypes.shape({})),
+ extra_questions: PropTypes.arrayOf(PropTypes.shape({})),
+ track_chair_rating_types: PropTypes.arrayOf(PropTypes.shape({})),
+ allowed_presentation_action_types: PropTypes.arrayOf(PropTypes.shape({}))
+ }).isRequired,
+ errors: PropTypes.shape({}),
+ currentSummit: PropTypes.shape({
+ id: PropTypes.number.isRequired,
+ time_zone_id: PropTypes.string.isRequired,
+ slug: PropTypes.string
+ }).isRequired,
+ extraQuestionsOrder: PropTypes.string.isRequired,
+ extraQuestionsOrderDir: PropTypes.number.isRequired,
+ actionTypesOrder: PropTypes.string.isRequired,
+ actionTypesOrderDir: PropTypes.number.isRequired,
+ allowedMembers: PropTypes.shape({
+ data: PropTypes.arrayOf(
+ PropTypes.shape({ id: PropTypes.number, email: PropTypes.string })
+ ).isRequired,
+ currentPage: PropTypes.number.isRequired,
+ lastPage: PropTypes.number.isRequired
+ }).isRequired,
+ onSave: PropTypes.func.isRequired,
+ onTrackGroupLink: PropTypes.func.isRequired,
+ onTrackGroupUnLink: PropTypes.func.isRequired,
+ onAddEventType: PropTypes.func.isRequired,
+ onDeleteEventType: PropTypes.func.isRequired,
+ onAddRatingType: PropTypes.func.isRequired,
+ onEditRatingType: PropTypes.func.isRequired,
+ onDeleteRatingType: PropTypes.func.isRequired,
+ onEditExtraQuestion: PropTypes.func.isRequired,
+ onDeleteExtraQuestion: PropTypes.func.isRequired,
+ onAddNewExtraQuestion: PropTypes.func.isRequired,
+ onAssignExtraQuestion2SelectionPlan: PropTypes.func.isRequired,
+ onAssignProgressFlag2SelectionPlan: PropTypes.func.isRequired,
+ onUnassignProgressFlag: PropTypes.func.isRequired,
+ onUpdateProgressFlagOrder: PropTypes.func.isRequired,
+ onUpdateRatingTypeOrder: PropTypes.func.isRequired,
+ updateExtraQuestionOrder: PropTypes.func.isRequired,
+ onImportAllowedMembers: PropTypes.func.isRequired,
+ onAllowedMemberAdd: PropTypes.func.isRequired,
+ onAllowedMemberDelete: PropTypes.func.isRequired,
+ onAllowedMembersPageChange: PropTypes.func.isRequired
+};
+
+SelectionPlanForm.defaultProps = {
+ errors: {}
+};
export default SelectionPlanForm;
diff --git a/src/components/forms/selection-plan-form/allowed-members-tab.js b/src/components/forms/selection-plan-form/allowed-members-tab.js
new file mode 100644
index 000000000..64ffe5980
--- /dev/null
+++ b/src/components/forms/selection-plan-form/allowed-members-tab.js
@@ -0,0 +1,133 @@
+/**
+ * Copyright 2017 OpenStack Foundation
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * */
+
+import React, { useState } from "react";
+import PropTypes from "prop-types";
+import T from "i18n-react/dist/i18n-react";
+import { useFormikContext } from "formik";
+import Table from "openstack-uicore-foundation/lib/components/mui/table";
+import Box from "@mui/material/Box";
+import Button from "@mui/material/Button";
+import Pagination from "@mui/material/Pagination";
+import TextField from "@mui/material/TextField";
+import ImportModal from "../../inputs/import-modal";
+
+const allowedMembersColumns = [
+ { columnKey: "id", header: T.translate("edit_selection_plan.id") },
+ { columnKey: "email", header: T.translate("edit_selection_plan.email") }
+];
+
+const AllowedMembersTab = ({
+ hidden,
+ allowedMembers,
+ onImportAllowedMembers,
+ onAllowedMemberAdd,
+ onAllowedMemberDelete,
+ onAllowedMembersPageChange
+}) => {
+ const { values } = useFormikContext();
+ const [newMemberEmail, setNewMemberEmail] = useState("");
+ const [showImportModal, setShowImportModal] = useState(false);
+
+ const handleImport = (importFile) => {
+ if (importFile) onImportAllowedMembers(values.id, importFile);
+ setShowImportModal(false);
+ };
+
+ return (
+
+
+
+ setShowImportModal(true)}
+ >
+ {T.translate("edit_selection_plan.import")}
+
+
+ setNewMemberEmail(ev.target.value)}
+ />
+ onAllowedMemberAdd(values.id, newMemberEmail)}
+ disabled={!newMemberEmail}
+ >
+ {T.translate("general.add")}
+
+
+
+ onAllowedMemberDelete(values.id, id)}
+ confirmButtonColor="error"
+ getName={(item) => item.email}
+ deleteDialogBody={(email) =>
+ `${T.translate(
+ "edit_selection_plan.delete_confirm.allowed_member"
+ )} ${email}`
+ }
+ />
+
+ onAllowedMembersPageChange(values.id, page)}
+ showFirstButton
+ showLastButton
+ />
+
+
+ setShowImportModal(false)}
+ onIngest={handleImport}
+ >
+ * email ( text )
+
+
+
+ );
+};
+
+AllowedMembersTab.propTypes = {
+ hidden: PropTypes.bool.isRequired,
+ allowedMembers: PropTypes.shape({
+ data: PropTypes.arrayOf(
+ PropTypes.shape({ id: PropTypes.number, email: PropTypes.string })
+ ).isRequired,
+ currentPage: PropTypes.number.isRequired,
+ lastPage: PropTypes.number.isRequired
+ }).isRequired,
+ onImportAllowedMembers: PropTypes.func.isRequired,
+ onAllowedMemberAdd: PropTypes.func.isRequired,
+ onAllowedMemberDelete: PropTypes.func.isRequired,
+ onAllowedMembersPageChange: PropTypes.func.isRequired
+};
+
+export default AllowedMembersTab;
diff --git a/src/components/forms/selection-plan-form/cfp-settings-tab.js b/src/components/forms/selection-plan-form/cfp-settings-tab.js
new file mode 100644
index 000000000..d8a5e6675
--- /dev/null
+++ b/src/components/forms/selection-plan-form/cfp-settings-tab.js
@@ -0,0 +1,348 @@
+/**
+ * Copyright 2017 OpenStack Foundation
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * */
+
+import React from "react";
+import PropTypes from "prop-types";
+import T from "i18n-react/dist/i18n-react";
+import { useFormikContext } from "formik";
+import TextEditorV3 from "openstack-uicore-foundation/lib/components/inputs/editor-input-v3";
+import Autocomplete from "@mui/material/Autocomplete";
+import Box from "@mui/material/Box";
+import Chip from "@mui/material/Chip";
+import Grid2 from "@mui/material/Grid2";
+import MuiSwitch from "@mui/material/Switch";
+import TextField from "@mui/material/TextField";
+import {
+ DEFAULT_ALLOWED_EDITABLE_QUESTIONS,
+ DEFAULT_ALLOWED_QUESTIONS,
+ DEFAULT_CFP_PRESENTATION_EDITION_TABS
+} from "../../../reducers/selection_plans/selection-plan-reducer";
+
+function renderAllowedQuestionsInput(params) {
+ const placeholder = T.translate(
+ "edit_selection_plan.placeholders.allowed_presentation_questions"
+ );
+ // eslint-disable-next-line react/jsx-props-no-spreading
+ return ;
+}
+
+function renderAllowedEditableQuestionsInput(params) {
+ const placeholder = T.translate(
+ "edit_selection_plan.placeholders.allowed_presentation_editable_questions"
+ );
+ // eslint-disable-next-line react/jsx-props-no-spreading
+ return ;
+}
+
+function renderDefaultTabInput(params) {
+ const placeholder = T.translate(
+ "edit_selection_plan.placeholders.cfp_presentation_edition_default_tab"
+ );
+ // eslint-disable-next-line react/jsx-props-no-spreading
+ return ;
+}
+
+const CfpSettingsTab = ({ hidden, currentSummit }) => {
+ const { values, errors, setFieldValue } = useFormikContext();
+
+ const hasErrors = (field) => errors[field] ?? "";
+
+ const handleChange = (ev) => {
+ const { id, value } = ev.target;
+ if (id.startsWith("cfp_")) {
+ const current = values.marketing_settings[id] || {};
+ setFieldValue(`marketing_settings.${id}`, { ...current, value });
+ } else {
+ setFieldValue(id, value);
+ }
+ };
+
+ const handleSwitchChange = (setting, value) => {
+ const current = values.marketing_settings[setting] || {};
+ setFieldValue(`marketing_settings.${setting}`, { ...current, value });
+ };
+
+ const ms = values.marketing_settings;
+
+ return (
+
+
+
+
+ {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
+
+ {T.translate(
+ "edit_selection_plan.cfp_presentation_edition_custom_message"
+ )}
+
+
+
+
+
+
+ {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
+
+ {T.translate(
+ "edit_selection_plan.allowed_presentation_questions"
+ )}
+
+ opt.label}
+ isOptionEqualToValue={(opt, val) => opt.value === val.value}
+ value={DEFAULT_ALLOWED_QUESTIONS.filter((opt) =>
+ (values.allowed_presentation_questions || []).includes(
+ opt.value
+ )
+ )}
+ onChange={(_, selected) =>
+ setFieldValue(
+ "allowed_presentation_questions",
+ selected.map((opt) => opt.value)
+ )
+ }
+ renderTags={(value, getTagProps) =>
+ value.map((option, index) => {
+ const { key, ...tagProps } = getTagProps({ index });
+ // eslint-disable-next-line react/jsx-props-no-spreading
+ return (
+
+ );
+ })
+ }
+ renderInput={renderAllowedQuestionsInput}
+ />
+
+
+ {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
+
+ {T.translate(
+ "edit_selection_plan.allowed_presentation_editable_questions"
+ )}{" "}
+ *
+
+ opt.label}
+ isOptionEqualToValue={(opt, val) => opt.value === val.value}
+ value={DEFAULT_ALLOWED_EDITABLE_QUESTIONS.filter((opt) =>
+ (values.allowed_presentation_editable_questions || []).includes(
+ opt.value
+ )
+ )}
+ onChange={(_, selected) =>
+ setFieldValue(
+ "allowed_presentation_editable_questions",
+ selected.map((opt) => opt.value)
+ )
+ }
+ renderTags={(value, getTagProps) =>
+ value.map((option, index) => {
+ const { key, ...tagProps } = getTagProps({ index });
+ // eslint-disable-next-line react/jsx-props-no-spreading
+ return (
+
+ );
+ })
+ }
+ renderInput={renderAllowedEditableQuestionsInput}
+ />
+
+
+ {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
+
+ {T.translate(
+ "edit_selection_plan.cfp_presentation_edition_default_tab"
+ )}
+
+ opt.label}
+ isOptionEqualToValue={(opt, val) => opt.value === val.value}
+ value={
+ DEFAULT_CFP_PRESENTATION_EDITION_TABS.find(
+ (opt) =>
+ opt.value === ms.cfp_presentation_edition_default_tab?.value
+ ) || null
+ }
+ onChange={(_, selected) =>
+ handleSwitchChange(
+ "cfp_presentation_edition_default_tab",
+ selected ? selected.value : ""
+ )
+ }
+ renderInput={renderDefaultTabInput}
+ />
+
+
+ {[
+ ["cfp_landing_page_title", "cfp_landing_page_title_info"],
+ ["cfp_track_question_label", "cfp_track_question_label_info"],
+ ["cfp_speakers_singular_label", "cfp_speakers_singular_label_info"],
+ ["cfp_speakers_plural_label", "cfp_speakers_plural_label_info"],
+ [
+ "cfp_presentations_singular_label",
+ "cfp_presentations_singular_label_info"
+ ],
+ [
+ "cfp_presentations_plural_label",
+ "cfp_presentations_plural_label_info"
+ ],
+ [
+ "cfp_presentation_summary_title_label",
+ "cfp_presentation_summary_title_label_info"
+ ],
+ [
+ "cfp_presentation_summary_abstract_label",
+ "cfp_presentation_summary_abstract_label_info"
+ ],
+ [
+ "cfp_presentation_summary_social_summary_label",
+ "cfp_presentation_summary_social_summary_label_info"
+ ],
+ [
+ "cfp_presentation_summary_links_label",
+ "cfp_presentation_summary_links_label_info"
+ ]
+ ].map(([field, infoKey]) => (
+
+ {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
+
+ {T.translate(`edit_selection_plan.${field}`)}
+
+
+
+
+
+ ))}
+
+ {[
+ [
+ "cfp_presentation_summary_hide_track_selection",
+ "cfp_presentation_summary_hide_track_selection_info"
+ ],
+ [
+ "cfp_presentation_summary_hide_activity_type_selection",
+ "cfp_presentation_summary_hide_activity_type_selection_info"
+ ]
+ ].map(([field, infoKey]) => (
+
+ {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
+
+ {T.translate(`edit_selection_plan.${field}`)}
+
+
+
+
+ handleSwitchChange(field, ev.target.checked)}
+ />
+
+ ))}
+
+ {window.CFP_APP_BASE_URL && (
+ <>
+
+ {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
+
+ {T.translate(
+ "edit_selection_plan.cfp_presentation_selection_plan_link"
+ )}
+
+
+
+ {`${window.CFP_APP_BASE_URL}/app/${currentSummit.slug}/all-plans/${values.id}`}
+
+
+
+ {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
+
+ {T.translate(
+ "edit_selection_plan.cfp_presentation_all_selection_plan_link"
+ )}
+
+
+
+ {`${window.CFP_APP_BASE_URL}/app/${currentSummit.slug}/all-plans`}
+
+
+ >
+ )}
+
+
+
+ );
+};
+
+CfpSettingsTab.propTypes = {
+ hidden: PropTypes.bool.isRequired,
+ currentSummit: PropTypes.shape({
+ slug: PropTypes.string
+ }).isRequired
+};
+
+export default CfpSettingsTab;
diff --git a/src/components/forms/selection-plan-form/email-templates-tab.js b/src/components/forms/selection-plan-form/email-templates-tab.js
new file mode 100644
index 000000000..c822f0bbe
--- /dev/null
+++ b/src/components/forms/selection-plan-form/email-templates-tab.js
@@ -0,0 +1,95 @@
+/**
+ * Copyright 2017 OpenStack Foundation
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * */
+
+import React from "react";
+import PropTypes from "prop-types";
+import T from "i18n-react/dist/i18n-react";
+import { useFormikContext } from "formik";
+import Box from "@mui/material/Box";
+import Grid2 from "@mui/material/Grid2";
+import EmailTemplateInput from "../../inputs/email-template-input";
+
+const EmailTemplatesTab = ({ hidden }) => {
+ const { values, setFieldValue } = useFormikContext();
+
+ const handleChange = (ev) => setFieldValue(ev.target.id, ev.target.value);
+
+ return (
+
+
+
+
+ {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
+
+ {T.translate(
+ "edit_selection_plan.creator_notification_email_template"
+ )}
+
+
+
+
+ {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
+
+ {T.translate(
+ "edit_selection_plan.moderator_notification_email_template"
+ )}
+
+
+
+
+ {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
+
+ {T.translate(
+ "edit_selection_plan.speaker_notification_email_template"
+ )}
+
+
+
+
+
+
+ );
+};
+
+EmailTemplatesTab.propTypes = {
+ hidden: PropTypes.bool.isRequired
+};
+
+export default EmailTemplatesTab;
diff --git a/src/components/forms/selection-plan-form/event-types-tab.js b/src/components/forms/selection-plan-form/event-types-tab.js
new file mode 100644
index 000000000..5552d0412
--- /dev/null
+++ b/src/components/forms/selection-plan-form/event-types-tab.js
@@ -0,0 +1,116 @@
+/**
+ * Copyright 2017 OpenStack Foundation
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * */
+
+import React, { useState } from "react";
+import PropTypes from "prop-types";
+import T from "i18n-react/dist/i18n-react";
+import { useFormikContext } from "formik";
+import { queryEventTypes } from "openstack-uicore-foundation/lib/utils/query-actions";
+import Table from "openstack-uicore-foundation/lib/components/mui/table";
+import Autocomplete from "@mui/material/Autocomplete";
+import Box from "@mui/material/Box";
+import Button from "@mui/material/Button";
+import TextField from "@mui/material/TextField";
+import { PresentationTypeClassName } from "../../../utils/constants";
+
+function renderEventTypeInput(params) {
+ const placeholder = T.translate(
+ "edit_selection_plan.placeholders.event_type_search"
+ );
+ // eslint-disable-next-line react/jsx-props-no-spreading
+ return ;
+}
+
+const eventTypesColumns = [
+ { columnKey: "name", header: T.translate("edit_selection_plan.name") }
+];
+
+const EventTypesTab = ({
+ hidden,
+ currentSummit,
+ onAddEventType,
+ onDeleteEventType
+}) => {
+ const { values } = useFormikContext();
+ const [selection, setSelection] = useState(null);
+ const [searchOptions, setSearchOptions] = useState([]);
+
+ const handleAdd = () => {
+ onAddEventType(values.id, selection);
+ setSelection(null);
+ setSearchOptions([]);
+ };
+
+ return (
+
+
+
+ opt.name ?? ""}
+ filterOptions={(x) => x}
+ onInputChange={(_, val) => {
+ if (val)
+ queryEventTypes(
+ currentSummit.id,
+ val,
+ setSearchOptions,
+ PresentationTypeClassName
+ );
+ }}
+ onChange={(_, val) => setSelection(val)}
+ renderInput={renderEventTypeInput}
+ sx={{ width: 320 }}
+ />
+
+ {T.translate("general.add")}
+
+
+ {values.event_types.length === 0 && (
+ {T.translate("edit_selection_plan.no_event_types")}
+ )}
+ {values.event_types.length > 0 && (
+ onDeleteEventType(values.id, id)}
+ confirmButtonColor="error"
+ deleteDialogBody={(name) =>
+ `${T.translate(
+ "edit_selection_plan.delete_confirm.event_type"
+ )} ${name}`
+ }
+ />
+ )}
+
+
+ );
+};
+
+EventTypesTab.propTypes = {
+ hidden: PropTypes.bool.isRequired,
+ currentSummit: PropTypes.shape({ id: PropTypes.number.isRequired })
+ .isRequired,
+ onAddEventType: PropTypes.func.isRequired,
+ onDeleteEventType: PropTypes.func.isRequired
+};
+
+export default EventTypesTab;
diff --git a/src/components/forms/selection-plan-form/extra-questions-tab.js b/src/components/forms/selection-plan-form/extra-questions-tab.js
new file mode 100644
index 000000000..65727441b
--- /dev/null
+++ b/src/components/forms/selection-plan-form/extra-questions-tab.js
@@ -0,0 +1,135 @@
+/**
+ * Copyright 2017 OpenStack Foundation
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * */
+
+import React from "react";
+import PropTypes from "prop-types";
+import T from "i18n-react/dist/i18n-react";
+import { useFormikContext } from "formik";
+import SortableTable from "openstack-uicore-foundation/lib/components/mui/sortable-table";
+import Box from "@mui/material/Box";
+import Button from "@mui/material/Button";
+import Grid2 from "@mui/material/Grid2";
+import Many2ManyDropDown from "../../inputs/many-2-many-dropdown";
+import { querySelectionPlanExtraQuestions } from "../../../actions/selection-plan-actions";
+import { stripTags } from "../../../utils/methods";
+
+const extraQuestionColumns = [
+ {
+ columnKey: "type",
+ header: T.translate("order_extra_question_list.question_type")
+ },
+ {
+ columnKey: "label",
+ header: T.translate("order_extra_question_list.visible_question")
+ },
+ {
+ columnKey: "name",
+ header: T.translate("order_extra_question_list.question_id")
+ }
+];
+
+const ExtraQuestionsTab = ({
+ hidden,
+ currentSummit,
+ extraQuestionsOrder,
+ extraQuestionsOrderDir,
+ onEditExtraQuestion,
+ onDeleteExtraQuestion,
+ onAddNewExtraQuestion,
+ onAssignExtraQuestion2SelectionPlan,
+ updateExtraQuestionOrder
+}) => {
+ const { values } = useFormikContext();
+
+ const fetchOptions = (input, callback) => {
+ if (!input) return Promise.resolve({ options: [] });
+ return querySelectionPlanExtraQuestions(currentSummit.id, input, callback);
+ };
+
+ const handleLink = (question) => {
+ onAssignExtraQuestion2SelectionPlan(
+ currentSummit.id,
+ values.id,
+ question.id
+ );
+ };
+
+ return (
+
+ );
+};
+
+ExtraQuestionsTab.propTypes = {
+ hidden: PropTypes.bool.isRequired,
+ currentSummit: PropTypes.shape({ id: PropTypes.number.isRequired })
+ .isRequired,
+ extraQuestionsOrder: PropTypes.string.isRequired,
+ extraQuestionsOrderDir: PropTypes.number.isRequired,
+ onEditExtraQuestion: PropTypes.func.isRequired,
+ onDeleteExtraQuestion: PropTypes.func.isRequired,
+ onAddNewExtraQuestion: PropTypes.func.isRequired,
+ onAssignExtraQuestion2SelectionPlan: PropTypes.func.isRequired,
+ updateExtraQuestionOrder: PropTypes.func.isRequired
+};
+
+export default ExtraQuestionsTab;
diff --git a/src/components/forms/selection-plan-form/main-tab.js b/src/components/forms/selection-plan-form/main-tab.js
new file mode 100644
index 000000000..f87df7bc2
--- /dev/null
+++ b/src/components/forms/selection-plan-form/main-tab.js
@@ -0,0 +1,221 @@
+/**
+ * Copyright 2017 OpenStack Foundation
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * */
+
+import React from "react";
+import PropTypes from "prop-types";
+import T from "i18n-react/dist/i18n-react";
+import { useFormikContext } from "formik";
+import TextEditorV3 from "openstack-uicore-foundation/lib/components/inputs/editor-input-v3";
+import Checkbox from "@mui/material/Checkbox";
+import FormControlLabel from "@mui/material/FormControlLabel";
+import Grid2 from "@mui/material/Grid2";
+import TextField from "@mui/material/TextField";
+import MuiFormikDatetimepicker from "../../mui/formik-inputs/mui-formik-datetimepicker";
+
+const MainTab = ({ hidden, currentSummit }) => {
+ const { values, errors, setFieldValue } = useFormikContext();
+
+ const hasErrors = (field) => errors[field] ?? "";
+
+ const handleChange = (ev) => {
+ const value =
+ ev.target.type === "checkbox" ? ev.target.checked : ev.target.value;
+ setFieldValue(ev.target.id, value);
+ };
+
+ return (
+
+
+
+ {T.translate("edit_selection_plan.name")} *
+
+
+
+
+ }
+ label={T.translate("edit_selection_plan.enabled")}
+ />
+
+
+
+ }
+ label={T.translate("edit_selection_plan.hidden")}
+ />
+
+
+
+ }
+ label={T.translate("edit_selection_plan.allow_proposed_schedules")}
+ />
+
+
+
+ }
+ label={T.translate("edit_selection_plan.allow_new_presentations")}
+ />
+
+
+
+
+
+
+ {T.translate("edit_selection_plan.submission_begin_date")}
+
+
+
+
+
+ {T.translate("edit_selection_plan.submission_end_date")}
+
+
+
+
+
+
+
+ {T.translate("edit_selection_plan.max_submissions")}
+
+
+
+
+ {T.translate(
+ "edit_selection_plan.submission_lock_down_presentation_status_date"
+ )}{" "}
+
+
+
+
+
+
+
+
+
+ {T.translate("edit_selection_plan.voting_begin_date")}
+
+
+
+ {T.translate("edit_selection_plan.voting_end_date")}
+
+
+
+
+
+
+
+ {T.translate("edit_selection_plan.selection_begin_date")}
+
+
+
+
+ {T.translate("edit_selection_plan.selection_end_date")}
+
+
+
+
+
+
+
+ {T.translate("edit_selection_plan.submission_period_disclaimer")} *
+
+
+
+
+
+ );
+};
+
+MainTab.propTypes = {
+ hidden: PropTypes.bool.isRequired,
+ currentSummit: PropTypes.shape({
+ time_zone_id: PropTypes.string.isRequired
+ }).isRequired
+};
+
+export default MainTab;
diff --git a/src/components/forms/selection-plan-form/presentation-action-types-tab.js b/src/components/forms/selection-plan-form/presentation-action-types-tab.js
new file mode 100644
index 000000000..9358ee35e
--- /dev/null
+++ b/src/components/forms/selection-plan-form/presentation-action-types-tab.js
@@ -0,0 +1,108 @@
+/**
+ * Copyright 2017 OpenStack Foundation
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * */
+
+import React from "react";
+import PropTypes from "prop-types";
+import T from "i18n-react/dist/i18n-react";
+import { useFormikContext } from "formik";
+import SortableTable from "openstack-uicore-foundation/lib/components/mui/sortable-table";
+import Box from "@mui/material/Box";
+import Grid2 from "@mui/material/Grid2";
+import Many2ManyDropDown from "../../inputs/many-2-many-dropdown";
+import { querySummitProgressFlags } from "../../../actions/track-chair-actions";
+
+const actionTypesColumns = [
+ { columnKey: "label", header: T.translate("progress_flags.label") }
+];
+
+const PresentationActionTypesTab = ({
+ hidden,
+ currentSummit,
+ actionTypesOrder,
+ actionTypesOrderDir,
+ onAssignProgressFlag2SelectionPlan,
+ onUnassignProgressFlag,
+ onUpdateProgressFlagOrder
+}) => {
+ const { values } = useFormikContext();
+
+ const fetchOptions = (input, callback) => {
+ if (!input) return Promise.resolve({ options: [] });
+ return querySummitProgressFlags(currentSummit.id, input, callback);
+ };
+
+ const handleLink = (progressFlag) => {
+ onAssignProgressFlag2SelectionPlan(
+ currentSummit.id,
+ values.id,
+ progressFlag.id
+ );
+ };
+
+ return (
+
+
+
+
+
+
+
+ {values.allowed_presentation_action_types.length === 0 && (
+
+ {T.translate("edit_selection_plan.no_presentation_action_types")}
+
+ )}
+ {values.allowed_presentation_action_types.length > 0 && (
+
+ )}
+
+
+ );
+};
+
+PresentationActionTypesTab.propTypes = {
+ hidden: PropTypes.bool.isRequired,
+ currentSummit: PropTypes.shape({ id: PropTypes.number.isRequired })
+ .isRequired,
+ actionTypesOrder: PropTypes.string.isRequired,
+ actionTypesOrderDir: PropTypes.number.isRequired,
+ onAssignProgressFlag2SelectionPlan: PropTypes.func.isRequired,
+ onUnassignProgressFlag: PropTypes.func.isRequired,
+ onUpdateProgressFlagOrder: PropTypes.func.isRequired
+};
+
+export default PresentationActionTypesTab;
diff --git a/src/components/forms/selection-plan-form/track-chair-settings-tab.js b/src/components/forms/selection-plan-form/track-chair-settings-tab.js
new file mode 100644
index 000000000..9c4b492b7
--- /dev/null
+++ b/src/components/forms/selection-plan-form/track-chair-settings-tab.js
@@ -0,0 +1,82 @@
+/**
+ * Copyright 2017 OpenStack Foundation
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * */
+
+import React from "react";
+import PropTypes from "prop-types";
+import T from "i18n-react/dist/i18n-react";
+import { useFormikContext } from "formik";
+import SortableTable from "openstack-uicore-foundation/lib/components/mui/sortable-table";
+import Box from "@mui/material/Box";
+import Button from "@mui/material/Button";
+import Checkbox from "@mui/material/Checkbox";
+import FormControlLabel from "@mui/material/FormControlLabel";
+
+const ratingTypesColumns = [
+ { columnKey: "name", header: T.translate("rating_type_list.name") },
+ { columnKey: "weight", header: T.translate("rating_type_list.weight") }
+];
+
+const TrackChairSettingsTab = ({
+ hidden,
+ onAddRatingType,
+ onEditRatingType,
+ onDeleteRatingType,
+ onUpdateRatingTypeOrder
+}) => {
+ const { values, setFieldValue } = useFormikContext();
+
+ const handleChange = (ev) => setFieldValue(ev.target.id, ev.target.checked);
+
+ return (
+
+
+
+ }
+ label={T.translate("track_chair_settings.allow_change_requests")}
+ />
+
+
+
+ {T.translate("track_chair_settings.add_rating_type")}
+
+
+ onEditRatingType(item.id)}
+ onDelete={onDeleteRatingType}
+ confirmButtonColor="error"
+ />
+
+
+ );
+};
+
+TrackChairSettingsTab.propTypes = {
+ hidden: PropTypes.bool.isRequired,
+ onAddRatingType: PropTypes.func.isRequired,
+ onEditRatingType: PropTypes.func.isRequired,
+ onDeleteRatingType: PropTypes.func.isRequired,
+ onUpdateRatingTypeOrder: PropTypes.func.isRequired
+};
+
+export default TrackChairSettingsTab;
diff --git a/src/components/forms/selection-plan-form/track-groups-tab.js b/src/components/forms/selection-plan-form/track-groups-tab.js
new file mode 100644
index 000000000..e4eb53deb
--- /dev/null
+++ b/src/components/forms/selection-plan-form/track-groups-tab.js
@@ -0,0 +1,119 @@
+/**
+ * Copyright 2017 OpenStack Foundation
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * */
+
+import React, { useState } from "react";
+import PropTypes from "prop-types";
+import T from "i18n-react/dist/i18n-react";
+import { useFormikContext } from "formik";
+import { queryTrackGroups } from "openstack-uicore-foundation/lib/utils/query-actions";
+import Table from "openstack-uicore-foundation/lib/components/mui/table";
+import Autocomplete from "@mui/material/Autocomplete";
+import Box from "@mui/material/Box";
+import Button from "@mui/material/Button";
+import TextField from "@mui/material/TextField";
+import { stripTags } from "../../../utils/methods";
+
+const trackGroupsColumns = [
+ { columnKey: "name", header: T.translate("edit_selection_plan.name") },
+ {
+ columnKey: "description",
+ header: T.translate("edit_selection_plan.description")
+ }
+];
+
+// Regular function declaration: eslint-disable-next-line must precede the line with {...params}
+function renderTrackGroupInput(params) {
+ const placeholder = T.translate(
+ "edit_selection_plan.placeholders.track_groups_search"
+ );
+ // eslint-disable-next-line react/jsx-props-no-spreading
+ return ;
+}
+
+const TrackGroupsTab = ({
+ hidden,
+ currentSummit,
+ onTrackGroupLink,
+ onTrackGroupUnLink
+}) => {
+ const { values } = useFormikContext();
+ const [selection, setSelection] = useState(null);
+ const [searchOptions, setSearchOptions] = useState([]);
+
+ const handleLink = () => {
+ onTrackGroupLink(values.id, selection);
+ setSelection(null);
+ setSearchOptions([]);
+ };
+
+ return (
+
+
+
+ opt.name ?? ""}
+ filterOptions={(x) => x}
+ onInputChange={(_, val) => {
+ if (val)
+ queryTrackGroups(currentSummit.id, val, setSearchOptions);
+ }}
+ onChange={(_, val) => setSelection(val)}
+ renderInput={renderTrackGroupInput}
+ sx={{ width: 320 }}
+ />
+
+ {T.translate("general.add")}
+
+
+ {values.track_groups.length === 0 && (
+ {T.translate("edit_selection_plan.no_track_groups")}
+ )}
+ {values.track_groups.length > 0 && (
+ ({
+ ...tg,
+ description: stripTags(tg.description ?? "")
+ }))}
+ columns={trackGroupsColumns}
+ options={{}}
+ onDelete={(id) => onTrackGroupUnLink(values.id, id)}
+ confirmButtonColor="error"
+ deleteDialogBody={(name) =>
+ `${T.translate(
+ "edit_selection_plan.delete_confirm.track_group"
+ )} ${name}`
+ }
+ />
+ )}
+
+
+ );
+};
+
+TrackGroupsTab.propTypes = {
+ hidden: PropTypes.bool.isRequired,
+ currentSummit: PropTypes.shape({ id: PropTypes.number.isRequired })
+ .isRequired,
+ onTrackGroupLink: PropTypes.func.isRequired,
+ onTrackGroupUnLink: PropTypes.func.isRequired
+};
+
+export default TrackGroupsTab;
diff --git a/src/components/inputs/import-modal/__tests__/import-modal.test.js b/src/components/inputs/import-modal/__tests__/import-modal.test.js
new file mode 100644
index 000000000..f0aea17bc
--- /dev/null
+++ b/src/components/inputs/import-modal/__tests__/import-modal.test.js
@@ -0,0 +1,88 @@
+/* eslint-disable react/jsx-props-no-spreading */
+import React from "react";
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import "@testing-library/jest-dom";
+import ImportModal from "../index";
+
+jest.mock("i18n-react/dist/i18n-react", () => ({
+ __esModule: true,
+ default: { translate: (key) => key }
+}));
+
+jest.mock("i18n-react", () => ({
+ __esModule: true,
+ default: { translate: (key) => key }
+}));
+
+jest.mock(
+ "openstack-uicore-foundation/lib/components/inputs/upload-input",
+ () => ({
+ __esModule: true,
+ default: ({ handleUpload }) => (
+ handleUpload(new File([""], "data.csv"))}
+ data-testid="upload-trigger"
+ >
+ pick file
+
+ )
+ })
+);
+
+describe("ImportModal", () => {
+ const baseProps = {
+ title: "Import Members",
+ show: true,
+ wrapperClass: "test-wrapper",
+ onHide: jest.fn(),
+ onIngest: jest.fn(),
+ children: format help
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it("renders in MUI Dialog layer (not bootstrap modal)", () => {
+ render( );
+ // MUI Dialog root must exist - proves it uses MUI, not bootstrap
+ expect(document.querySelector(".MuiDialog-root")).toBeTruthy();
+ // Bootstrap modal-backdrop must NOT exist - proves no z-index 1040/1050 layer
+ expect(document.querySelector(".modal-backdrop")).toBeNull();
+ });
+
+ it("shows title and children when open", () => {
+ render( );
+ expect(screen.getByText("Import Members")).toBeInTheDocument();
+ expect(screen.getByText("format help")).toBeInTheDocument();
+ });
+
+ it("ingest button is disabled before a file is selected", () => {
+ render( );
+ expect(
+ screen.getByRole("button", { name: "general.ingest" })
+ ).toBeDisabled();
+ });
+
+ it("enables ingest and calls onIngest after a file is selected", async () => {
+ render( );
+ await userEvent.click(screen.getByTestId("upload-trigger"));
+ const ingestBtn = screen.getByRole("button", { name: "general.ingest" });
+ expect(ingestBtn).not.toBeDisabled();
+ await userEvent.click(ingestBtn);
+ expect(baseProps.onIngest).toHaveBeenCalledWith(expect.any(File));
+ });
+
+ it("calls onHide when the close button is clicked", async () => {
+ render( );
+ await userEvent.click(screen.getByRole("button", { name: "Close" }));
+ expect(baseProps.onHide).toHaveBeenCalled();
+ });
+
+ it("does not render dialog content when show is false", () => {
+ render( );
+ expect(document.querySelector(".MuiDialog-root")).toBeNull();
+ });
+});
diff --git a/src/components/inputs/import-modal/index.jsx b/src/components/inputs/import-modal/index.jsx
index c2f8a3a4d..b3ffc9e81 100644
--- a/src/components/inputs/import-modal/index.jsx
+++ b/src/components/inputs/import-modal/index.jsx
@@ -1,9 +1,23 @@
import React, { useState } from "react";
-import { Modal } from "react-bootstrap";
+import PropTypes from "prop-types";
import T from "i18n-react";
-import { UploadInput } from "openstack-uicore-foundation/lib/components";
+import Button from "@mui/material/Button";
+import Dialog from "@mui/material/Dialog";
+import DialogActions from "@mui/material/DialogActions";
+import DialogContent from "@mui/material/DialogContent";
+import DialogTitle from "@mui/material/DialogTitle";
+import IconButton from "@mui/material/IconButton";
+import CloseIcon from "@mui/icons-material/Close";
+import UploadInput from "openstack-uicore-foundation/lib/components/inputs/upload-input";
-export default ({ title, children, show, wrapperClass, onHide, onIngest }) => {
+const ImportModal = ({
+ title,
+ children,
+ show,
+ wrapperClass,
+ onHide,
+ onIngest
+}) => {
const [importFile, setImportFile] = useState(null);
const handleImport = () => {
@@ -12,11 +26,20 @@ export default ({ title, children, show, wrapperClass, onHide, onIngest }) => {
};
return (
-
-
- {title}
-
-
+
+
+ {title}
+
+
+
+
+
Format must be the following: (Minimal data required)
@@ -34,16 +57,32 @@ export default ({ title, children, show, wrapperClass, onHide, onIngest }) => {
/>
-
-
-
+
+
{T.translate("general.ingest")}
-
-
-
+
+
+
);
};
+
+ImportModal.propTypes = {
+ title: PropTypes.string.isRequired,
+ children: PropTypes.node,
+ show: PropTypes.bool.isRequired,
+ wrapperClass: PropTypes.string,
+ onHide: PropTypes.func.isRequired,
+ onIngest: PropTypes.func.isRequired
+};
+
+ImportModal.defaultProps = {
+ children: null,
+ wrapperClass: ""
+};
+
+export default ImportModal;
diff --git a/src/components/mui/formik-inputs/mui-formik-datetimepicker.js b/src/components/mui/formik-inputs/mui-formik-datetimepicker.js
new file mode 100644
index 000000000..925c8783c
--- /dev/null
+++ b/src/components/mui/formik-inputs/mui-formik-datetimepicker.js
@@ -0,0 +1,77 @@
+/**
+ * Copyright 2026 OpenStack Foundation
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * */
+
+import React from "react";
+import PropTypes from "prop-types";
+import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
+import { AdapterMoment } from "@mui/x-date-pickers/AdapterMoment";
+import { DateTimePicker } from "@mui/x-date-pickers/DateTimePicker";
+import { useField } from "formik";
+
+const MuiFormikDatetimepicker = ({
+ name,
+ label,
+ required,
+ disabled = false,
+ timezone,
+ ...props
+}) => {
+ const [field, meta, helpers] = useField(name);
+ const requiredLabel = `${label} *`;
+
+ return (
+
+
+
+ );
+};
+
+MuiFormikDatetimepicker.propTypes = {
+ name: PropTypes.string.isRequired,
+ label: PropTypes.string,
+ required: PropTypes.bool,
+ disabled: PropTypes.bool,
+ timezone: PropTypes.string
+};
+
+export default MuiFormikDatetimepicker;
diff --git a/src/i18n/en.json b/src/i18n/en.json
index eda7913d2..10feb3b30 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -499,6 +499,7 @@
"selection_begin_date": "TC Selections Start",
"selection_end_date": "TC Selections End",
"track_groups": "Category Groups",
+ "no_track_groups": "No category groups found.",
"description": "Description",
"submission_period_disclaimer": "Disclaimer",
"max_submissions": "Max Submissions Per User",
@@ -507,6 +508,7 @@
"selection_plan_saved": "Selection Plan saved successfully.",
"selection_plan_created": "Selection Plan created successfully.",
"event_types": "Activity Types",
+ "no_event_types": "No activity types found.",
"extra_questions": "Extra Questions",
"add_extra_questions": "Create New Extra Question",
"no_extra_questions": "No extra questions found.",
@@ -556,12 +558,22 @@
"cfp_presentation_edition_default_tab": "Default Landing Tab on Edition",
"cfp_presentation_selection_plan_link": "Selection Plan Link",
"cfp_presentation_all_selection_plan_link": "All Selection Plans Link",
+ "delete_confirm": {
+ "track_group": "Please verify you want to delete category group",
+ "event_type": "Please verify you want to delete activity type",
+ "extra_question": "Please verify you want to delete extra question",
+ "rating_type": "Please verify you want to delete rating type",
+ "action_type": "Please verify you want to delete progress flag",
+ "allowed_member": "Please verify you want to delete member"
+ },
"placeholders": {
"creator_notification_email_select_template": "Select a creator notification email template...",
"moderator_notification_email_select_template": "Select a moderator notification email template...",
"speaker_notification_email_select_template": "Select a speaker notification email template...",
- "link_question": "Link an Existent Question...",
- "link_presentation_action_type": "Link an Existent Action Type...",
+ "link_question": "Link an Existing Question...",
+ "link_presentation_action_type": "Link an Existing Action Type...",
+ "track_groups_search": "Search category groups...",
+ "event_type_search": "Search activity types...",
"allowed_presentation_questions": "Select questions to display...",
"allowed_presentation_editable_questions": "Select questions to Edit...",
"cfp_presentation_edition_default_tab": "Select a landing tab ..."
diff --git a/src/layouts/selection-plan-id-layout.js b/src/layouts/selection-plan-id-layout.js
index 4fffb8749..04e41a5ed 100644
--- a/src/layouts/selection-plan-id-layout.js
+++ b/src/layouts/selection-plan-id-layout.js
@@ -11,9 +11,6 @@ import {
import { getMarketingSettingsBySelectionPlan } from "../actions/marketing-actions";
import { MAX_PER_PAGE } from "../utils/constants";
-const EditSelectionPlanPage = React.lazy(() =>
- import("../pages/selection-plans/edit-selection-plan-page")
-);
const SelectionPlanExtraQuestionsLayout = React.lazy(() =>
import("./selection-plan-extra-questions-layout")
);
@@ -54,12 +51,6 @@ const SelectionPlanIdLayout = ({
}>
-
-
+
diff --git a/src/layouts/selection-plan-layout.js b/src/layouts/selection-plan-layout.js
index 57d103b6d..3a4043345 100644
--- a/src/layouts/selection-plan-layout.js
+++ b/src/layouts/selection-plan-layout.js
@@ -38,7 +38,7 @@ const SelectionPlanLayout = ({ match, currentSummit }) => (
strict
exact
path={`${match.url}/new`}
- component={SelectionPlanIdLayout}
+ render={() => }
/>
({
+ getSelectionPlans: jest.fn(),
+ getSelectionPlan: jest.fn(),
+ deleteSelectionPlan: jest.fn(),
+ resetSelectionPlanForm: jest.fn(),
+ saveSelectionPlan: jest.fn(),
+ saveSelectionPlanSettings: jest.fn()
+}));
+
+jest.mock("../../../actions/marketing-actions", () => ({
+ getMarketingSettingsBySelectionPlan: jest.fn()
+}));
+
+jest.mock("openstack-uicore-foundation/lib/components/mui/table", () => ({
+ __esModule: true,
+ default: ({ onEdit, onDelete }) => (
+
+ onEdit({ id: 1, name: "CFP 2026" })}>
+ edit-row
+
+ onDelete(1)}>
+ delete-row
+
+
+ )
+}));
+
+jest.mock(
+ "openstack-uicore-foundation/lib/components/mui/search-input",
+ () => ({
+ __esModule: true,
+ default: () =>
+ })
+);
+
+jest.mock("../edit-selection-plan-page", () => ({
+ __esModule: true,
+ default: ({ onSave }) => (
+
+ onSave({ marketing_settings: {} })}>
+ popup-save
+
+
+ )
+}));
+
+jest.mock("i18n-react/dist/i18n-react", () => ({
+ __esModule: true,
+ default: { translate: (key) => key }
+}));
+
+const mockHistory = { replace: jest.fn() };
+const mockMatch = { params: {} };
+
+const initialState = {
+ currentSummitState: {
+ currentSummit: { id: 1 }
+ },
+ currentSelectionPlanListState: {
+ selectionPlans: [
+ { id: 1, name: "CFP 2026", is_enabled: "yes", is_hidden: "no" }
+ ],
+ totalSelectionPlans: 1,
+ perPage: 10,
+ currentPage: 1,
+ term: "",
+ order: "id",
+ orderDir: 1
+ },
+ currentSelectionPlanState: {
+ entity: { id: 0, name: "" },
+ errors: {}
+ }
+};
+
+describe("SelectionPlanListPage", () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ getSelectionPlans.mockReturnValue(() => Promise.resolve());
+ getSelectionPlan.mockReturnValue(() => Promise.resolve());
+ deleteSelectionPlan.mockReturnValue(() => Promise.resolve());
+ resetSelectionPlanForm.mockReturnValue({
+ type: "RESET_SELECTION_PLAN_FORM"
+ });
+ getMarketingSettingsBySelectionPlan.mockReturnValue(() =>
+ Promise.resolve()
+ );
+ saveSelectionPlan.mockReturnValue(() => Promise.resolve({ id: 1 }));
+ saveSelectionPlanSettings.mockReturnValue(() => Promise.resolve());
+ });
+
+ it("reloads the list after a successful save", async () => {
+ renderWithRedux(
+ ,
+ { initialState }
+ );
+
+ // Open dialog
+ await userEvent.click(
+ screen.getByRole("button", {
+ name: "selection_plan_list.add_selection_plan"
+ })
+ );
+ expect(screen.getByTestId("edit-selection-plan")).toBeInTheDocument();
+
+ await act(async () => {
+ await userEvent.click(screen.getByRole("button", { name: "popup-save" }));
+ await flushPromises();
+ });
+
+ // Call 1: useEffect on mount; call 2: handleSave → refreshSelectionPlans
+ expect(getSelectionPlans).toHaveBeenCalledTimes(2);
+ });
+
+ it("reloads the list after a successful delete", async () => {
+ renderWithRedux(
+ ,
+ { initialState }
+ );
+
+ await act(async () => {
+ await userEvent.click(screen.getByRole("button", { name: "delete-row" }));
+ await flushPromises();
+ });
+
+ // Call 1: useEffect on mount; call 2: handleDelete .then()
+ expect(getSelectionPlans).toHaveBeenCalledTimes(2);
+ });
+
+ it("does not reload the list after a failed delete", async () => {
+ deleteSelectionPlan.mockReturnValue(() =>
+ Promise.reject(new Error("delete failed"))
+ );
+
+ renderWithRedux(
+ ,
+ { initialState }
+ );
+
+ await act(async () => {
+ await userEvent.click(screen.getByRole("button", { name: "delete-row" }));
+ await flushPromises();
+ });
+
+ // Only call 1: useEffect on mount — .then() does not fire on rejection
+ expect(getSelectionPlans).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/src/pages/selection-plans/edit-selection-plan-page.js b/src/pages/selection-plans/edit-selection-plan-page.js
index 07d37b595..7eb80ef62 100644
--- a/src/pages/selection-plans/edit-selection-plan-page.js
+++ b/src/pages/selection-plans/edit-selection-plan-page.js
@@ -28,20 +28,18 @@ import {
importAllowedMembersCSV,
removeAllowedMemberFromSelectionPlan,
removeTrackGroupFromSelectionPlan,
- saveSelectionPlan,
- saveSelectionPlanSettings,
unassignProgressFlagFromSelectionPlan,
updateProgressFlagOrder,
updateRatingTypeOrder,
updateSelectionPlanExtraQuestionOrder
} from "../../actions/selection-plan-actions";
-import AddNewButton from "../../components/buttons/add-new-button";
const EditSelectionPlanPage = ({
currentSummit,
entity,
allowedMembers,
errors,
+ onSave,
history,
extraQuestionsOrder,
extraQuestionsOrderDir,
@@ -53,8 +51,6 @@ const EditSelectionPlanPage = ({
updateProgressFlagOrder,
addTrackGroupToSelectionPlan,
removeTrackGroupFromSelectionPlan,
- saveSelectionPlan,
- saveSelectionPlanSettings,
addAllowedMemberToSelectionPlan,
addEventTypeSelectionPlan,
assignExtraQuestion2SelectionPlan,
@@ -64,10 +60,6 @@ const EditSelectionPlanPage = ({
importAllowedMembersCSV,
removeAllowedMemberFromSelectionPlan
}) => {
- const title = entity.id
- ? T.translate("general.edit")
- : T.translate("general.add");
-
const onDeleteExtraQuestion = (questionId) => {
const extraQuestion = entity.extra_questions.find(
(t) => t.id === questionId
@@ -184,45 +176,37 @@ const EditSelectionPlanPage = ({
};
return (
-
-
- {title} {T.translate("edit_selection_plan.selection_plan")}
-
-
-
-
-
+
);
};
@@ -235,7 +219,6 @@ const mapStateToProps = ({
});
export default connect(mapStateToProps, {
- saveSelectionPlan,
addTrackGroupToSelectionPlan,
removeTrackGroupFromSelectionPlan,
addEventTypeSelectionPlan,
@@ -251,6 +234,5 @@ export default connect(mapStateToProps, {
addAllowedMemberToSelectionPlan,
removeAllowedMemberFromSelectionPlan,
getAllowedMembers,
- importAllowedMembersCSV,
- saveSelectionPlanSettings
+ importAllowedMembersCSV
})(EditSelectionPlanPage);
diff --git a/src/pages/selection-plans/selection-plan-list-page.js b/src/pages/selection-plans/selection-plan-list-page.js
index 349ac5218..a4c6266bc 100644
--- a/src/pages/selection-plans/selection-plan-list-page.js
+++ b/src/pages/selection-plans/selection-plan-list-page.js
@@ -11,169 +11,255 @@
* limitations under the License.
* */
-import React, { useEffect } from "react";
+import React, { useCallback, useEffect, useState } from "react";
import { connect } from "react-redux";
import T from "i18n-react/dist/i18n-react";
-import Swal from "sweetalert2";
-import { Pagination } from "react-bootstrap";
-import Table from "openstack-uicore-foundation/lib/components/table"
-import FreeTextSearch from "openstack-uicore-foundation/lib/components/free-text-search";
+import Grid2 from "@mui/material/Grid2";
+import Button from "@mui/material/Button";
+import AddIcon from "@mui/icons-material/Add";
+import SearchInput from "openstack-uicore-foundation/lib/components/mui/search-input";
+import MuiTable from "openstack-uicore-foundation/lib/components/mui/table";
import {
deleteSelectionPlan,
- getSelectionPlans
+ getSelectionPlan,
+ getSelectionPlans,
+ resetSelectionPlanForm,
+ saveSelectionPlan,
+ saveSelectionPlanSettings
} from "../../actions/selection-plan-actions";
+import { getMarketingSettingsBySelectionPlan } from "../../actions/marketing-actions";
+import { DEFAULT_CURRENT_PAGE, MAX_PER_PAGE } from "../../utils/constants";
+import SelectionPlanPopup from "./selection-plan-popup";
const SelectionPlanListPage = ({
currentSummit,
history,
selectionPlans,
+ currentSelectionPlan,
totalSelectionPlans,
+ perPage,
term,
order,
orderDir,
- lastPage,
currentPage,
+ getSelectionPlan,
getSelectionPlans,
- deleteSelectionPlan
+ resetSelectionPlanForm,
+ getMarketingSettingsBySelectionPlan,
+ deleteSelectionPlan,
+ saveSelectionPlan,
+ saveSelectionPlanSettings
}) => {
+ const [openSelectionPlanPopup, setOpenSelectionPlanPopup] = useState(false);
+
+ const openEditModal = useCallback(
+ (selectionPlanId) => {
+ if (!selectionPlanId) return;
+
+ getSelectionPlan(selectionPlanId)
+ .then(() =>
+ getMarketingSettingsBySelectionPlan(
+ selectionPlanId,
+ null,
+ DEFAULT_CURRENT_PAGE,
+ MAX_PER_PAGE
+ )
+ )
+ .then(() => setOpenSelectionPlanPopup(true));
+ },
+ [getMarketingSettingsBySelectionPlan, getSelectionPlan]
+ );
+
useEffect(() => {
- getSelectionPlans();
- }, []);
+ if (currentSummit?.id) {
+ getSelectionPlans(term, DEFAULT_CURRENT_PAGE, perPage, order, orderDir);
+ }
+ }, [currentSummit]);
- const handleEdit = (selectionPlanId) => {
- history.push(
- `/app/summits/${currentSummit.id}/selection-plans/${selectionPlanId}`
- );
+ const refreshSelectionPlans = () =>
+ getSelectionPlans(term, currentPage, perPage, order, orderDir);
+
+ const handleEdit = (selectionPlan) => {
+ if (!selectionPlan?.id) return;
+ openEditModal(selectionPlan.id);
};
- const handleDelete = (selectionPlanId) => {
- const selectionPlan = selectionPlans.find((s) => s.id === selectionPlanId);
-
- Swal.fire({
- title: T.translate("general.are_you_sure"),
- text: `${T.translate("selection_plan_list.remove_warning")} ${
- selectionPlan.name
- }`,
- type: "warning",
- showCancelButton: true,
- confirmButtonColor: "#DD6B55",
- confirmButtonText: T.translate("general.yes_delete")
- }).then((result) => {
- if (result.value) {
- deleteSelectionPlan(selectionPlanId);
- }
- });
+ const handleDelete = (id) => {
+ if (!id) return;
+
+ deleteSelectionPlan(id)
+ .then(() => refreshSelectionPlans())
+ .catch(() => {});
};
const handleNew = () => {
- history.push(`/app/summits/${currentSummit.id}/selection-plans/new`);
+ resetSelectionPlanForm();
+ setOpenSelectionPlanPopup(true);
+ };
+
+ const handleClosePopup = () => {
+ resetSelectionPlanForm();
+ setOpenSelectionPlanPopup(false);
};
- const handleSort = (index, key, dir) => {
- getSelectionPlans(term, currentPage, key, dir);
+ const handleSave = (entity) =>
+ saveSelectionPlan(entity)
+ .then((savedEntity) => {
+ if (!savedEntity?.id) return null;
+ return saveSelectionPlanSettings(
+ entity.marketing_settings ?? {},
+ savedEntity.id
+ );
+ })
+ .then(() => refreshSelectionPlans());
+
+ const handleSort = (key, dir) => {
+ getSelectionPlans(term, currentPage, perPage, key, dir);
};
- const handlePageChange = (newPage) => {
- getSelectionPlans(term, newPage, order, orderDir);
+ const handlePageChange = (page) => {
+ getSelectionPlans(term, page, perPage, order, orderDir);
+ };
+
+ const handlePerPageChange = (newPerPage) => {
+ getSelectionPlans(
+ term,
+ DEFAULT_CURRENT_PAGE,
+ parseInt(newPerPage, 10),
+ order,
+ orderDir
+ );
};
const handleSearch = (newTerm) => {
- getSelectionPlans(newTerm, 1, order, orderDir);
+ getSelectionPlans(newTerm, DEFAULT_CURRENT_PAGE, perPage, order, orderDir);
};
const columns = [
- { columnKey: "id", value: T.translate("selection_plan_list.id") },
+ {
+ columnKey: "id",
+ header: T.translate("selection_plan_list.id"),
+ width: 120,
+ sortable: true
+ },
{
columnKey: "name",
- value: T.translate("selection_plan_list.name")
+ header: T.translate("selection_plan_list.name")
},
{
columnKey: "type",
- value: T.translate("selection_plan_list.type")
+ header: T.translate("selection_plan_list.type")
},
{
columnKey: "is_enabled",
- value: T.translate("selection_plan_list.is_enabled")
+ header: T.translate("selection_plan_list.is_enabled")
},
{
columnKey: "is_hidden",
- value: T.translate("selection_plan_list.is_hidden")
+ header: T.translate("selection_plan_list.is_hidden")
}
];
const tableOptions = {
sortCol: order,
- sortDir: orderDir,
- actions: {
- edit: { onClick: handleEdit },
- delete: { onClick: handleDelete }
- }
+ sortDir: orderDir
};
if (!currentSummit.id) return
;
return (
-
- {" "}
- {T.translate("selection_plan_list.selection_plan_list")} (
- {totalSelectionPlans})
-
-
-
-
-
-
-
-
- {T.translate("selection_plan_list.add_selection_plan")}
-
-
-
+
{T.translate("selection_plan_list.selection_plan_list")}
+
+
+
+ {totalSelectionPlans} items
+
+
+
+
+
+
+ }
+ >
+ {T.translate("selection_plan_list.add_selection_plan")}
+
+
+
+
+
{selectionPlans.length === 0 && (
{T.translate("selection_plan_list.no_selection_plans")}
)}
{selectionPlans.length > 0 && (
-
-
+ `${T.translate("selection_plan_list.remove_warning")}${name}?`
+ }
+ confirmButtonColor="error"
/>
)}
+
+ {openSelectionPlanPopup && (
+
+ )}
);
};
const mapStateToProps = ({
currentSummitState,
- currentSelectionPlanListState
+ currentSelectionPlanListState,
+ currentSelectionPlanState
}) => ({
currentSummit: currentSummitState.currentSummit,
- ...currentSelectionPlanListState
+ ...currentSelectionPlanListState,
+ currentSelectionPlan: currentSelectionPlanState.entity
});
export default connect(mapStateToProps, {
getSelectionPlans,
- deleteSelectionPlan
+ getSelectionPlan,
+ resetSelectionPlanForm,
+ getMarketingSettingsBySelectionPlan,
+ deleteSelectionPlan,
+ saveSelectionPlan,
+ saveSelectionPlanSettings
})(SelectionPlanListPage);
diff --git a/src/pages/selection-plans/selection-plan-popup.js b/src/pages/selection-plans/selection-plan-popup.js
new file mode 100644
index 000000000..879b97290
--- /dev/null
+++ b/src/pages/selection-plans/selection-plan-popup.js
@@ -0,0 +1,98 @@
+/**
+ * Copyright 2019 OpenStack Foundation
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * */
+
+import React, { useState } from "react";
+import PropTypes from "prop-types";
+import T from "i18n-react/dist/i18n-react";
+import Button from "@mui/material/Button";
+import Dialog from "@mui/material/Dialog";
+import DialogActions from "@mui/material/DialogActions";
+import DialogContent from "@mui/material/DialogContent";
+import DialogTitle from "@mui/material/DialogTitle";
+import Divider from "@mui/material/Divider";
+import IconButton from "@mui/material/IconButton";
+import CloseIcon from "@mui/icons-material/Close";
+import EditSelectionPlanPage from "./edit-selection-plan-page";
+
+const SelectionPlanPopup = ({ isEditing, onClose, onSave, history }) => {
+ const [isSaving, setIsSaving] = useState(false);
+
+ const handleClose = () => {
+ if (isSaving) return;
+ onClose();
+ };
+
+ const handleSave = (values) => {
+ if (isSaving) return Promise.resolve();
+ setIsSaving(true);
+ return Promise.resolve(onSave(values))
+ .then(() => onClose())
+ .catch(() => {})
+ .finally(() => setIsSaving(false));
+ };
+
+ return (
+
+
+ {isEditing ? T.translate("general.edit") : T.translate("general.add")}{" "}
+ {T.translate("edit_selection_plan.selection_plan")}
+
+
+
+
+
+
+
+
+
+
+
+ {T.translate("general.save")}
+
+
+
+ );
+};
+
+SelectionPlanPopup.propTypes = {
+ isEditing: PropTypes.bool,
+ onClose: PropTypes.func.isRequired,
+ onSave: PropTypes.func.isRequired,
+ history: PropTypes.shape({ push: PropTypes.func }).isRequired
+};
+
+SelectionPlanPopup.defaultProps = {
+ isEditing: false
+};
+
+export default SelectionPlanPopup;
diff --git a/src/pages/sponsors/__tests__/edit-sponsor-page.test.js b/src/pages/sponsors/__tests__/edit-sponsor-page.test.js
index 48af66530..0818b46a7 100644
--- a/src/pages/sponsors/__tests__/edit-sponsor-page.test.js
+++ b/src/pages/sponsors/__tests__/edit-sponsor-page.test.js
@@ -16,6 +16,7 @@ jest.mock(
"../sponsor-page/tabs/sponsor-forms-tab/components/manage-items/sponsor-forms-manage-items.js"
);
jest.mock("../sponsor-page/tabs/sponsor-users-list-per-sponsor/index.js");
+jest.mock("../sponsor-page/tabs/sponsor-cart-tab/index.js", () => () => null);
jest.mock("../../../actions/sponsor-actions", () => ({
...jest.requireActual("../../../actions/sponsor-actions"),
diff --git a/src/reducers/selection_plans/selection-plan-list-reducer.js b/src/reducers/selection_plans/selection-plan-list-reducer.js
index 888fa5783..6f55da1ac 100644
--- a/src/reducers/selection_plans/selection-plan-list-reducer.js
+++ b/src/reducers/selection_plans/selection-plan-list-reducer.js
@@ -39,12 +39,19 @@ const selectionPlanListReducer = (state = DEFAULT_STATE, action) => {
return DEFAULT_STATE;
}
case REQUEST_SELECTION_PLANS: {
- const { order, orderDir } = payload;
+ const { order, orderDir, page, perPage, term } = payload;
- return { ...state, order, orderDir };
+ return {
+ ...state,
+ order,
+ orderDir,
+ currentPage: page,
+ perPage,
+ term
+ };
}
case RECEIVE_SELECTION_PLANS: {
- const { current_page, total, last_page, data } = payload.response;
+ const { total, last_page, current_page, data } = payload.response;
const selectionPlans = data.map((sp) => ({
...sp,