forked from OpenStackweb/summit-admin
-
Notifications
You must be signed in to change notification settings - Fork 4
fix: migrate email log list page into MUI/uicore components #1044
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
331931a
fix: migrate email log list page into MUI/uicore components
tomrndom fec3705
fix: clean localization provider, adjust async input, add onError for…
tomrndom 2da35c4
fix: remove chip multi select, add ref on tempalte request from coder…
tomrndom 04f0047
fix: replace column header prop
tomrndom f6a3753
fix: change params on handleSort, reset pages on sorting
tomrndom 0941e7e
fix: apply grid filter component, fix sort
tomrndom 86a2727
fix: hide join operatores on email log list page
tomrndom 2577350
fix: adjust filter names, simplify customParser
tomrndom 501a2c0
fix: update uicore version, update reducer, restore action, set col w…
tomrndom b68f33f
fix: remove pagination test on email log list page, keep reducer tests
tomrndom 6f0884a
fix: adjust orderDir to display as default by descending id
tomrndom File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
60 changes: 60 additions & 0 deletions
60
src/pages/emails/__tests__/email-log-list-page.helpers.test.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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(); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 }) => ( | ||
| <div> | ||
| <button type="button" onClick={() => onPerPageChange(50)}> | ||
| set-per-page-50 | ||
| </button> | ||
| <button type="button" onClick={() => onPageChange(2)}> | ||
| go-to-page-2 | ||
| </button> | ||
| {data.map((row) => ( | ||
| <div key={row.id}> | ||
| {columns.map((col) => ( | ||
| <span key={col.columnKey} data-testid={`cell-${col.columnKey}`}> | ||
| {col.render ? col.render(row) : row[col.columnKey]} | ||
| </span> | ||
| ))} | ||
| </div> | ||
| ))} | ||
| </div> | ||
| ) | ||
| })); | ||
|
|
||
| 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: | ||
| "<p><a href=\"https://docs.example.test/draft\">https://docs.example.test/draft</a></p>", | ||
| 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: | ||
| "<p>Thank you for your submission.</p>", | ||
| EMAIL_TEMPLATE_SPEAKER_ACCEPTED_NEXT_STEPS: "<p>Next steps go here.</p>", | ||
| 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( | ||
| <Provider store={store}> | ||
| <EmailLogListPage /> | ||
| </Provider> | ||
| ); | ||
|
|
||
| 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 | ||
| ); | ||
| }); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 ?? "" | ||
| }; | ||
| }; | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@tomrndom
buildEmailFiltersis the entire GridFilter → API mapping and it ships with no test, and the page it feeds has none either.This is pure, dependency-free logic that decides which emails an admin sees, so a wrong mapping fails silently: the grid renders normally and just returns the wrong rows. The load-bearing part is the operator → slot mapping —
AFTERmust land insent_date_filter[0](whichparseFilterssends asfrom_sent_date) andBEFOREin[1](to_sent_date). Swapping them yields a plausible-looking but inverted range that no reviewer would catch by reading the diff.Worth pinning specifically because the match is on operator strings that are not unique in uicore's
OPERATORStable:BEFORE.valueandLESS_OR_EQUAL.valueare both"<=", andAFTER.valueandGREATER_OR_EQUAL.valueare both">=". It's correct today becausesent_date_filteronly offersBEFORE/AFTER, but nothing in the code prevents a future criteria edit from breaking it, and nothing would fail if it did.There is precedent for page-level coverage in this same directory — the sibling page migrated to
MuiTablehassrc/pages/emails/__tests__/email-template-list-page.test.js. Per.claude/rules/summit-admin-testing-patterns.md, connected pages go throughrenderWithReduxfromsrc/utils/test-utils.jsand assert observable output (rendered rows, dispatched action arguments), not internals.Three tests worth adding, each tied to a concrete break:
buildEmailFiltersunit —AFTER → [0]/BEFORE → [1],template_filterunwrapped from the async option object (value.value), and[null, null]when no date criteria is present. Breaks if the operator constants or the slot order are ever swapped.row.payloadwhen selected in Show Columns. This is ~6 lines againstMuiTableand it does catch the empty-cell regression: withrender: (row, data)the value is absent from the DOM, withrender: (row)it is present.onPerPageChange(50)followed byonPageChange(2)should dispatchper_page=50on both calls. Fails today becauseperPagenever returns to the reducer.Run with
yarn test.