From 4e95f76298b50c906514a43cffc6e168840c93ba Mon Sep 17 00:00:00 2001 From: Santiago Palenque Date: Tue, 15 Sep 2026 16:32:53 -0300 Subject: [PATCH 1/2] chore: migrate scheudle settings to mui --- .../schedule-settings-list-page.test.js | 179 +++++++++++++++ .../schedule-settings-list-page.js | 217 ++++++++---------- 2 files changed, 270 insertions(+), 126 deletions(-) create mode 100644 src/pages/schedule_settings/__tests__/schedule-settings-list-page.test.js diff --git a/src/pages/schedule_settings/__tests__/schedule-settings-list-page.test.js b/src/pages/schedule_settings/__tests__/schedule-settings-list-page.test.js new file mode 100644 index 000000000..0e8954c92 --- /dev/null +++ b/src/pages/schedule_settings/__tests__/schedule-settings-list-page.test.js @@ -0,0 +1,179 @@ +import React from "react"; +import { act, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import "@testing-library/jest-dom"; +import flushPromises from "flush-promises"; +import { renderWithRedux, createMockSummit } from "../../../utils/test-utils"; +import ScheduleSettingsListPage from "../schedule-settings-list-page"; +import { + getAllScheduleSettings, + deleteScheduleSetting, + seedDefaultScheduleSettings +} from "../../../actions/schedule-settings-actions"; + +jest.mock("../../../actions/schedule-settings-actions", () => ({ + getAllScheduleSettings: jest.fn(), + deleteScheduleSetting: jest.fn(), + seedDefaultScheduleSettings: jest.fn() +})); + +jest.mock("openstack-uicore-foundation/lib/components/mui/table", () => ({ + __esModule: true, + default: ({ data, onEdit, onDelete, onSort, canDelete }) => ( +
+ {data.map((row) => ( +
+ {row.key} + + {canDelete(row) && ( + + )} +
+ ))} + +
+ ) +})); + +jest.mock("i18n-react/dist/i18n-react", () => ({ + __esModule: true, + default: { translate: (key) => key } +})); + +const mockHistory = { push: jest.fn() }; + +const scheduleSettings = [ + { id: 1, key: "default-setting", is_default: true }, + { id: 2, key: "custom-setting", is_default: false } +]; + +const initialState = { + currentSummitState: { currentSummit: createMockSummit() }, + scheduleSettingsListState: { + scheduleSettings, + order: "key", + orderDir: 1, + totalScheduleSettings: 2 + } +}; + +describe("ScheduleSettingsListPage", () => { + beforeEach(() => { + jest.clearAllMocks(); + getAllScheduleSettings.mockReturnValue(() => Promise.resolve()); + deleteScheduleSetting.mockReturnValue(() => Promise.resolve()); + seedDefaultScheduleSettings.mockReturnValue(() => Promise.resolve()); + }); + + it("loads schedule settings on mount using the persisted order", () => { + renderWithRedux(, { + initialState + }); + + expect(getAllScheduleSettings).toHaveBeenCalledWith("key", 1); + }); + + it("navigates to the edit route", async () => { + renderWithRedux(, { + initialState + }); + + await userEvent.click(screen.getByRole("button", { name: "edit-2" })); + + expect(mockHistory.push).toHaveBeenCalledWith( + `/app/summits/${createMockSummit().id}/schedule-settings/2` + ); + }); + + it("navigates to the add-setting route", async () => { + renderWithRedux(, { + initialState + }); + + await userEvent.click( + screen.getByRole("button", { + name: "schedule_settings_list.add_schedule_settings" + }) + ); + + expect(mockHistory.push).toHaveBeenCalledWith( + `/app/summits/${createMockSummit().id}/schedule-settings/new` + ); + }); + + it("hides the delete action for the default schedule setting", () => { + renderWithRedux(, { + initialState + }); + + expect( + screen.queryByRole("button", { name: "delete-1" }) + ).not.toBeInTheDocument(); + expect( + screen.getByRole("button", { name: "delete-2" }) + ).toBeInTheDocument(); + }); + + it("deletes a non-default schedule setting by id", async () => { + renderWithRedux(, { + initialState + }); + + await act(async () => { + await userEvent.click(screen.getByRole("button", { name: "delete-2" })); + await flushPromises(); + }); + + expect(deleteScheduleSetting).toHaveBeenCalledWith(2); + }); + + it("keeps sorting wired to getAllScheduleSettings", async () => { + renderWithRedux(, { + initialState + }); + + await userEvent.click(screen.getByRole("button", { name: "sort-col" })); + + expect(getAllScheduleSettings).toHaveBeenCalledWith("key", -1); + }); + + it("seeds default schedule settings", async () => { + renderWithRedux(, { + initialState + }); + + await act(async () => { + await userEvent.click( + screen.getByRole("button", { + name: "schedule_settings_list.seed_defaults" + }) + ); + await flushPromises(); + }); + + expect(seedDefaultScheduleSettings).toHaveBeenCalled(); + }); + + it("shows the empty state when there are no schedule settings", () => { + renderWithRedux(, { + initialState: { + ...initialState, + scheduleSettingsListState: { + ...initialState.scheduleSettingsListState, + scheduleSettings: [], + totalScheduleSettings: 0 + } + } + }); + + expect( + screen.getByText("schedule_settings_list.no_schedule_settings") + ).toBeInTheDocument(); + }); +}); diff --git a/src/pages/schedule_settings/schedule-settings-list-page.js b/src/pages/schedule_settings/schedule-settings-list-page.js index 8b26554e0..e8f2819d1 100644 --- a/src/pages/schedule_settings/schedule-settings-list-page.js +++ b/src/pages/schedule_settings/schedule-settings-list-page.js @@ -9,155 +9,120 @@ * 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 React, { useEffect } from "react"; import { connect } from "react-redux"; import T from "i18n-react/dist/i18n-react"; -import Swal from "sweetalert2"; -import Table from "openstack-uicore-foundation/lib/components/table"; +import Button from "@mui/material/Button"; +import AddIcon from "@mui/icons-material/Add"; +import MuiTable from "openstack-uicore-foundation/lib/components/mui/table"; +import GridToolbar from "../../components/mui/grid-toolbar"; import { getAllScheduleSettings, deleteScheduleSetting, seedDefaultScheduleSettings } from "../../actions/schedule-settings-actions"; -class ScheduleSettingsListPage extends React.Component { - componentDidMount() { - const { currentSummit } = this.props; +const ScheduleSettingsListPage = ({ + currentSummit, + scheduleSettings, + order, + orderDir, + totalScheduleSettings, + history, + getAllScheduleSettings, + deleteScheduleSetting, + seedDefaultScheduleSettings +}) => { + useEffect(() => { if (currentSummit) { - this.props.getAllScheduleSettings(); + getAllScheduleSettings(order, orderDir); } - } + }, [currentSummit]); - handleEdit = (schedule_settings_id) => { - const { currentSummit, history } = this.props; + const handleEdit = (row) => { history.push( - `/app/summits/${currentSummit.id}/schedule-settings/${schedule_settings_id}` + `/app/summits/${currentSummit.id}/schedule-settings/${row.id}` ); }; - handleDelete = (scheduleSettingId) => { - const { deleteScheduleSetting, scheduleSettings } = this.props; - let scheduleSetting = scheduleSettings.find( - (t) => t.id === scheduleSettingId - ); - - Swal.fire({ - title: T.translate("general.are_you_sure"), - text: - T.translate("schedule_settings_list.remove_warning") + - " " + - scheduleSetting.key, - type: "warning", - showCancelButton: true, - confirmButtonColor: "#DD6B55", - confirmButtonText: T.translate("general.yes_delete") - }).then(function (result) { - if (result.value) { - deleteScheduleSetting(scheduleSettingId); - } - }); - }; - - handleSort = (index, key, dir, func) => { - this.props.getAllScheduleSettings(key, dir); - }; - - isNotDefault = (id) => { - const { scheduleSettings } = this.props; - let scheduleSetting = scheduleSettings.find((e) => e.id === id); - - return !scheduleSetting.is_default; + const handleSort = (key, dir) => { + getAllScheduleSettings(key, dir); }; - handleNewScheduleSetting = (ev) => { - const { currentSummit, history } = this.props; + const handleNewScheduleSetting = (ev) => { + ev.preventDefault(); history.push(`/app/summits/${currentSummit.id}/schedule-settings/new`); }; - handleSeedDefaults = () => { - this.props.seedDefaultScheduleSettings(); + const handleSeedDefaults = () => { + seedDefaultScheduleSettings(); }; - render() { - const { - currentSummit, - scheduleSettings, - order, - orderDir, - totalScheduleSettings - } = this.props; - - const columns = [ - { columnKey: "key", value: T.translate("edit_schedule_settings.key") }, - { - columnKey: "is_enabled_str", - value: T.translate("edit_schedule_settings.enabled") - }, - { - columnKey: "is_my_schedule_str", - value: T.translate("edit_schedule_settings.is_my_schedule") - }, - { - columnKey: "is_access_level_str", - value: T.translate("edit_schedule_settings.access_levels_only") - } - ]; - - const table_options = { - sortCol: order, - sortDir: orderDir, - actions: { - edit: { onClick: this.handleEdit }, - delete: { onClick: this.handleDelete, display: this.isNotDefault } - } - }; - - if (!currentSummit.id) return null; - - return ( -
-

- {" "} - {T.translate("schedule_settings_list.schedule_settings")} ( - {totalScheduleSettings}) -

-
-
- - -
-
- - {scheduleSettings.length === 0 && ( -
- {T.translate("schedule_settings_list.no_schedule_settings")} -
- )} - - {scheduleSettings.length > 0 && ( - - )} - - ); - } -} + const columns = [ + { columnKey: "key", header: T.translate("edit_schedule_settings.key") }, + { + columnKey: "is_enabled_str", + header: T.translate("edit_schedule_settings.enabled") + }, + { + columnKey: "is_my_schedule_str", + header: T.translate("edit_schedule_settings.is_my_schedule") + }, + { + columnKey: "is_access_level_str", + header: T.translate("edit_schedule_settings.access_levels_only") + } + ]; + + const tableOptions = { sortCol: order, sortDir: orderDir }; + + if (!currentSummit.id) return null; + + return ( +
+

+ {" "} + {T.translate("schedule_settings_list.schedule_settings")} ( + {totalScheduleSettings}) +

+ + + + + + {scheduleSettings.length === 0 && ( +
{T.translate("schedule_settings_list.no_schedule_settings")}
+ )} + + {scheduleSettings.length > 0 && ( + !row.is_default} + getName={(row) => row.key} + deleteDialogBody={(name) => + `${T.translate("schedule_settings_list.remove_warning")} ${name}` + } + confirmButtonColor="error" + /> + )} +
+ ); +}; const mapStateToProps = ({ currentSummitState, From 2e52b7fc9861d2bb9359d95749380089ea51f054 Mon Sep 17 00:00:00 2001 From: Santiago Palenque Date: Wed, 16 Sep 2026 13:58:03 -0300 Subject: [PATCH 2/2] chore: self review --- src/actions/schedule-settings-actions.js | 104 +++++++++++++---------- 1 file changed, 58 insertions(+), 46 deletions(-) diff --git a/src/actions/schedule-settings-actions.js b/src/actions/schedule-settings-actions.js index 50dde7cbc..e688720de 100644 --- a/src/actions/schedule-settings-actions.js +++ b/src/actions/schedule-settings-actions.js @@ -1,4 +1,4 @@ -/** +/* * * Copyright 2021 OpenStack Foundation * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -9,8 +9,9 @@ * 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 T from "i18n-react"; import { getRequest, createAction, @@ -23,10 +24,9 @@ import { showMessage, showSuccessMessage } from "openstack-uicore-foundation/lib/utils/actions"; -import { getAccessTokenSafely } from "../utils/methods"; +import { getAccessTokenSafely } from "../utils/methods"; import history from "../history"; -import T from "i18n-react"; export const REQUEST_ALL_SCHEDULE_SETTINGS = "REQUEST_ALL_SCHEDULE_SETTINGS"; export const RECEIVE_ALL_SCHEDULE_SETTINGS = "RECEIVE_ALL_SCHEDULE_SETTINGS"; @@ -56,12 +56,11 @@ export const FILTER_TYPES = { }; export const seedDefaultScheduleSettings = () => async (dispatch, getState) => { + dispatch(startLoading()); const { currentSummitState } = getState(); const accessToken = await getAccessTokenSafely(); const { currentSummit } = currentSummitState; - dispatch(startLoading()); - const params = { access_token: accessToken }; @@ -72,20 +71,21 @@ export const seedDefaultScheduleSettings = () => async (dispatch, getState) => { `${window.API_BASE_URL}/api/v1/summits/${currentSummit.id}/schedule-settings/seed`, {}, authErrorHandler - )(params)(dispatch).then(() => { - dispatch(stopLoading()); - }); + )(params)(dispatch) + .finally(() => { + dispatch(stopLoading()); + }) + .catch(() => {}); }; export const getAllScheduleSettings = (order = "key", orderDir = 1) => async (dispatch, getState) => { + dispatch(startLoading()); const { currentSummitState } = getState(); const accessToken = await getAccessTokenSafely(); const { currentSummit } = currentSummitState; - dispatch(startLoading()); - const params = { page: 1, per_page: 100, @@ -95,7 +95,7 @@ export const getAllScheduleSettings = // order if (order != null && orderDir != null) { const orderDirSign = orderDir === 1 ? "+" : "-"; - params["order"] = `${orderDirSign}${order}`; + params.order = `${orderDirSign}${order}`; } return getRequest( @@ -104,17 +104,19 @@ export const getAllScheduleSettings = `${window.API_BASE_URL}/api/v1/summits/${currentSummit.id}/schedule-settings`, authErrorHandler, { order, orderDir } - )(params)(dispatch).then(() => { - dispatch(stopLoading()); - }); + )(params)(dispatch) + .finally(() => { + dispatch(stopLoading()); + }) + .catch(() => {}); }; export const getScheduleSetting = (scheduleSettingId) => async (dispatch, getState) => { + dispatch(startLoading()); const { currentSummitState } = getState(); const { currentSummit } = currentSummitState; const accessToken = await getAccessTokenSafely(); - dispatch(startLoading()); const params = { access_token: accessToken, @@ -127,13 +129,16 @@ export const getScheduleSetting = `${window.API_BASE_URL}/api/v1/summits/${currentSummit.id}/schedule-settings/${scheduleSettingId}`, authErrorHandler, {} - )(params)(dispatch).then(() => { - dispatch(stopLoading()); - }); + )(params)(dispatch) + .finally(() => { + dispatch(stopLoading()); + }) + .catch(() => {}); }; export const deleteScheduleSetting = (scheduleSettingId) => async (dispatch, getState) => { + dispatch(startLoading()); const { currentSummitState } = getState(); const accessToken = await getAccessTokenSafely(); const { currentSummit } = currentSummitState; @@ -148,16 +153,19 @@ export const deleteScheduleSetting = `${window.API_BASE_URL}/api/v1/summits/${currentSummit.id}/schedule-settings/${scheduleSettingId}`, null, authErrorHandler - )(params)(dispatch).then(() => { - dispatch(stopLoading()); - }); + )(params)(dispatch) + .finally(() => { + dispatch(stopLoading()); + }) + .catch(() => {}); }; -export const resetScheduleSettingsForm = () => (dispatch, getState) => { +export const resetScheduleSettingsForm = () => (dispatch) => { dispatch(createAction(RESET_SCHEDULE_SETTINGS_FORM)({})); }; export const saveScheduleSettings = (entity) => async (dispatch, getState) => { + dispatch(startLoading()); const { currentSummitState } = getState(); const accessToken = await getAccessTokenSafely(); const { currentSummit } = currentSummitState; @@ -168,34 +176,38 @@ export const saveScheduleSettings = (entity) => async (dispatch, getState) => { const normalizedEntity = normalizeEntity(entity); - dispatch(startLoading()); - if (entity.id) { - putRequest( + return putRequest( createAction(UPDATE_SCHEDULE_SETTINGS), createAction(SCHEDULE_SETTINGS_UPDATED), `${window.API_BASE_URL}/api/v1/summits/${currentSummit.id}/schedule-settings/${entity.id}`, normalizedEntity, authErrorHandler, entity - )(params)(dispatch).then((payload) => { - dispatch(showSuccessMessage(T.translate("edit_schedule_settings.saved"))); - }); - } else { - const success_message = { - title: T.translate("general.done"), - html: T.translate("edit_schedule_settings.created"), - type: "success" - }; + )(params)(dispatch) + .then(() => { + dispatch( + showSuccessMessage(T.translate("edit_schedule_settings.saved")) + ); + }) + .finally(() => dispatch(stopLoading())); + } - postRequest( - createAction(UPDATE_SCHEDULE_SETTINGS), - createAction(SCHEDULE_SETTINGS_ADDED), - `${window.API_BASE_URL}/api/v1/summits/${currentSummit.id}/schedule-settings/`, - normalizedEntity, - authErrorHandler, - entity - )(params)(dispatch).then((payload) => { + const success_message = { + title: T.translate("general.done"), + html: T.translate("edit_schedule_settings.created"), + type: "success" + }; + + return postRequest( + createAction(UPDATE_SCHEDULE_SETTINGS), + createAction(SCHEDULE_SETTINGS_ADDED), + `${window.API_BASE_URL}/api/v1/summits/${currentSummit.id}/schedule-settings/`, + normalizedEntity, + authErrorHandler, + entity + )(params)(dispatch) + .then((payload) => { dispatch( showMessage(success_message, () => { history.push( @@ -203,8 +215,8 @@ export const saveScheduleSettings = (entity) => async (dispatch, getState) => { ); }) ); - }); - } + }) + .finally(() => dispatch(stopLoading())); }; const normalizeEntity = (entity) => { @@ -217,7 +229,7 @@ const normalizeEntity = (entity) => { is_enabled: f.is_enabled })); normalized.pre_filters = entity.pre_filters.map((pf) => { - let values = pf.values; + let {values} = pf; if (pf.type === FILTER_TYPES.company) { values = values.map((v) => v.id); }