item.email}
+ deleteDialogBody={(email) =>
+ `${T.translate(
+ "edit_selection_plan.delete_confirm.allowed_member"
+ )} ${email}`
}
- placeholder={T.translate(
- "edit_selection_plan.placeholders.cfp_presentation_edition_default_tab"
- )}
- onChange={this.handleChange}
- options={DEFAULT_CFP_PRESENTATION_EDITION_TABS}
- isMulti={false}
- isClearable
/>
-
+
+
+ handleAllowedMembersPageChange(page)
+ }
+ showFirstButton
+ showLastButton
+ />
+
+
-
-
-
-
-
-
- {window.CFP_APP_BASE_URL && (
-
-
-
+
+ handleOnSwitchChange(
+ "cfp_presentation_summary_hide_track_selection",
+ ev.target.checked
+ )
+ }
+ />
+
+
-
- {`${window.CFP_APP_BASE_URL}/app/${currentSummit.slug}/all-plans`}
-
-
-
- )}
-
+
+ handleOnSwitchChange(
+ "cfp_presentation_summary_hide_activity_type_selection",
+ ev.target.checked
+ )
+ }
+ />
+
+ {window.CFP_APP_BASE_URL && (
+ <>
+
+
+
+
+ {`${window.CFP_APP_BASE_URL}/app/${currentSummit.slug}/all-plans/${formik.values.id}`}
+
+
+
+
+
+
+ {`${window.CFP_APP_BASE_URL}/app/${currentSummit.slug}/all-plans`}
+
+
+ >
+ )}
+
+
+
>
)}
-
-
this.setState({ showImportModal: false })}
- onIngest={this.handleImportAllowedMembers}
+ onHide={() => setShowImportModal(false)}
+ onIngest={handleImportAllowedMembers}
>
* email ( text )
-
- );
- }
-}
+
+
+ );
+};
export default SelectionPlanForm;
diff --git a/src/components/inputs/import-modal/index.jsx b/src/components/inputs/import-modal/index.jsx
index c2f8a3a4d..210ca514e 100644
--- a/src/components/inputs/import-modal/index.jsx
+++ b/src/components/inputs/import-modal/index.jsx
@@ -1,7 +1,7 @@
import React, { useState } from "react";
import { Modal } from "react-bootstrap";
import T from "i18n-react";
-import { UploadInput } from "openstack-uicore-foundation/lib/components";
+import UploadInput from "openstack-uicore-foundation/lib/components/inputs/upload-input";
export default ({ title, children, show, wrapperClass, onHide, onIngest }) => {
const [importFile, setImportFile] = useState(null);
diff --git a/src/i18n/en.json b/src/i18n/en.json
index eda7913d2..afcbd0660 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -556,12 +556,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...",
+ "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()
+}));
+
+jest.mock("../../../actions/marketing-actions", () => ({
+ getMarketingSettingsBySelectionPlan: jest.fn()
+}));
+
+jest.mock("openstack-uicore-foundation/lib/components/mui/table", () => ({
+ __esModule: true,
+ default: ({ onEdit, onDelete }) => (
+
+
+
+
+ )
+}));
+
+jest.mock(
+ "openstack-uicore-foundation/lib/components/mui/search-input",
+ () => ({
+ __esModule: true,
+ default: () =>
+ })
+);
+
+jest.mock("../edit-selection-plan-page", () => ({
+ __esModule: true,
+ default: ({ onSaved, onSavingChange }) => (
+
+
+
+
+ )
+}));
+
+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()
+ );
+ });
+
+ 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: handleSelectionPlanSaved → 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 .finally()
+ expect(getSelectionPlans).toHaveBeenCalledTimes(2);
+ });
+
+ it("re-syncs 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();
+ });
+
+ // Call 1: useEffect on mount; call 2: handleDelete .finally() fires even on rejection
+ expect(getSelectionPlans).toHaveBeenCalledTimes(2);
+ });
+});
diff --git a/src/pages/selection-plans/edit-selection-plan-page.js b/src/pages/selection-plans/edit-selection-plan-page.js
index 07d37b595..f03003e02 100644
--- a/src/pages/selection-plans/edit-selection-plan-page.js
+++ b/src/pages/selection-plans/edit-selection-plan-page.js
@@ -35,13 +35,14 @@ import {
updateRatingTypeOrder,
updateSelectionPlanExtraQuestionOrder
} from "../../actions/selection-plan-actions";
-import AddNewButton from "../../components/buttons/add-new-button";
const EditSelectionPlanPage = ({
currentSummit,
entity,
allowedMembers,
errors,
+ onSaved,
+ onSavingChange,
history,
extraQuestionsOrder,
extraQuestionsOrderDir,
@@ -64,10 +65,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 +181,40 @@ const EditSelectionPlanPage = ({
};
return (
-
-
- {title} {T.translate("edit_selection_plan.selection_plan")}
-
-
-
-
-
+
);
};
diff --git a/src/pages/selection-plans/selection-plan-list-page.js b/src/pages/selection-plans/selection-plan-list-page.js
index 349ac5218..28669b2a3 100644
--- a/src/pages/selection-plans/selection-plan-list-page.js
+++ b/src/pages/selection-plans/selection-plan-list-page.js
@@ -11,169 +11,240 @@
* 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
} 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,
+ resetSelectionPlanForm,
+ getMarketingSettingsBySelectionPlan,
deleteSelectionPlan
}) => {
+ 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 = (selectionPlan) => {
+ if (!selectionPlan?.id) return;
+
+ deleteSelectionPlan(selectionPlan.id)
+ .finally(() => 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 handleSelectionPlanSaved = () => {
+ setOpenSelectionPlanPopup(false);
+ refreshSelectionPlans();
};
- const handlePageChange = (newPage) => {
- getSelectionPlans(term, newPage, order, orderDir);
+ const handleSort = (key, dir) => {
+ getSelectionPlans(term, currentPage, perPage, key, dir);
+ };
+
+ 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.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 && (
)}
+
+ {openSelectionPlanPopup && (
+
+ )}
);
};
const mapStateToProps = ({
currentSummitState,
- currentSelectionPlanListState
+ currentSelectionPlanListState,
+ currentSelectionPlanState
}) => ({
currentSummit: currentSummitState.currentSummit,
- ...currentSelectionPlanListState
+ ...currentSelectionPlanListState,
+ currentSelectionPlan: currentSelectionPlanState.entity
});
export default connect(mapStateToProps, {
getSelectionPlans,
+ getSelectionPlan,
+ resetSelectionPlanForm,
+ getMarketingSettingsBySelectionPlan,
deleteSelectionPlan
})(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..16c1bded9
--- /dev/null
+++ b/src/pages/selection-plans/selection-plan-popup.js
@@ -0,0 +1,82 @@
+/**
+ * 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, { useRef, useState } from "react";
+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, onSaved, history }) => {
+ const [isSaving, setIsSaving] = useState(false);
+ const isSavingRef = useRef(false);
+
+ const handleSavingChange = (saving) => {
+ isSavingRef.current = saving;
+ setIsSaving(saving);
+ };
+
+ const handleClose = () => {
+ if (isSavingRef.current) return;
+ onClose();
+ };
+
+ return (
+
+ );
+};
+
+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..8118e9a42 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, data } = payload.response;
const selectionPlans = data.map((sp) => ({
...sp,
@@ -56,7 +63,6 @@ const selectionPlanListReducer = (state = DEFAULT_STATE, action) => {
...state,
selectionPlans,
totalSelectionPlans: total,
- currentPage: current_page,
lastPage: last_page
};
}