diff --git a/src/actions/email-actions.js b/src/actions/email-actions.js index 2e0799df0..62f54d116 100644 --- a/src/actions/email-actions.js +++ b/src/actions/email-actions.js @@ -352,7 +352,7 @@ export const getSentEmails = createAction(RECEIVE_EMAILS), `${window.EMAIL_API_BASE_URL}/api/v1/mails`, authErrorHandler, - { order, orderDir, term, filters } + { order, orderDir, term, page, perPage, filters } )(params)(dispatch).then(() => { dispatch(stopLoading()); }); diff --git a/src/i18n/en.json b/src/i18n/en.json index 4b290f79f..aa5389d90 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -3276,6 +3276,7 @@ } }, "emails": { + "emails": "Emails", "email_templates": "Email Templates", "sent": "Sent", "templates": "Templates", @@ -3329,7 +3330,9 @@ "email_logs": { "email_logs": "Email Logs", "email_list": "Email List", - "apply_filters": "Apply Filters", + "is_sent_filter": "Is Sent?", + "template_filter": "Template", + "not_sent": "Not Sent", "email_templates": "Email Templates", "subject": "Subject", "from_email": "From Email", @@ -3337,13 +3340,7 @@ "sent_date": "Sent Date", "last_error": "Last Error", "payload": "Payload", - "select_fields": "Show Columns", - "placeholders": { - "select_fields": "Select data to display", - "template": "Filter by Template", - "sent_date_from": "Filter Sent Date from", - "sent_date_to": "Filter Sent Date to" - } + "select_fields": "Show Columns" }, "summitdoc": { "summitdocs": "Event Docs", diff --git a/src/pages/emails/__tests__/email-log-list-page.helpers.test.js b/src/pages/emails/__tests__/email-log-list-page.helpers.test.js new file mode 100644 index 000000000..f6123a591 --- /dev/null +++ b/src/pages/emails/__tests__/email-log-list-page.helpers.test.js @@ -0,0 +1,60 @@ +import { OPERATORS } from "openstack-uicore-foundation/lib/components/mui/grid-filter"; +import { buildEmailFilters } from "../email-log-list-page.helpers"; + +jest.mock("i18n-react/dist/i18n-react", () => ({ + __esModule: true, + default: { translate: (key) => key } +})); + +jest.mock("../../../actions/email-actions", () => ({ + queryTemplates: jest.fn() +})); + +describe("buildEmailFilters", () => { + test("maps AFTER to sent_date_filter[0] and BEFORE to sent_date_filter[1]", () => { + const result = buildEmailFilters([ + { + criteria: "sent_date_filter", + operator: OPERATORS.BEFORE.value, + value: 200 + }, + { + criteria: "sent_date_filter", + operator: OPERATORS.AFTER.value, + value: 100 + } + ]); + + expect(result.sent_date_filter).toEqual([100, 200]); + }); + + test("returns [null, null] when no date criteria is present", () => { + const result = buildEmailFilters([]); + + expect(result.sent_date_filter).toEqual([null, null]); + }); + + test("unwraps template_filter from the async option object", () => { + const result = buildEmailFilters([ + { + criteria: "template_filter", + operator: OPERATORS.IS.value, + value: { value: "welcome-email", label: "welcome-email" } + } + ]); + + expect(result.template_filter).toBe("welcome-email"); + }); + + test("defaults template_filter to an empty string when absent", () => { + const result = buildEmailFilters([]); + + expect(result.template_filter).toBe(""); + }); + + test("defaults is_sent_filter to null when absent", () => { + const result = buildEmailFilters([]); + + expect(result.is_sent_filter).toBeNull(); + }); +}); diff --git a/src/pages/emails/__tests__/email-log-list-page.test.js b/src/pages/emails/__tests__/email-log-list-page.test.js new file mode 100644 index 000000000..ec42f273b --- /dev/null +++ b/src/pages/emails/__tests__/email-log-list-page.test.js @@ -0,0 +1,191 @@ +import React from "react"; +import { screen, act, render } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import "@testing-library/jest-dom"; +import { createStore, combineReducers, applyMiddleware } from "redux"; +import thunk from "redux-thunk"; +import { Provider } from "react-redux"; +import { getRequest } from "openstack-uicore-foundation/lib/utils/actions"; +import EmailLogListPage from "../email-log-list-page"; +import emailLogListReducer from "../../../reducers/emails/email-log-list-reducer"; +import * as methods from "../../../utils/methods"; + +jest.mock("openstack-uicore-foundation/lib/utils/actions", () => ({ + __esModule: true, + ...jest.requireActual("openstack-uicore-foundation/lib/utils/actions"), + getRequest: jest.fn() +})); + +jest.mock("openstack-uicore-foundation/lib/components/mui/grid-filter", () => ({ + __esModule: true, + ...jest.requireActual( + "openstack-uicore-foundation/lib/components/mui/grid-filter" + ), + GridFilter: () => null, + useGridFilter: () => ({ parsedFilter: [], filterValues: [] }) +})); + +jest.mock( + "openstack-uicore-foundation/lib/components/mui/search-input", + () => ({ + __esModule: true, + default: () => null + }) +); + +jest.mock("openstack-uicore-foundation/lib/components/mui/table", () => ({ + __esModule: true, + default: ({ data, columns, onPageChange, onPerPageChange }) => ( +
+ + + {data.map((row) => ( +
+ {columns.map((col) => ( + + {col.render ? col.render(row) : row[col.columnKey]} + + ))} +
+ ))} +
+ ) +})); + +jest.mock("i18n-react/dist/i18n-react", () => ({ + __esModule: true, + default: { translate: (key) => key } +})); + +const buildStore = (emailLogListOverrides) => + createStore( + combineReducers({ + currentSummitState: (state = { currentSummit: { id: 1 } }) => state, + emailLogListState: emailLogListReducer + }), + { + emailLogListState: { + ...emailLogListReducer(undefined, {}), + ...emailLogListOverrides + } + }, + applyMiddleware(thunk) + ); + +describe("SentEmailListPage", () => { + beforeEach(() => { + jest.spyOn(methods, "getAccessTokenSafely").mockResolvedValue("TOKEN"); + getRequest.mockImplementation( + () => () => () => + Promise.resolve({ + response: { total: 0, last_page: 1, current_page: 1, data: [] } + }) + ); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe("Show Columns", () => { + const mockPayloadObject = { + summit_reassign_ticket_till_date: "Monday 1 January 2027 12:00 AM PST", + order_owner_full_name: "Jane Doe", + order_owner_company: "Acme Corp", + order_owner_email: "jane.doe@acme.test", + owner_first_name: "John", + owner_last_name: "Smith", + owner_company: "Acme Corp", + owner_email: "john.smith@acme.test", + owner_full_name: "John Smith", + support_email: "support@example.test", + summit_virtual_site_oauth2_client_id: "abcXYZ123.client", + summit_marketing_site_oauth2_client_id: "abcXYZ123.client", + summit_marketing_site_oauth2_scopes: + "openid profile email offline_access", + summit_id: 99, + summit_name: "Test Summit 2027", + summit_logo: "https://example.test/logo.svg", + summit_virtual_site_url: + "https://idp.example.test/auth/password/set/token?client_id=abcXYZ123.client", + summit_marketing_site_url: + "https://idp.example.test/auth/password/set/token?client_id=abcXYZ123.client", + raw_summit_virtual_site_url: "https://virtual.example.test/a", + raw_summit_marketing_site_url: "https://marketing.example.test", + summit_date: "January 1, 2027", + summit_dates_label: "January 1-3, 2027", + summit_schedule_url: "", + summit_site_url: "https://marketing.example.test/", + registration_link: null, + virtual_event_site_link: "https://virtual.example.test/a", + main_venue_address: "123 Main St, Testville, TS", + summit_marketing_site_url_magic_link: "", + edit_ticket_link: + "https://marketing.example.test/#login=1&email=john.smith@acme.test&BackUrl=/a/my-tickets", + EMAIL_TEMPLATE_GENERIC_BANNER: "https://example.test/banner.png", + EMAIL_TEMPLATE_DRAFT_INSTRUCTIONS_URL: + "

https://docs.example.test/draft

", + EMAIL_TEMPLATE_DRAFT_DUE_DATE: "March 1 - 5", + EMAIL_TEMPLATE_FINAL_DUE_DATE: "April 1", + EMAIL_TEMPLATE_REVIEW_PERIOD: "March 1 - 5, 2027", + EMAIL_TEMPLATE_SPEAKER_PORTAL_URL: "https://speaker.example.test/plans", + EMAIL_TEMPLATE_SPEAKER_ACCEPTED_INTRO: + "

Thank you for your submission.

", + EMAIL_TEMPLATE_SPEAKER_ACCEPTED_NEXT_STEPS: "

Next steps go here.

", + EMAIL_TEMPLATE_GREETING: "Hello", + EMAIL_TEMPLATE_GENERIC_SPEAKER_BANNER: + "https://example.test/speaker-banner.png", + EMAIL_TEMPLATE_TICKET_TOP_GRAPHIC: "https://example.test/ticket-top.jpg", + EMAIL_TEMPLATE_TICKET_BOTTOM_GRAPHIC: + "https://example.test/ticket-bottom.jpg", + EMAIL_TEMPLATE_PRIMARY_COLOR: "#111111", + EMAIL_TEMPLATE_SECONDARY_COLOR: "#eeeeee" + }; + + // The reducer hands the page an already-serialized string, not the object. + const serializedPayload = JSON.stringify(mockPayloadObject); + + it("renders the full serialized payload when Payload is selected in Show Columns", async () => { + const store = buildStore({ + emails: [ + { + id: 1, + template: "welcome-email", + subject: "Welcome", + from_email: "from@test.com", + to_email: "to@test.com", + sent_date: "2020-01-01", + last_error: "N/A", + payload: serializedPayload + } + ], + totalEmails: 1 + }); + + render( + + + + ); + + await act(async () => { + await userEvent.click( + screen.getByRole("combobox", { name: "email_logs.select_fields" }) + ); + }); + await act(async () => { + await userEvent.click( + screen.getByRole("option", { name: "email_logs.payload" }) + ); + }); + + expect(screen.getByTestId("cell-payload")).toHaveTextContent( + serializedPayload + ); + }); + }); +}); diff --git a/src/pages/emails/email-log-list-page.helpers.js b/src/pages/emails/email-log-list-page.helpers.js new file mode 100644 index 000000000..187d77e8f --- /dev/null +++ b/src/pages/emails/email-log-list-page.helpers.js @@ -0,0 +1,82 @@ +/** + * Copyright 2020 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 T from "i18n-react/dist/i18n-react"; +import { OPERATORS } from "openstack-uicore-foundation/lib/components/mui/grid-filter"; +import { queryTemplates } from "../../actions/email-actions"; +import { DATE_FILTER_ARRAY_SIZE } from "../../utils/constants"; + +export const getCriterias = () => [ + { + key: "is_sent_filter", + label: T.translate("email_logs.is_sent_filter"), + operators: [OPERATORS.IS], + values: { + type: "select", + props: { + options: [ + { value: "1", label: T.translate("emails.sent") }, + { value: "0", label: T.translate("email_logs.not_sent") } + ] + } + } + }, + { + key: "sent_date_filter", + label: T.translate("email_logs.sent_date"), + operators: [OPERATORS.BEFORE, OPERATORS.AFTER], + values: { + type: "datetime", + props: { mode: "datetime", timezone: "UTC" } + } + }, + { + key: "template_filter", + label: T.translate("email_logs.template_filter"), + operators: [OPERATORS.IS], + values: { + type: "asyncSelect", + props: { + queryFunction: queryTemplates, + formatOption: (t) => ({ value: t.identifier, label: t.identifier }), + multiple: false + } + }, + customParser: (f) => [`template_filter==${f.value.value}`] + } +]; + +export const buildEmailFilters = (filterValues) => { + const isSentEntry = filterValues.find((f) => f.criteria === "is_sent_filter"); + const afterEntry = filterValues.find( + (f) => + f.criteria === "sent_date_filter" && f.operator === OPERATORS.AFTER.value + ); + const beforeEntry = filterValues.find( + (f) => + f.criteria === "sent_date_filter" && f.operator === OPERATORS.BEFORE.value + ); + const templateEntry = filterValues.find( + (f) => f.criteria === "template_filter" + ); + + const sentDateFilter = Array(DATE_FILTER_ARRAY_SIZE).fill(null); + sentDateFilter[0] = afterEntry?.value ?? null; + sentDateFilter[1] = beforeEntry?.value ?? null; + + return { + is_sent_filter: isSentEntry?.value ?? null, + sent_date_filter: sentDateFilter, + template_filter: templateEntry?.value?.value ?? "" + }; +}; diff --git a/src/pages/emails/email-log-list-page.js b/src/pages/emails/email-log-list-page.js index 417c16571..aa9ec3d91 100644 --- a/src/pages/emails/email-log-list-page.js +++ b/src/pages/emails/email-log-list-page.js @@ -14,64 +14,70 @@ import React, { useEffect, useState } from "react"; import { connect } from "react-redux"; import T from "i18n-react/dist/i18n-react"; -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 Dropdown from "openstack-uicore-foundation/lib/components/inputs/dropdown" -import DateTimePicker from "openstack-uicore-foundation/lib/components/inputs/datetimepicker"; -import { epochToMomentTimeZone } from "openstack-uicore-foundation/lib/utils/methods"; -import { SegmentedControl } from "segmented-control"; -import { getSentEmails } from "../../actions/email-actions"; -import "../../styles/email-logs-page.less"; -import EmailTemplateInput from "../../components/inputs/email-template-input"; import { - DATE_FILTER_ARRAY_SIZE, - DEFAULT_CURRENT_PAGE -} from "../../utils/constants"; + Box, + FormControl, + Grid2, + InputLabel, + MenuItem, + OutlinedInput, + Select +} from "@mui/material"; +import MuiTable from "openstack-uicore-foundation/lib/components/mui/table"; +import SearchInput from "openstack-uicore-foundation/lib/components/mui/search-input"; +import { + GridFilter, + useGridFilter +} from "openstack-uicore-foundation/lib/components/mui/grid-filter"; +import { getSentEmails } from "../../actions/email-actions"; +import { DEFAULT_CURRENT_PAGE } from "../../utils/constants"; +import { buildEmailFilters, getCriterias } from "./email-log-list-page.helpers"; + +const FILTER_ID = "email_log_list"; -const SentEmailListPage = function ({ +const SentEmailListPage = ({ emails, - lastPage, currentPage, term, order, orderDir, totalEmails, - match, perPage, - filters, - getSentEmails, - ...props -}) { - useEffect(() => { - getSentEmails(term, currentPage, perPage, order, orderDir, filters); - }, []); + getSentEmails +}) => { + const { parsedFilter, filterValues } = useGridFilter(FILTER_ID); + const emailFilters = buildEmailFilters(filterValues); - const defaultFilters = { - is_sent_filter: null, - sent_date_filter: Array(DATE_FILTER_ARRAY_SIZE).fill(null), - template_filter: "" - }; + useEffect(() => { + getSentEmails( + term, + DEFAULT_CURRENT_PAGE, + perPage, + order, + orderDir, + emailFilters + ); + }, [parsedFilter.join(",")]); - const [enabledFilters, setEnabledFilters] = useState( - Object.keys(filters).filter((e) => - Array.isArray(filters[e]) - ? filters[e]?.some((e) => e !== null) - : filters[e]?.length > 0 - ) - ); - const [emailFilters, setEmailFilters] = useState({ - ...defaultFilters, - ...filters - }); const [selectedColumns, setSelectedColumns] = useState([]); const handlePageChange = (newPage) => { getSentEmails(term, newPage, perPage, order, orderDir, emailFilters); }; - const handleSort = (index, key, dir, func) => { - getSentEmails(term, currentPage, perPage, key, dir, emailFilters); + const handleSort = (key, dir) => { + getSentEmails(term, DEFAULT_CURRENT_PAGE, perPage, key, dir, emailFilters); + }; + + const handlePerPageChange = (newPerPage) => { + getSentEmails( + term, + DEFAULT_CURRENT_PAGE, + newPerPage, + order, + orderDir, + emailFilters + ); }; const handleSearch = (newTerm) => { @@ -88,93 +94,18 @@ const SentEmailListPage = function ({ const handleDDLSortByLabel = (ddlArray) => ddlArray.sort((a, b) => a.label.localeCompare(b.label)); - const handleFiltersChange = (ev) => { - const { value } = ev.target; - if (value.length < enabledFilters.length) { - if (value.length === 0) { - setEnabledFilters(value); - setEmailFilters(defaultFilters); - } else { - const removedFilter = enabledFilters.filter( - (e) => !value.includes(e) - )[0]; - const defaultValue = Array.isArray(emailFilters[removedFilter]) - ? [] - : ""; - const newEventFilters = { - ...emailFilters, - [removedFilter]: defaultValue - }; - setEnabledFilters(value); - setEmailFilters(newEventFilters); - } - } else { - setEnabledFilters(value); - } - }; - - const handleChangeDateFilter = (ev, lastDate) => { - const { value, id } = ev.target; - const newDateFilter = emailFilters[id]; - - setEmailFilters({ - ...emailFilters, - [id]: lastDate - ? [newDateFilter[0], value.unix()] - : [value.unix(), newDateFilter[1]] - }); - }; - - const handleEmailFilterChange = (ev) => { - const { type, id } = ev.target; - let { value } = ev.target; - if (type === "operatorinput") { - value = Array.isArray(value) - ? value - : `${ev.target.operator}${ev.target.value}`; - if (id === "duration_filter") { - value = Array.isArray(value) - ? value - : `${ev.target.operator}${ev.target.value}`; - } - } - if (type === "mediatypeinput") { - value = { - operator: ev.target.operator, - value: ev.target.value - }; - } - setEmailFilters({ ...emailFilters, [id]: value }); - }; - const handleColumnsChange = (ev) => { const { value } = ev.target; - const newColumns = value; - - setSelectedColumns(newColumns); - }; - - const handleSetSentFilter = (ev) => { - setEmailFilters({ ...emailFilters, is_sent_filter: ev }); - }; - - const handleApplyEmailFilters = () => { - getSentEmails( - term, - DEFAULT_CURRENT_PAGE, - perPage, - order, - orderDir, - emailFilters - ); + setSelectedColumns(value); }; const fieldNames = [ - { columnKey: "last_error", value: "last_error" }, + { columnKey: "last_error", header: "last_error" }, { columnKey: "payload", - value: "payload", - render: (row, data) =>
{data}
+ header: "payload", + width: 300, + render: (row) => row.payload } ]; @@ -183,7 +114,7 @@ const SentEmailListPage = function ({ .map((f2) => { let c = { columnKey: f2.columnKey, - value: T.translate(`email_logs.${f2.value}`), + header: T.translate(`email_logs.${f2.header}`), sortable: f2.sortable }; // optional fields @@ -191,27 +122,27 @@ const SentEmailListPage = function ({ if (f2.hasOwnProperty("render")) c = { ...c, render: f2.render }; + if (f2.hasOwnProperty("width")) c = { ...c, width: f2.width }; + return c; }); let columns = [ - { columnKey: "id", value: T.translate("general.id"), sortable: true }, + { columnKey: "id", header: T.translate("general.id"), sortable: true }, { columnKey: "template", - value: T.translate("email_logs.email_templates"), - styles: { wordBreak: "break-all" }, + header: T.translate("email_logs.email_templates"), sortable: true }, - { columnKey: "subject", value: T.translate("email_logs.subject") }, - { columnKey: "from_email", value: T.translate("email_logs.from_email") }, + { columnKey: "subject", header: T.translate("email_logs.subject") }, + { columnKey: "from_email", header: T.translate("email_logs.from_email") }, { columnKey: "to_email", - value: T.translate("email_logs.to_email"), - styles: { wordBreak: "break-word" } + header: T.translate("email_logs.to_email") }, { columnKey: "sent_date", - value: T.translate("email_logs.sent_date"), + header: T.translate("email_logs.sent_date"), sortable: true } ]; @@ -228,180 +159,91 @@ const SentEmailListPage = function ({ sortDir: orderDir }; - const filters_ddl = [ - { label: "Is Sent?", value: "is_sent_filter" }, - { label: "Sent Date", value: "sent_date_filter" }, - { label: "Template", value: "template_filter" } - ]; - return (
-

- {" "} - {T.translate("email_logs.email_list")} ({totalEmails}) -

-
-
- -
-
-
-
-
- -
-
- -
-
-
- {enabledFilters.includes("is_sent_filter") && ( -
- handleSetSentFilter(newValue)} - style={{ - width: "100%", - height: 40, - color: "#337ab7", - fontSize: "10px" - }} +

{T.translate("email_logs.email_list")}

+ + + + {totalEmails} {T.translate("emails.emails")} + + + + + -
- )} - {enabledFilters.includes("sent_date_filter") && ( - <> -
- handleChangeDateFilter(ev, false)} - timezone="UTC" - value={epochToMomentTimeZone( - emailFilters.sent_date_filter[0], - "UTC" - )} - className="event-list-date-picker" - /> -
-
- handleChangeDateFilter(ev, true)} - timezone="UTC" - value={epochToMomentTimeZone( - emailFilters.sent_date_filter[1], - "UTC" - )} - className="event-list-date-picker" - /> -
- - )} - {enabledFilters.includes("template_filter") && ( -
- -
- )} -
-
-
- - + + + {emails.length === 0 &&
{T.translate("emails.no_emails")}
} {emails.length > 0 && ( - <> -
- - - - + )} ); diff --git a/src/reducers/emails/__tests__/email-log-list-reducer.test.js b/src/reducers/emails/__tests__/email-log-list-reducer.test.js index a928f18a4..5b3c34930 100644 --- a/src/reducers/emails/__tests__/email-log-list-reducer.test.js +++ b/src/reducers/emails/__tests__/email-log-list-reducer.test.js @@ -17,7 +17,7 @@ function createDefaultState() { emails: [], term: "", order: "id", - orderDir: 0, + orderDir: -1, currentPage: 1, lastPage: 1, perPage: 10, diff --git a/src/reducers/emails/email-log-list-reducer.js b/src/reducers/emails/email-log-list-reducer.js index 8c5b6197d..bfcc26bfd 100644 --- a/src/reducers/emails/email-log-list-reducer.js +++ b/src/reducers/emails/email-log-list-reducer.js @@ -19,7 +19,7 @@ const DEFAULT_STATE = { emails: [], term: "", order: "id", - orderDir: 0, + orderDir: -1, currentPage: 1, lastPage: 1, perPage: 10, @@ -34,9 +34,17 @@ const emailLogListReducer = (state = DEFAULT_STATE, action) => { return DEFAULT_STATE; } case REQUEST_EMAILS: { - const { order, orderDir, term, filters } = payload; + const { order, orderDir, term, page, perPage, filters } = payload; - return { ...state, order, orderDir, term, filters }; + return { + ...state, + order, + orderDir, + term, + currentPage: page, + perPage, + filters + }; } case RECEIVE_EMAILS: { let { total, last_page, current_page, data } = payload.response; diff --git a/src/styles/email-logs-page.less b/src/styles/email-logs-page.less deleted file mode 100644 index 3e3ad68c4..000000000 --- a/src/styles/email-logs-page.less +++ /dev/null @@ -1,7 +0,0 @@ -.email-logs-table-wrapper { - overflow: auto; -} - -.email-table-payload { - max-width: 300px; -}