From 74a439df38c6c966ad26df4e8b1f4094a4dabd27 Mon Sep 17 00:00:00 2001 From: anna Date: Mon, 21 Sep 2026 14:45:43 -0500 Subject: [PATCH 1/7] refactor: change media type filter from an include/exclude select button to a dropdown --- src/actions/media-upload-actions.js | 22 ++- .../__tests__/media-type-filter.test.js | 145 +++++++----------- .../filters/media-type-filter/index.js | 113 ++++---------- .../media-type-filter/index.module.less | 4 - .../inputs/media-upload-type-input.js | 65 -------- 5 files changed, 114 insertions(+), 235 deletions(-) delete mode 100644 src/components/filters/media-type-filter/index.module.less delete mode 100644 src/components/inputs/media-upload-type-input.js diff --git a/src/actions/media-upload-actions.js b/src/actions/media-upload-actions.js index 1695a2652..0d65149a8 100644 --- a/src/actions/media-upload-actions.js +++ b/src/actions/media-upload-actions.js @@ -28,7 +28,11 @@ import { } from "openstack-uicore-foundation/lib/utils/actions"; import debounce from "lodash/debounce"; import { getAccessTokenSafely } from "../utils/methods"; -import { DEBOUNCE_WAIT, DEFAULT_PER_PAGE } from "../utils/constants"; +import { + DEBOUNCE_WAIT, + DEFAULT_PER_PAGE, + MAX_PER_PAGE +} from "../utils/constants"; URI.escapeQuerySpace = false; @@ -142,6 +146,22 @@ export const queryMediaUploads = debounce(async (summitId, input, callback) => { .catch(fetchErrorHandler); }, DEBOUNCE_WAIT); +export const getAllMediaUploadTypes = async (summitId) => { + const accessToken = await getAccessTokenSafely(); + const apiUrl = URI( + `${window.API_BASE_URL}/api/v1/summits/${summitId}/media-upload-types` + ); + + apiUrl.addQuery("access_token", accessToken); + apiUrl.addQuery("order", "name"); + apiUrl.addQuery("per_page", MAX_PER_PAGE); + + return fetch(apiUrl.toString()) + .then(fetchResponseHandler) + .then((json) => json.data) + .catch(fetchErrorHandler); +}; + export const resetMediaUploadForm = () => (dispatch) => { dispatch(createAction(RESET_MEDIA_UPLOAD_FORM)({})); }; diff --git a/src/components/filters/media-type-filter/__tests__/media-type-filter.test.js b/src/components/filters/media-type-filter/__tests__/media-type-filter.test.js index 6fb5e8a3d..6177a22fa 100644 --- a/src/components/filters/media-type-filter/__tests__/media-type-filter.test.js +++ b/src/components/filters/media-type-filter/__tests__/media-type-filter.test.js @@ -1,48 +1,54 @@ import React from "react"; -import { fireEvent, render, screen } from "@testing-library/react"; +import { render, screen, waitFor } from "@testing-library/react"; import MediaTypeFilter from ".."; +import { getAllMediaUploadTypes } from "../../../../actions/media-upload-actions"; jest.mock("i18n-react/dist/i18n-react", () => ({ translate: (key) => key })); -jest.mock("react-select", () => function MockSelect({ id, value, options, onChange }) { - return ( - - ); - }); +jest.mock("../../../../actions/media-upload-actions", () => ({ + getAllMediaUploadTypes: jest.fn() +})); -jest.mock("../../../inputs/media-upload-type-input", () => function MockMediaUploadTypeInput({ id, value, onChange }) { - return ( - onChange({ target: { value: e.target.value } })} - /> - ); - }); +// Mirrors the real Dropdown (react-select) wrapper's isMulti contract: +// value/onChange both deal in a flat array of raw option ids. +jest.mock( + "openstack-uicore-foundation/lib/components/inputs/dropdown", + () => + function MockDropdown({ id, value, options, onChange }) { + return ( + + ); + } +); describe("MediaTypeFilter", () => { + const mediaTypes = [ + { id: "1", name: "Video" }, + { id: "2", name: "Slides" } + ]; + const baseProps = { onChange: jest.fn(), - operatorInitialValue: null, filterInitialValue: null, id: "media-type-filter", summitId: 1 @@ -50,85 +56,52 @@ describe("MediaTypeFilter", () => { beforeEach(() => { jest.clearAllMocks(); + getAllMediaUploadTypes.mockResolvedValue(mediaTypes); }); - test("does not crash when selecting media type before selecting operator", () => { + test("loads the available media types for the given summit", async () => { render(); - expect(() => { - fireEvent.change(screen.getByTestId("media-upload-type-input"), { - target: { value: "video" } - }); - }).not.toThrow(); - - expect(baseProps.onChange).toHaveBeenCalledTimes(1); - expect(baseProps.onChange).toHaveBeenCalledWith({ - target: { - id: "media-type-filter", - value: "video", - type: "mediatypeinput", - operator: null - } - }); + await waitFor(() => expect(getAllMediaUploadTypes).toHaveBeenCalledWith(1)); + expect(screen.getByText("Video")).toBeInTheDocument(); + expect(screen.getByText("Slides")).toBeInTheDocument(); }); - test("keeps existing behavior when selecting operator first and then value", () => { + test("selecting a media type reports it as an inclusive filter", async () => { render(); + await waitFor(() => screen.getByText("Video")); - fireEvent.change(screen.getByTestId("operator-select"), { - target: { value: "has_media_upload_with_type==" } - }); - - fireEvent.change(screen.getByTestId("media-upload-type-input"), { - target: { value: "slides" } - }); + const select = screen.getByTestId("media-type-dropdown"); + select.querySelector("option[value='1']").selected = true; + select.dispatchEvent(new Event("change", { bubbles: true })); - expect(baseProps.onChange).toHaveBeenCalledTimes(2); - expect(baseProps.onChange).toHaveBeenNthCalledWith(1, { - target: { - id: "media-type-filter", - value: null, - type: "mediatypeinput", - operator: "has_media_upload_with_type==" - } - }); - expect(baseProps.onChange).toHaveBeenNthCalledWith(2, { + expect(baseProps.onChange).toHaveBeenCalledWith({ target: { id: "media-type-filter", - value: "slides", + value: [{ id: "1", name: "Video" }], type: "mediatypeinput", operator: "has_media_upload_with_type==" } }); }); - test("does not crash when clearing operator and re-selecting filter value", () => { + test("clearing the selection clears the filter", async () => { render( ); + await waitFor(() => screen.getByText("Video")); - fireEvent.change(screen.getByTestId("media-upload-type-input"), { - target: { value: "video" } - }); + const select = screen.getByTestId("media-type-dropdown"); + select.querySelector("option[value='1']").selected = false; + select.dispatchEvent(new Event("change", { bubbles: true })); - fireEvent.change(screen.getByTestId("operator-select"), { - target: { value: "" } - }); - - expect(() => { - fireEvent.change(screen.getByTestId("media-upload-type-input"), { - target: { value: "slides" } - }); - }).not.toThrow(); - - expect(baseProps.onChange).toHaveBeenCalledTimes(2); - expect(baseProps.onChange).toHaveBeenLastCalledWith({ + expect(baseProps.onChange).toHaveBeenCalledWith({ target: { id: "media-type-filter", - value: "slides", + value: [], type: "mediatypeinput", operator: null } diff --git a/src/components/filters/media-type-filter/index.js b/src/components/filters/media-type-filter/index.js index 6eb6ac395..0d104481a 100644 --- a/src/components/filters/media-type-filter/index.js +++ b/src/components/filters/media-type-filter/index.js @@ -11,99 +11,54 @@ * limitations under the License. * */ -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import T from "i18n-react/dist/i18n-react"; -import Select from "react-select"; -import MediaUploadTypeInput from "../../inputs/media-upload-type-input"; +import Dropdown from "openstack-uicore-foundation/lib/components/inputs/dropdown"; +import { getAllMediaUploadTypes } from "../../../actions/media-upload-actions"; -import styles from "./index.module.less"; +const MediaTypeFilter = ({ onChange, filterInitialValue, id, summitId }) => { + const [mediaTypes, setMediaTypes] = useState([]); -const MediaTypeFilter = ({ - onChange, - operatorInitialValue, - filterInitialValue, - id, - summitId -}) => { - const operatorOptions = [ - { - label: T.translate("media_upload_type_filter.has_media_upload"), - value: "has_media_upload_with_type==" - }, - { - label: T.translate("media_upload_type_filter.has_not_media_upload"), - value: "has_not_media_upload_with_type==" - } - ]; + useEffect(() => { + getAllMediaUploadTypes(summitId).then((types) => + setMediaTypes(types || []) + ); + }, [summitId]); - const [operatorValue, setOperatorValue] = useState( - operatorInitialValue - ? operatorOptions.find((o) => o.value === operatorInitialValue) - : null - ); - const [filterValue, setFilterValue] = useState(filterInitialValue || null); + const options = mediaTypes.map((mediaType) => ({ + label: mediaType.name, + value: mediaType.id + })); - const onChangeOperator = (newOperatorValue) => { - setOperatorValue(newOperatorValue); - if (newOperatorValue?.value == null) return; + const value = (filterInitialValue || []).map((mediaType) => mediaType.id); - const ev = { - target: { - id, - value: filterValue, - type: "mediatypeinput", - operator: newOperatorValue?.value ?? null - } - }; - onChange(ev); - }; + const handleChange = (ev) => { + const selectedIds = ev.target.value || []; + const selectedTypes = mediaTypes.filter((mediaType) => + selectedIds.includes(mediaType.id) + ); - const onChangeFilterValue = (newFilterValue) => { - const { value } = newFilterValue.target; - setFilterValue(value); - const ev = { + onChange({ target: { id, - value, + value: selectedTypes, type: "mediatypeinput", - operator: operatorValue?.value ?? null + operator: + selectedTypes.length > 0 ? "has_media_upload_with_type==" : null } - }; - onChange(ev); + }); }; return ( -
-
- {T.translate("media_upload_type_filter.media_type")} -
-
- +
+
+ mediaType.id)} + onChange={onChangeFilterValue} + options={mediaTypes.map((mediaType) => ({ + label: mediaType.name, + value: mediaType.id + }))} + isClearable + isMulti + placeholder={T.translate( + `media_upload_type_filter.placeholders.${ + operatorValue?.value ?? "has_media_upload_with_type==" + }` + )} + /> +
+
); }; diff --git a/src/components/filters/media-type-filter/index.module.less b/src/components/filters/media-type-filter/index.module.less new file mode 100644 index 000000000..273e828d3 --- /dev/null +++ b/src/components/filters/media-type-filter/index.module.less @@ -0,0 +1,24 @@ +.mediaTypeFilterWrapper { + display: flex; + align-items: center; +} + +/* Push the two controls together into one segmented control. */ +.operatorCol { + padding-right: 0; +} + +.typesCol { + padding-left: 0; +} + +.operatorCol :global(.mediaTypeOperator__control) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.typesCol :global(.mediaTypeValue__control) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + margin-left: -1px; +} diff --git a/src/i18n/en.json b/src/i18n/en.json index 9ae520bd2..32fc9aecc 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -3903,7 +3903,13 @@ "following": " of the following: " }, "media_upload_type_filter": { - "media_type": "Filter By Media Upload Type" + "has_media_upload": "Uploaded", + "has_not_media_upload": "Not uploaded", + "placeholders": { + "has_media_upload_with_type==": "Select media type(s)", + "has_not_media_upload_with_type==": "Select media type(s)", + "operator": "Uploaded or not uploaded" + } }, "edit_reg_feed_metadata": { "reg_feed_metadata": "Registration Feed Metadata", diff --git a/src/pages/summit_speakers/summit-speakers-list-page.js b/src/pages/summit_speakers/summit-speakers-list-page.js index 5c0193faf..614ae8448 100644 --- a/src/pages/summit_speakers/summit-speakers-list-page.js +++ b/src/pages/summit_speakers/summit-speakers-list-page.js @@ -832,6 +832,7 @@ class SummitSpeakersListPage extends React.Component {
Date: Thu, 24 Sep 2026 18:38:06 -0500 Subject: [PATCH 4/7] test: recovered operator tests and included tests for the "has not uploaded" case --- .../__tests__/media-type-filter.test.js | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/src/components/filters/media-type-filter/__tests__/media-type-filter.test.js b/src/components/filters/media-type-filter/__tests__/media-type-filter.test.js index 6177a22fa..7e2b9f9b2 100644 --- a/src/components/filters/media-type-filter/__tests__/media-type-filter.test.js +++ b/src/components/filters/media-type-filter/__tests__/media-type-filter.test.js @@ -1,5 +1,6 @@ import React from "react"; import { render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; import MediaTypeFilter from ".."; import { getAllMediaUploadTypes } from "../../../../actions/media-upload-actions"; @@ -11,6 +12,31 @@ jest.mock("../../../../actions/media-upload-actions", () => ({ getAllMediaUploadTypes: jest.fn() })); +// Mirrors the real react-select single-select contract: the operator control +// receives and emits whole option objects, not raw values. +jest.mock( + "react-select", + () => + function MockSelect({ id, value, options, onChange }) { + return ( + + ); + } +); + // Mirrors the real Dropdown (react-select) wrapper's isMulti contract: // value/onChange both deal in a flat array of raw option ids. jest.mock( @@ -107,4 +133,26 @@ describe("MediaTypeFilter", () => { } }); }); + + test("switching the operator reports the current selection as an exclusion", async () => { + const user = userEvent.setup(); + render(); + await waitFor(() => screen.getByText("Video")); + + await user.selectOptions(screen.getByTestId("media-type-dropdown"), ["1"]); + await user.selectOptions( + screen.getByTestId("operator-select"), + "has_not_media_upload_with_type==" + ); + + expect(baseProps.onChange).toHaveBeenCalledTimes(2); + expect(baseProps.onChange).toHaveBeenLastCalledWith({ + target: { + id: "media-type-filter", + value: [{ id: "1", name: "Video" }], + type: "mediatypeinput", + operator: "has_not_media_upload_with_type==" + } + }); + }); }); From dcdb44245862a658b105032c30e0a4da15d2540a Mon Sep 17 00:00:00 2001 From: anna Date: Fri, 25 Sep 2026 13:36:07 -0500 Subject: [PATCH 5/7] fix: Update tests to use the get all media types thunk. Add tests for using getRequest and fanning out multiple pages --- .../__tests__/media-upload-actions.test.js | 82 ++++++++++++++++++- src/actions/media-upload-actions.js | 55 +++++++++---- .../__tests__/media-type-filter.test.js | 10 +-- .../filters/media-type-filter/index.js | 4 +- .../summit-speakers-list-page.js | 17 ++-- 5 files changed, 139 insertions(+), 29 deletions(-) diff --git a/src/actions/__tests__/media-upload-actions.test.js b/src/actions/__tests__/media-upload-actions.test.js index bf20ef843..b61f1be36 100644 --- a/src/actions/__tests__/media-upload-actions.test.js +++ b/src/actions/__tests__/media-upload-actions.test.js @@ -4,9 +4,16 @@ import configureStore from "redux-mock-store"; import thunk from "redux-thunk"; import flushPromises from "flush-promises"; -import { getRequest } from "openstack-uicore-foundation/lib/utils/actions"; -import { getMediaUpload } from "../media-upload-actions"; +import { + getRequest, + snackbarErrorHandler +} from "openstack-uicore-foundation/lib/utils/actions"; +import { + getAllMediaUploadTypes, + getMediaUpload +} from "../media-upload-actions"; import * as methods from "../../utils/methods"; +import { MAX_PER_PAGE } from "../../utils/constants"; jest.mock("openstack-uicore-foundation/lib/utils/actions", () => ({ __esModule: true, @@ -72,3 +79,74 @@ describe("getMediaUpload", () => { expect(actionTypes).toContain("RECEIVE_MEDIA_UPLOAD"); }); }); + +describe("getAllMediaUploadTypes", () => { + const middlewares = [thunk]; + const mockStore = configureStore(middlewares); + const ENDPOINT = "https://api.test/api/v1/summits/42/media-upload-types"; + + let requestedParams; + + // One row per page, named after its page, so the accumulated list mirrors + // exactly which pages the thunk asked for and in what order it merged them. + const mockPagedRequest = (lastPage) => { + getRequest.mockImplementation(() => (params) => () => { + requestedParams.push(params); + return Promise.resolve({ + response: { + last_page: lastPage, + data: [{ id: params.page, name: `type-${params.page}` }] + } + }); + }); + }; + + beforeEach(() => { + requestedParams = []; + // getRequest is a module-level jest.fn() shared with the suites above; + // restoreAllMocks does not reset its call history. + getRequest.mockClear(); + window.API_BASE_URL = "https://api.test"; + jest.spyOn(methods, "getAccessTokenSafely").mockResolvedValue("TOKEN"); + }); + + afterEach(() => { + jest.restoreAllMocks(); + delete window.API_BASE_URL; + }); + + it("loads every remaining page and concatenates them in page order", async () => { + mockPagedRequest(4); + const store = mockStore({}); + + const result = await store.dispatch(getAllMediaUploadTypes(42)); + + expect(requestedParams.map((p) => p.page).sort((a, b) => a - b)).toEqual([ + 1, 2, 3, 4 + ]); + // Promise.all resolves in input order, so page order survives the fan-out. + expect(result.map((type) => type.name)).toEqual([ + "type-1", + "type-2", + "type-3", + "type-4" + ]); + }); + + it("requests only the fields the filter renders, sorted by name, through getRequest", async () => { + mockPagedRequest(1); + const store = mockStore({}); + + await store.dispatch(getAllMediaUploadTypes(42)); + + expect(requestedParams[0]).toEqual({ + access_token: "TOKEN", + order: "name", + per_page: MAX_PER_PAGE, + fields: "id,name", + page: 1 + }); + expect(getRequest.mock.calls[0][2]).toBe(ENDPOINT); + expect(getRequest.mock.calls[0][3]).toBe(snackbarErrorHandler); + }); +}); diff --git a/src/actions/media-upload-actions.js b/src/actions/media-upload-actions.js index 0d65149a8..53b6e885c 100644 --- a/src/actions/media-upload-actions.js +++ b/src/actions/media-upload-actions.js @@ -12,6 +12,7 @@ * */ import T from "i18n-react/dist/i18n-react"; import URI from "urijs"; +import pLimit from "p-limit"; import { getRequest, putRequest, @@ -27,11 +28,13 @@ import { fetchErrorHandler } from "openstack-uicore-foundation/lib/utils/actions"; import debounce from "lodash/debounce"; -import { getAccessTokenSafely } from "../utils/methods"; +import { getAccessTokenSafely, range } from "../utils/methods"; import { DEBOUNCE_WAIT, DEFAULT_PER_PAGE, - MAX_PER_PAGE + MAX_PER_PAGE, + TEN, + TWO } from "../utils/constants"; URI.escapeQuerySpace = false; @@ -146,20 +149,44 @@ export const queryMediaUploads = debounce(async (summitId, input, callback) => { .catch(fetchErrorHandler); }, DEBOUNCE_WAIT); -export const getAllMediaUploadTypes = async (summitId) => { +export const getAllMediaUploadTypes = (summitId) => async (dispatch) => { const accessToken = await getAccessTokenSafely(); - const apiUrl = URI( - `${window.API_BASE_URL}/api/v1/summits/${summitId}/media-upload-types` + const endpoint = `${window.API_BASE_URL}/api/v1/summits/${summitId}/media-upload-types`; + const baseParams = { + access_token: accessToken, + order: "name", + per_page: MAX_PER_PAGE, + fields: "id,name" + }; + const getPage = (page) => + getRequest( + createAction("DUMMY"), + createAction("DUMMY"), + endpoint, + snackbarErrorHandler + )({ ...baseParams, page })(dispatch); + const limit = pLimit(TEN); + + return ( + getPage(1) + .then(({ response }) => { + const { last_page: lastPage, data: firstPageData } = response; + if (lastPage <= 1) return firstPageData; + // local range() is stop-INCLUSIVE: range(TWO, lastPage, 1) === [2..lastPage] + return Promise.all( + range(TWO, lastPage, 1).map((page) => limit(() => getPage(page))) + ).then((responses) => { + // Promise.all preserves input order -> page-order accumulation. + const accumulated = [...firstPageData]; + responses.forEach(({ response: pageResponse }) => { + accumulated.push(...pageResponse.data); + }); + return accumulated; + }); + }) + // Swallow the rejection since message is already shown by error handler + .catch(() => []) ); - - apiUrl.addQuery("access_token", accessToken); - apiUrl.addQuery("order", "name"); - apiUrl.addQuery("per_page", MAX_PER_PAGE); - - return fetch(apiUrl.toString()) - .then(fetchResponseHandler) - .then((json) => json.data) - .catch(fetchErrorHandler); }; export const resetMediaUploadForm = () => (dispatch) => { diff --git a/src/components/filters/media-type-filter/__tests__/media-type-filter.test.js b/src/components/filters/media-type-filter/__tests__/media-type-filter.test.js index 7e2b9f9b2..e2e8accec 100644 --- a/src/components/filters/media-type-filter/__tests__/media-type-filter.test.js +++ b/src/components/filters/media-type-filter/__tests__/media-type-filter.test.js @@ -2,16 +2,11 @@ import React from "react"; import { render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import MediaTypeFilter from ".."; -import { getAllMediaUploadTypes } from "../../../../actions/media-upload-actions"; jest.mock("i18n-react/dist/i18n-react", () => ({ translate: (key) => key })); -jest.mock("../../../../actions/media-upload-actions", () => ({ - getAllMediaUploadTypes: jest.fn() -})); - // Mirrors the real react-select single-select contract: the operator control // receives and emits whole option objects, not raw values. jest.mock( @@ -73,11 +68,14 @@ describe("MediaTypeFilter", () => { { id: "2", name: "Slides" } ]; + const getAllMediaUploadTypes = jest.fn(); + const baseProps = { onChange: jest.fn(), filterInitialValue: null, id: "media-type-filter", - summitId: 1 + summitId: 1, + getAllMediaUploadTypes }; beforeEach(() => { diff --git a/src/components/filters/media-type-filter/index.js b/src/components/filters/media-type-filter/index.js index 7bfe923a3..12d0c2a31 100644 --- a/src/components/filters/media-type-filter/index.js +++ b/src/components/filters/media-type-filter/index.js @@ -15,7 +15,6 @@ import React, { useEffect, useState } from "react"; import T from "i18n-react/dist/i18n-react"; import Select from "react-select"; import Dropdown from "openstack-uicore-foundation/lib/components/inputs/dropdown"; -import { getAllMediaUploadTypes } from "../../../actions/media-upload-actions"; import styles from "./index.module.less"; @@ -24,7 +23,8 @@ const MediaTypeFilter = ({ operatorInitialValue, filterInitialValue, id, - summitId + summitId, + getAllMediaUploadTypes }) => { const [mediaTypes, setMediaTypes] = useState([]); diff --git a/src/pages/summit_speakers/summit-speakers-list-page.js b/src/pages/summit_speakers/summit-speakers-list-page.js index 614ae8448..b52459898 100644 --- a/src/pages/summit_speakers/summit-speakers-list-page.js +++ b/src/pages/summit_speakers/summit-speakers-list-page.js @@ -55,6 +55,7 @@ import { AUTO_GENERATED_SPEAKERS_PROMO_CODE, AUTO_GENERATED_SPEAKERS_DISCOUNT_CODE } from "../../actions/promocode-actions"; +import { getAllMediaUploadTypes } from "../../actions/media-upload-actions"; import { DEFAULT_CURRENT_PAGE, @@ -374,13 +375,13 @@ class SummitSpeakersListPage extends React.Component { ev.stopPropagation(); ev.preventDefault(); const { currentPromocodeSpecification } = this.props; - const { promoCodeStrategy, testRecipient, source } = this.state; + const { promoCodeStrategy, testRecipient, source } = this.state; const isSpeakerMode = source === sources.speakers; const excerptRecipient = this.ingestEmailRef.value; const shouldSendCopy2Submitter = - isSpeakerMode && this.shouldSendCopy2SubmitterRef.checked; + isSpeakerMode && this.shouldSendCopy2SubmitterRef.checked; const shouldResend = this.shouldResendRef.checked; - const { term } = this.getSubjectProps(); + const { term } = this.getSubjectProps(); this.props.validateSpecs( promoCodeStrategy, @@ -502,7 +503,11 @@ class SummitSpeakersListPage extends React.Component { } render() { - const { currentSummit, currentPromocodeSpecification } = this.props; + const { + currentSummit, + currentPromocodeSpecification, + getAllMediaUploadTypes + } = this.props; const { testRecipient, source, promoCodeStrategy } = this.state; @@ -836,6 +841,7 @@ class SummitSpeakersListPage extends React.Component { filterInitialValue={mediaUploadTypeFilter.value} summitId={currentSummit.id} onChange={this.handleChangeMediaUploadTypeFilter} + getAllMediaUploadTypes={getAllMediaUploadTypes} />
@@ -1099,5 +1105,6 @@ export default connect(mapStateToProps, { setCurrentSubmitterFlowEvent, sendSubmitterEmails, validateSpecs, - resetPromoCodeSpecForm + resetPromoCodeSpecForm, + getAllMediaUploadTypes })(SummitSpeakersListPage); From 5d41022d4a6f6a8b458440650eaf28869290eaab Mon Sep 17 00:00:00 2001 From: anna Date: Fri, 25 Sep 2026 14:10:03 -0500 Subject: [PATCH 6/7] fix: email label updates depending on speaker or submitter filter --- src/i18n/en.json | 1 + src/pages/summit_speakers/summit-speakers-list-page.js | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/i18n/en.json b/src/i18n/en.json index 32fc9aecc..8b8292ad6 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -1185,6 +1185,7 @@ "select_items": "Select at least one submitter to perform the action.", "send_emails_title": "You are about to send an EMAIL BLAST to selected submitters !", "resend_done": "Emails sent successfully.", + "email_section_title": "Email Submitters", "submitters": "Submitters", "submitters_no_speakers": "Submitters (no speakers)", "items_qty": "Selected {qty} Submitters | {activitiesQty} Activities" diff --git a/src/pages/summit_speakers/summit-speakers-list-page.js b/src/pages/summit_speakers/summit-speakers-list-page.js index b52459898..374621d82 100644 --- a/src/pages/summit_speakers/summit-speakers-list-page.js +++ b/src/pages/summit_speakers/summit-speakers-list-page.js @@ -847,7 +847,11 @@ class SummitSpeakersListPage extends React.Component {
-

{T.translate("summit_speakers_list.email_section_title")}

+

+ {source === sources.speakers + ? T.translate("summit_speakers_list.email_section_title") + : T.translate("summit_submitters_list.email_section_title")} +

Date: Fri, 25 Sep 2026 14:33:06 -0500 Subject: [PATCH 7/7] fix: render media upload types as text in the filter dropdown --- src/components/filters/media-type-filter/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/filters/media-type-filter/index.js b/src/components/filters/media-type-filter/index.js index 12d0c2a31..5df01c736 100644 --- a/src/components/filters/media-type-filter/index.js +++ b/src/components/filters/media-type-filter/index.js @@ -108,6 +108,7 @@ const MediaTypeFilter = ({ label: mediaType.name, value: mediaType.id }))} + formatOptionLabel={(option) => option.label} isClearable isMulti placeholder={T.translate(