From 936ce306b1bb4f511e0e33374f2609ad4c495199 Mon Sep 17 00:00:00 2001 From: gbutler Date: Thu, 7 May 2026 10:16:28 -0500 Subject: [PATCH 1/2] 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. --- src/actions/speaker-actions.js | 17 ++++++++++++++++ src/actions/submitter-actions.js | 20 +++++++++++++++++++ src/i18n/en.json | 6 ++++-- .../summit-speakers-list-reducer.js | 7 ++++++- .../summit-submitters-list-reducer.js | 7 ++++++- .../summits/current-summit-reducer.js | 2 ++ webpack.dev.js | 2 +- 7 files changed, 56 insertions(+), 5 deletions(-) diff --git a/src/actions/speaker-actions.js b/src/actions/speaker-actions.js index 22df33669..326438e75 100644 --- a/src/actions/speaker-actions.js +++ b/src/actions/speaker-actions.js @@ -91,6 +91,11 @@ 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 }; @@ -1167,3 +1172,15 @@ export const unselectAllSummitSpeakers = () => (dispatch) => { export const setCurrentFlowEvent = (value) => (dispatch) => { dispatch(createAction(SET_SPEAKERS_CURRENT_FLOW_EVENT)(value)); }; + +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); + }; \ No newline at end of file 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 e73973b47..fbf0bc520 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", @@ -1101,7 +1102,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" @@ -1115,7 +1116,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/reducers/summit_speakers/summit-speakers-list-reducer.js b/src/reducers/summit_speakers/summit-speakers-list-reducer.js index 8b9746723..bfdb2b9db 100644 --- a/src/reducers/summit_speakers/summit-speakers-list-reducer.js +++ b/src/reducers/summit_speakers/summit-speakers-list-reducer.js @@ -20,7 +20,8 @@ import { SELECT_ALL_SUMMIT_SPEAKERS, UNSELECT_ALL_SUMMIT_SPEAKERS, SEND_SPEAKERS_EMAILS, - SET_SPEAKERS_CURRENT_FLOW_EVENT + SET_SPEAKERS_CURRENT_FLOW_EVENT, + RECEIVE_SPEAKERS_ACTIVITIES_COUNT } from "../../actions/speaker-actions"; import { @@ -38,6 +39,7 @@ const DEFAULT_STATE = { lastPage: 1, perPage: 10, totalItems: 0, + totalActivities: 0, selectedCount: 0, selectedItems: [], excludedItems: [], @@ -205,6 +207,9 @@ const summitSpeakersListReducer = (state = DEFAULT_STATE, action = {}) => { 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", From b9413624ff12b4878ecbb1e23d3292e31b1844b4 Mon Sep 17 00:00:00 2001 From: gbutler Date: Tue, 12 May 2026 11:30:16 -0500 Subject: [PATCH 2/2] fix(speakers): only show activities count when selection covers all filtered results Show "Selected N Speakers | M Activities" only when the full filtered set is selected (selectedAll with no exclusions), where totalActivities from the backend is accurate. Fall back to "Selected N Speakers" for partial or cross-page selections where a correct count cannot be determined client-side. --- src/actions/speaker-actions.js | 26 ++++++++++--------- src/i18n/en.json | 4 ++- .../summit-speakers-list-page.js | 15 +++++++---- webpack.dev.js | 2 +- 4 files changed, 28 insertions(+), 19 deletions(-) diff --git a/src/actions/speaker-actions.js b/src/actions/speaker-actions.js index 326438e75..1b25d275c 100644 --- a/src/actions/speaker-actions.js +++ b/src/actions/speaker-actions.js @@ -887,6 +887,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, @@ -933,6 +945,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), @@ -1172,15 +1186,3 @@ export const unselectAllSummitSpeakers = () => (dispatch) => { export const setCurrentFlowEvent = (value) => (dispatch) => { dispatch(createAction(SET_SPEAKERS_CURRENT_FLOW_EVENT)(value)); }; - -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); - }; \ No newline at end of file diff --git a/src/i18n/en.json b/src/i18n/en.json index fbf0bc520..6e45c95ca 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -1103,6 +1103,7 @@ "should_send_copy_2_submitter": "Also send to submitter?", "allows_to_reassign": "Allow to reassign?", "items_qty": "Selected {qty} Speakers | {activitiesQty} Activities", + "items_qty_no_activities": "Selected {qty} Speakers", "placeholders": { "search_speakers": "Search by Full Name, Email, Speaker Id, Member Id, Title Or Abstract", "test_recipient": "Optional Test Recipient" @@ -1117,7 +1118,8 @@ "resend_done": "Emails sent successfully.", "submitters": "Submitters", "submitters_no_speakers": "Submitters (no speakers)", - "items_qty": "Selected {qty} Submitters | {activitiesQty} Activities" + "items_qty": "Selected {qty} Submitters | {activitiesQty} Activities", + "items_qty_no_activities": "Selected {qty} Submitters" }, "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..3d36a1fbf 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 { @@ -710,7 +710,8 @@ class SummitSpeakersListPage extends React.Component { activityTypeFilter, selectionStatusFilter, mediaUploadTypeFilter, - currentFlowEvent + currentFlowEvent, + totalActivities } = this.getSubjectProps(); const columns = [ @@ -901,7 +902,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")})
diff --git a/webpack.dev.js b/webpack.dev.js index 6380181bb..be1e51682 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: "http" } + server: { type: "https" } }, output: { filename: "[name].js",