diff --git a/src/pages/marketing/__tests__/marketing-setting-list-page.test.js b/src/pages/marketing/__tests__/marketing-setting-list-page.test.js new file mode 100644 index 000000000..d14a499fb --- /dev/null +++ b/src/pages/marketing/__tests__/marketing-setting-list-page.test.js @@ -0,0 +1,263 @@ +import React from "react"; +import { act, render, 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 MarketingSettingListPage from "../marketing-setting-list-page"; +import { + getMarketingSettings, + deleteSetting, + cloneMarketingSettings +} from "../../../actions/marketing-actions"; +import showConfirmDialog from "../../../components/mui/showConfirmDialog"; +import { DEFAULT_CURRENT_PAGE } from "../../../utils/constants"; + +jest.mock("../../../actions/marketing-actions", () => ({ + getMarketingSettings: jest.fn(), + deleteSetting: jest.fn(), + cloneMarketingSettings: jest.fn() +})); + +jest.mock("../../../components/mui/showConfirmDialog", () => jest.fn()); + +jest.mock("../../../components/summit-dropdown", () => ({ + __esModule: true, + default: ({ onClick }) => ( + + ) +})); + +let capturedColumns; + +jest.mock("openstack-uicore-foundation/lib/components/mui/table", () => ({ + __esModule: true, + default: ({ + onEdit, + onDelete, + onSort, + onPageChange, + onPerPageChange, + columns + }) => { + capturedColumns = columns; + return ( +
+ + + + + +
+ ); + } +})); + +jest.mock( + "openstack-uicore-foundation/lib/components/mui/search-input", + () => ({ + __esModule: true, + default: ({ onSearch }) => ( + + ) + }) +); + +jest.mock("i18n-react/dist/i18n-react", () => ({ + __esModule: true, + default: { translate: (key) => key } +})); + +const mockHistory = { push: jest.fn() }; + +const initialState = { + currentSummitState: { currentSummit: createMockSummit() }, + marketingSettingListState: { + settings: [ + { + id: 1, + key: "test-key", + type: "TEXT", + value: "test-value", + selection_plan_id: "N/A" + } + ], + totalSettings: 1, + perPage: 10, + currentPage: 1, + term: "", + order: "id", + orderDir: 1, + lastPage: 1 + } +}; + +describe("MarketingSettingListPage", () => { + beforeEach(() => { + jest.clearAllMocks(); + getMarketingSettings.mockReturnValue(() => Promise.resolve()); + deleteSetting.mockReturnValue(() => Promise.resolve()); + cloneMarketingSettings.mockReturnValue(() => Promise.resolve()); + showConfirmDialog.mockResolvedValue(true); + }); + + it("bounds the value column width and wraps long values instead of overflowing", () => { + renderWithRedux(, { + initialState + }); + + const valueColumn = capturedColumns.find((c) => c.columnKey === "value"); + expect(valueColumn.width).toBe(450); + + const longValue = "lorem ipsum ".repeat(50); + const { container } = render(valueColumn.render({ value: longValue })); + + expect(container.firstChild).toHaveStyle({ + wordBreak: "break-word", + overflowWrap: "anywhere" + }); + expect(container).toHaveTextContent(longValue.trim()); + }); + + it("deletes the setting by id (confirm is handled inside MuiTable)", async () => { + renderWithRedux(, { + initialState + }); + + await act(async () => { + await userEvent.click(screen.getByRole("button", { name: "delete-row" })); + await flushPromises(); + }); + + expect(deleteSetting).toHaveBeenCalledWith(1); + }); + + it("clones settings when the confirm dialog resolves true", async () => { + renderWithRedux(, { + initialState + }); + + await act(async () => { + await userEvent.click( + screen.getByRole("button", { name: "clone-trigger" }) + ); + await flushPromises(); + }); + + expect(cloneMarketingSettings).toHaveBeenCalledWith(999); + }); + + it("does not clone settings when the confirm dialog resolves false", async () => { + showConfirmDialog.mockResolvedValue(false); + + renderWithRedux(, { + initialState + }); + + await act(async () => { + await userEvent.click( + screen.getByRole("button", { name: "clone-trigger" }) + ); + await flushPromises(); + }); + + expect(cloneMarketingSettings).not.toHaveBeenCalled(); + }); + + it("resets to the first page on search", async () => { + renderWithRedux(, { + initialState + }); + + await act(async () => { + await userEvent.click( + screen.getByRole("button", { name: "search-trigger" }) + ); + }); + + expect(getMarketingSettings).toHaveBeenLastCalledWith( + "newterm", + DEFAULT_CURRENT_PAGE, + 10, + "id", + 1 + ); + }); + + it("resets to the first page on per-page change", async () => { + renderWithRedux(, { + initialState + }); + + await act(async () => { + await userEvent.click(screen.getByRole("button", { name: "perpage-50" })); + }); + + expect(getMarketingSettings).toHaveBeenLastCalledWith( + "", + DEFAULT_CURRENT_PAGE, + 50, + "id", + 1 + ); + }); + + it("keeps the current page on sort", async () => { + renderWithRedux(, { + initialState + }); + + await act(async () => { + await userEvent.click(screen.getByRole("button", { name: "sort-col" })); + }); + + expect(getMarketingSettings).toHaveBeenLastCalledWith("", 1, 10, "key", -1); + }); + + it("navigates to the edit page", async () => { + renderWithRedux(, { + initialState + }); + + await act(async () => { + await userEvent.click(screen.getByRole("button", { name: "edit-row" })); + }); + + expect(mockHistory.push).toHaveBeenCalledWith( + "/app/summits/456/marketing/1" + ); + }); + + it("navigates to the add-setting page", async () => { + renderWithRedux(, { + initialState + }); + + await act(async () => { + await userEvent.click( + screen.getByRole("button", { name: "marketing.add_setting" }) + ); + }); + + expect(mockHistory.push).toHaveBeenCalledWith( + "/app/summits/456/marketing/new" + ); + }); +}); diff --git a/src/pages/marketing/marketing-setting-list-page.js b/src/pages/marketing/marketing-setting-list-page.js index c18f720b4..e3d622255 100644 --- a/src/pages/marketing/marketing-setting-list-page.js +++ b/src/pages/marketing/marketing-setting-list-page.js @@ -11,208 +11,181 @@ * 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 { Pagination } from "react-bootstrap"; -import FreeTextSearch from "openstack-uicore-foundation/lib/components/free-text-search" -import Table from "openstack-uicore-foundation/lib/components/table"; +import Box from "@mui/material/Box"; +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 SummitDropdown from "../../components/summit-dropdown"; +import showConfirmDialog from "../../components/mui/showConfirmDialog"; import { getSummitById } from "../../actions/summit-actions"; import { getMarketingSettings, deleteSetting, cloneMarketingSettings } from "../../actions/marketing-actions"; +import { DEFAULT_CURRENT_PAGE } from "../../utils/constants"; + +const wrapLongText = (value) => ( +
+ {value} +
+); + +const MarketingSettingListPage = ({ + currentSummit, + settings, + currentPage, + perPage, + term, + order, + orderDir, + totalSettings, + history, + getMarketingSettings, + deleteSetting, + cloneMarketingSettings +}) => { + useEffect(() => { + if (currentSummit) { + getMarketingSettings(term, currentPage, perPage, order, orderDir); + } + }, [currentSummit]); -import "../../styles/table.less"; - -class MarketingSettingListPage extends React.Component { - constructor(props) { - super(props); - - this.handleEdit = this.handleEdit.bind(this); - this.handlePageChange = this.handlePageChange.bind(this); - this.handleSort = this.handleSort.bind(this); - this.handleSearch = this.handleSearch.bind(this); - this.handleNewSetting = this.handleNewSetting.bind(this); - this.handleDeleteSetting = this.handleDeleteSetting.bind(this); - this.handleCloneSettings = this.handleCloneSettings.bind(this); + const handleEdit = (row) => { + history.push(`/app/summits/${currentSummit.id}/marketing/${row.id}`); + }; - this.state = {}; - } + const handlePageChange = (page) => { + getMarketingSettings(term, page, perPage, order, orderDir); + }; - componentDidMount() { - const { currentSummit } = this.props; - if (currentSummit) { - this.props.getMarketingSettings(); - } - } - - handleEdit(setting_id) { - const { currentSummit, history } = this.props; - history.push(`/app/summits/${currentSummit.id}/marketing/${setting_id}`); - } - - handlePageChange(page) { - const { term, order, orderDir, perPage } = this.props; - this.props.getMarketingSettings(term, page, perPage, order, orderDir); - } - - handleSort(index, key, dir, func) { - const { term, page, perPage } = this.props; - this.props.getMarketingSettings(term, page, perPage, key, dir); - } - - handleSearch(term) { - const { order, orderDir, page, perPage } = this.props; - this.props.getMarketingSettings(term, page, perPage, order, orderDir); - } - - handleNewSetting(ev) { - const { currentSummit, history } = this.props; - history.push(`/app/summits/${currentSummit.id}/marketing/new`); - } + const handlePerPageChange = (newPerPage) => { + getMarketingSettings( + term, + DEFAULT_CURRENT_PAGE, + newPerPage, + order, + orderDir + ); + }; - handleDeleteSetting(settingId) { - const { deleteSetting, settings } = this.props; - const setting = settings.find((s) => s.id === settingId); + const handleSort = (key, dir) => { + getMarketingSettings(term, currentPage, perPage, key, dir); + }; - Swal.fire({ - title: T.translate("general.are_you_sure"), - text: `${T.translate("marketing.delete_setting_warning")} ${setting.key}`, - type: "warning", - showCancelButton: true, - confirmButtonColor: "#DD6B55", - confirmButtonText: T.translate("general.yes_delete") - }).then((result) => { - if (result.value) { - deleteSetting(settingId); - } - }); - } + const handleSearch = (newTerm) => { + getMarketingSettings( + newTerm, + DEFAULT_CURRENT_PAGE, + perPage, + order, + orderDir + ); + }; - handleCloneSettings(summitId) { - const { cloneMarketingSettings } = this.props; + const handleNewSetting = (ev) => { + ev.preventDefault(); + history.push(`/app/summits/${currentSummit.id}/marketing/new`); + }; - Swal.fire({ + const handleCloneSettings = async (summitId) => { + const confirmed = await showConfirmDialog({ title: T.translate("general.are_you_sure"), text: T.translate("marketing.clone_settings_warning"), - type: "warning", - showCancelButton: true, - confirmButtonColor: "#DD6B55", - confirmButtonText: T.translate("marketing.yes_clone") - }).then((result) => { - if (result.value) { - cloneMarketingSettings(summitId); - } + iconType: "warning", + confirmButtonText: T.translate("marketing.yes_clone"), + confirmButtonColor: "error" }); - } - - render() { - const { - currentSummit, - settings, - lastPage, - currentPage, - term, - order, - orderDir, - totalSettings - } = this.props; - - const columns = [ - { columnKey: "id", value: T.translate("general.id"), sortable: true }, - { columnKey: "key", value: T.translate("marketing.key"), sortable: true }, - { columnKey: "type", value: T.translate("marketing.type") }, - { - columnKey: "value", - value: T.translate("marketing.value"), - title: true - }, - { - columnKey: "selection_plan_id", - value: T.translate("marketing.selection_plan"), - title: true - } - ]; - - const table_options = { - sortCol: order, - sortDir: orderDir, - className: "marketing-table", - actions: { - edit: { onClick: this.handleEdit }, - delete: { onClick: this.handleDeleteSetting } - } - }; - - if (!currentSummit.id) return
; - - return ( -
-

- {" "} - {T.translate("marketing.setting_list")} ({totalSettings}) -

-
-
- -
-
- -
-
- -
-
- {settings.length === 0 && ( -
{T.translate("marketing.no_settings")}
- )} - - {settings.length > 0 && ( -
- - - - )} - - ); - } -} + if (confirmed) cloneMarketingSettings(summitId); + }; + + const columns = [ + { columnKey: "id", header: T.translate("general.id"), sortable: true }, + { + columnKey: "key", + header: T.translate("marketing.key"), + sortable: true + }, + { columnKey: "type", header: T.translate("marketing.type") }, + { + columnKey: "value", + header: T.translate("marketing.value"), + width: 450, + render: (row) => wrapLongText(row.value) + }, + { + columnKey: "selection_plan_id", + header: T.translate("marketing.selection_plan"), + render: (row) => wrapLongText(row.selection_plan_id) + } + ]; + + const tableOptions = { sortCol: order, sortDir: orderDir }; + + if (!currentSummit.id) return
; + + return ( +
+

+ {" "} + {T.translate("marketing.setting_list")} ({totalSettings}) +

+ + + + + + + + {settings.length === 0 && ( +
{T.translate("marketing.no_settings")}
+ )} + + {settings.length > 0 && ( +
+ row.key} + deleteDialogBody={(name) => + `${T.translate("marketing.delete_setting_warning")} ${name}` + } + confirmButtonColor="error" + /> +
+ )} +
+ ); +}; const mapStateToProps = ({ currentSummitState, diff --git a/src/styles/table.less b/src/styles/table.less deleted file mode 100644 index f12c5c459..000000000 --- a/src/styles/table.less +++ /dev/null @@ -1,16 +0,0 @@ -.marketing-table { - td { - max-width: 700px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - - @media (min-width: 768px) { - max-width: 450px; - } - - @media only screen and (max-width: 768px) { - max-width: 150px; - } - } -}