From b7dc10dbce435e1e0cc7be318e93312173ba0a9c Mon Sep 17 00:00:00 2001 From: gbutler Date: Thu, 7 May 2026 10:16:28 -0500 Subject: [PATCH] feat(speakers): display unique activities count on speakers/submitters list Add total filtered activities count to the page header alongside the speaker count, and selected activities count above the grid, for both the Speakers and Submitters views. Activity counts are deduplicated across accepted, alternate, and rejected presentations using a Set client-side for selections and a dedicated backend endpoint for the filtered total. Also fix null guards in current-summit-reducer for reg-lite and print-app settings payloads, and switch dev server to http. --- src/actions/speaker-actions.js | 18 +++++++++ src/actions/submitter-actions.js | 20 ++++++++++ src/i18n/en.json | 6 ++- .../summit-speakers-list-page.js | 38 +++++++++++++++---- .../summit-speakers-list-reducer.js | 7 +++- .../summit-submitters-list-reducer.js | 7 +++- .../summits/current-summit-reducer.js | 2 + webpack.dev.js | 2 +- 8 files changed, 88 insertions(+), 12 deletions(-) diff --git a/src/actions/speaker-actions.js b/src/actions/speaker-actions.js index fa8a8b9f9..b6a46c071 100644 --- a/src/actions/speaker-actions.js +++ b/src/actions/speaker-actions.js @@ -90,6 +90,10 @@ export const UNSELECT_ALL_SUMMIT_SPEAKERS = "UNSELECT_ALL_SUMMIT_SPEAKERS"; export const SEND_SPEAKERS_EMAILS = "SEND_SPEAKERS_EMAILS"; export const SET_SPEAKERS_CURRENT_FLOW_EVENT = "SET_SPEAKERS_CURRENT_FLOW_EVENT"; +export const REQUEST_SPEAKERS_ACTIVITIES_COUNT = + "REQUEST_SPEAKERS_ACTIVITIES_COUNT"; +export const RECEIVE_SPEAKERS_ACTIVITIES_COUNT = + "RECEIVE_SPEAKERS_ACTIVITIES_COUNT"; const normalizeEntity = (entity) => { const normalizedEntity = { ...entity }; @@ -882,6 +886,18 @@ const parseFilters = (filters) => { return filter; }; +const getSpeakersActivitiesCount = + (summitId, filter, accessToken) => (dispatch) => { + const params = { access_token: accessToken }; + if (filter.length > 0) params["filter[]"] = filter; + return getRequest( + createAction(REQUEST_SPEAKERS_ACTIVITIES_COUNT), + createAction(RECEIVE_SPEAKERS_ACTIVITIES_COUNT), + `${window.API_BASE_URL}/api/v1/summits/${summitId}/speakers/all/events/count`, + authErrorHandler + )(params)(dispatch); + }; + export const getSpeakersBySummit = ( term = null, @@ -928,6 +944,8 @@ export const getSpeakersBySummit = params.order = `${orderDirSign}${order}`; } + dispatch(getSpeakersActivitiesCount(currentSummit.id, filter, accessToken)); + return getRequest( createAction(REQUEST_SPEAKERS_BY_SUMMIT), createAction(RECEIVE_SPEAKERS_BY_SUMMIT), diff --git a/src/actions/submitter-actions.js b/src/actions/submitter-actions.js index 57da93e08..906be372d 100644 --- a/src/actions/submitter-actions.js +++ b/src/actions/submitter-actions.js @@ -42,11 +42,27 @@ export const UNSELECT_ALL_SUMMIT_SUBMITTERS = "UNSELECT_ALL_SUMMIT_SUBMITTERS"; export const SEND_SUBMITTERS_EMAILS = "SEND_SUBMITTERS_EMAILS"; export const SET_SUBMITTERS_CURRENT_FLOW_EVENT = "SET_SUBMITTERS_CURRENT_FLOW_EVENT"; +export const REQUEST_SUBMITTERS_ACTIVITIES_COUNT = + "REQUEST_SUBMITTERS_ACTIVITIES_COUNT"; +export const RECEIVE_SUBMITTERS_ACTIVITIES_COUNT = + "RECEIVE_SUBMITTERS_ACTIVITIES_COUNT"; export const initSubmittersList = () => async (dispatch) => { dispatch(createAction(INIT_SUBMITTERS_LIST_PARAMS)()); }; +const getSubmittersActivitiesCount = + (summitId, filter, accessToken) => (dispatch) => { + const params = { access_token: accessToken }; + if (filter.length > 0) params["filter[]"] = filter; + return getRequest( + createAction(REQUEST_SUBMITTERS_ACTIVITIES_COUNT), + createAction(RECEIVE_SUBMITTERS_ACTIVITIES_COUNT), + `${window.API_BASE_URL}/api/v1/summits/${summitId}/submitters/all/events/count`, + authErrorHandler + )(params)(dispatch); + }; + export const getSubmittersBySummit = ( term = null, @@ -97,6 +113,10 @@ export const getSubmittersBySummit = params.order = `${orderDirSign}${order}`; } + dispatch( + getSubmittersActivitiesCount(currentSummit.id, filter, accessToken) + ); + return getRequest( createAction(REQUEST_SUBMITTERS_BY_SUMMIT), createAction(RECEIVE_SUBMITTERS_BY_SUMMIT), diff --git a/src/i18n/en.json b/src/i18n/en.json index 9c4f9fd05..f9aa8bb98 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -35,6 +35,7 @@ "member": "Member", "members": "Members", "event": "Activity", + "activities": "Activities", "group": "Group", "yes": "Yes", "no": "No", @@ -1045,7 +1046,7 @@ "send_emails_title": "You are about to send an EMAIL BLAST to selected speakers !", "should_send_copy_2_submitter": "Also send to submitter?", "allows_to_reassign": "Allow to reassign?", - "items_qty": "Selected {qty} Speakers", + "items_qty": "Selected {qty} Speakers | {activitiesQty} Activities", "placeholders": { "search_speakers": "Search by Full Name, Email, Speaker Id, Member Id, Title Or Abstract", "test_recipient": "Optional Test Recipient" @@ -1059,7 +1060,8 @@ "send_emails_title": "You are about to send an EMAIL BLAST to selected submitters !", "resend_done": "Emails sent successfully.", "submitters": "Submitters", - "submitters_no_speakers": "Submitters (no speakers)" + "submitters_no_speakers": "Submitters (no speakers)", + "items_qty": "Selected {qty} Submitters | {activitiesQty} Activities" }, "speaker_attendance_list": { "speaker_attendance_list": "Speaker Attendance List", diff --git a/src/pages/summit_speakers/summit-speakers-list-page.js b/src/pages/summit_speakers/summit-speakers-list-page.js index 24e47df74..9cf5e6bf8 100644 --- a/src/pages/summit_speakers/summit-speakers-list-page.js +++ b/src/pages/summit_speakers/summit-speakers-list-page.js @@ -16,9 +16,9 @@ import { connect } from "react-redux"; import T from "i18n-react/dist/i18n-react"; import Swal from "sweetalert2"; import { Modal, Pagination } from "react-bootstrap"; -import FreeTextSearch from "openstack-uicore-foundation/lib/components/free-text-search" -import SelectableTable from "openstack-uicore-foundation/lib/components/table-selectable" -import Dropdown from "openstack-uicore-foundation/lib/components/inputs/dropdown" +import FreeTextSearch from "openstack-uicore-foundation/lib/components/free-text-search"; +import SelectableTable from "openstack-uicore-foundation/lib/components/table-selectable"; +import Dropdown from "openstack-uicore-foundation/lib/components/inputs/dropdown"; import Input from "openstack-uicore-foundation/lib/components/inputs/text-input"; import SpeakerPromoCodeSpecForm from "../../components/forms/speakers-promo-code-spec-form"; import { @@ -702,8 +702,11 @@ class SummitSpeakersListPage extends React.Component { order, orderDir, totalItems, + totalActivities, selectedCount, selectedAll, + selectedItems, + excludedItems, selectionPlanFilter, trackFilter, trackGroupFilter, @@ -713,6 +716,20 @@ class SummitSpeakersListPage extends React.Component { currentFlowEvent } = this.getSubjectProps(); + const selectedActivities = (() => { + if (selectedAll && excludedItems.length === 0) return totalActivities; + const relevant = selectedAll + ? items.filter((item) => !excludedItems.includes(item.id)) + : items.filter((item) => selectedItems.includes(item.id)); + const ids = new Set(); + relevant.forEach((item) => { + (item.accepted_presentations || []).forEach((p) => ids.add(p.id)); + (item.alternate_presentations || []).forEach((p) => ids.add(p.id)); + (item.rejected_presentations || []).forEach((p) => ids.add(p.id)); + }); + return ids.size; + })(); + const columns = [ { columnKey: "full_name", @@ -901,7 +918,11 @@ class SummitSpeakersListPage extends React.Component { {this.state.source === sources.speakers ? T.translate("summit_speakers_list.summit_speakers_list") : T.translate("summit_submitters_list.summit_submitters_list")}{" "} - ({totalItems}) + ({totalItems}{" "} + {this.state.source === sources.speakers + ? T.translate("summit_speakers_list.speakers") + : T.translate("summit_submitters_list.submitters")}{" "} + | {totalActivities} {T.translate("general.activities")})
@@ -1044,9 +1065,12 @@ class SummitSpeakersListPage extends React.Component {
- {T.translate("summit_speakers_list.items_qty", { - qty: selectedCount - })} + {T.translate( + this.state.source === sources.speakers + ? "summit_speakers_list.items_qty" + : "summit_submitters_list.items_qty", + { qty: selectedCount, activitiesQty: selectedActivities } + )} { case SET_SPEAKERS_CURRENT_FLOW_EVENT: { return { ...state, currentFlowEvent: payload }; } + case RECEIVE_SPEAKERS_ACTIVITIES_COUNT: { + return { ...state, totalActivities: payload.response.count }; + } default: return state; } diff --git a/src/reducers/summit_submitters/summit-submitters-list-reducer.js b/src/reducers/summit_submitters/summit-submitters-list-reducer.js index 330c46f73..daf1253e8 100644 --- a/src/reducers/summit_submitters/summit-submitters-list-reducer.js +++ b/src/reducers/summit_submitters/summit-submitters-list-reducer.js @@ -20,7 +20,8 @@ import { SELECT_ALL_SUMMIT_SUBMITTERS, UNSELECT_ALL_SUMMIT_SUBMITTERS, SEND_SUBMITTERS_EMAILS, - SET_SUBMITTERS_CURRENT_FLOW_EVENT + SET_SUBMITTERS_CURRENT_FLOW_EVENT, + RECEIVE_SUBMITTERS_ACTIVITIES_COUNT } from "../../actions/submitter-actions"; import { @@ -38,6 +39,7 @@ const DEFAULT_STATE = { lastPage: 1, perPage: 10, totalItems: 0, + totalActivities: 0, selectedCount: 0, selectedItems: [], excludedItems: [], @@ -193,6 +195,9 @@ const summitSubmittersListReducer = (state = DEFAULT_STATE, action) => { case SET_SUBMITTERS_CURRENT_FLOW_EVENT: { return { ...state, currentFlowEvent: payload }; } + case RECEIVE_SUBMITTERS_ACTIVITIES_COUNT: { + return { ...state, totalActivities: payload.response.count }; + } default: return state; } diff --git a/src/reducers/summits/current-summit-reducer.js b/src/reducers/summits/current-summit-reducer.js index 3e229b606..dc729f12c 100644 --- a/src/reducers/summits/current-summit-reducer.js +++ b/src/reducers/summits/current-summit-reducer.js @@ -673,6 +673,7 @@ const currentSummitReducer = (state = DEFAULT_STATE, action) => { }; } case RECEIVE_REG_LITE_SETTINGS: { + if (!payload.response) return state; const { data } = payload.response; const reg_lite_marketing_settings = {}; @@ -695,6 +696,7 @@ const currentSummitReducer = (state = DEFAULT_STATE, action) => { return { ...state, reg_lite_marketing_settings: newMarketingSettings }; } case RECEIVE_PRINT_APP_SETTINGS: { + if (!payload.response) return state; const { data } = payload.response; const print_app_marketing_settings = {}; diff --git a/webpack.dev.js b/webpack.dev.js index be1e51682..6380181bb 100644 --- a/webpack.dev.js +++ b/webpack.dev.js @@ -8,7 +8,7 @@ module.exports = merge(common, { devtool: "inline-source-map", devServer: { historyApiFallback: true, - server: { type: "https" } + server: { type: "http" } }, output: { filename: "[name].js",