Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/actions/speaker-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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),
Expand Down
20 changes: 20 additions & 0 deletions src/actions/submitter-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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),
Expand Down
8 changes: 6 additions & 2 deletions src/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"member": "Member",
"members": "Members",
"event": "Activity",
"activities": "Activities",
"group": "Group",
"yes": "Yes",
"no": "No",
Expand Down Expand Up @@ -1045,7 +1046,8 @@
"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",
"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"
Expand All @@ -1059,7 +1061,9 @@
"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",
"items_qty_no_activities": "Selected {qty} Submitters"
},
"speaker_attendance_list": {
"speaker_attendance_list": "Speaker Attendance List",
Expand Down
32 changes: 25 additions & 7 deletions src/pages/summit_speakers/summit-speakers-list-page.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -702,8 +702,10 @@ class SummitSpeakersListPage extends React.Component {
order,
orderDir,
totalItems,
totalActivities,
selectedCount,
selectedAll,
excludedItems,
selectionPlanFilter,
trackFilter,
trackGroupFilter,
Expand All @@ -713,6 +715,8 @@ class SummitSpeakersListPage extends React.Component {
currentFlowEvent
} = this.getSubjectProps();

const activitiesCountAccurate = selectedAll && excludedItems.length === 0;

const columns = [
{
columnKey: "full_name",
Expand Down Expand Up @@ -901,7 +905,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")})
</h3>
<div className="row">
<div className="col-md-6">
Expand Down Expand Up @@ -1044,9 +1052,19 @@ class SummitSpeakersListPage extends React.Component {
<div>
<span>
<b>
{T.translate("summit_speakers_list.items_qty", {
qty: selectedCount
})}
{activitiesCountAccurate
? T.translate(
this.state.source === sources.speakers
? "summit_speakers_list.items_qty"
: "summit_submitters_list.items_qty",
{ qty: selectedCount, activitiesQty: totalActivities }
)
: T.translate(
this.state.source === sources.speakers
? "summit_speakers_list.items_qty_no_activities"
: "summit_submitters_list.items_qty_no_activities",
{ qty: selectedCount }
)}
</b>
</span>
<SelectableTable
Expand Down
7 changes: 6 additions & 1 deletion src/reducers/summit_speakers/summit-speakers-list-reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -38,6 +39,7 @@ const DEFAULT_STATE = {
lastPage: 1,
perPage: 10,
totalItems: 0,
totalActivities: 0,
selectedCount: 0,
selectedItems: [],
excludedItems: [],
Expand Down Expand Up @@ -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 };
}
Comment on lines +210 to +212
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot May 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Prevent stale count responses from clobbering current speaker totals.

At Line 211, the reducer accepts whichever count response arrives last. Under rapid filter/search changes, stale responses can overwrite the active query’s totalActivities.

Please gate this update with request context (request id/filter hash/current summit) so only the latest relevant response updates state.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/reducers/summit_speakers/summit-speakers-list-reducer.js` around lines
210 - 212, The RECEIVE_SPEAKERS_ACTIVITIES_COUNT reducer is unconditionally
writing payload.response.count and can be clobbered by stale responses; change
it to only update totalActivities when the incoming response matches the current
request context (e.g., compare payload.requestId or a filterHash and
payload.summitId against the request-tracking fields stored in state such as
state.currentRequestId/state.currentFilterHash/state.currentSummitId). Ensure
the action that triggers the async fetch includes a stable identifier (requestId
or filterHash + summitId) and that the reducer for the request start stores that
identifier on state (e.g., in the handler that sets loading state), then in the
RECEIVE_SPEAKERS_ACTIVITIES_COUNT case only set totalActivities =
payload.response.count if payload.requestId === state.currentRequestId (or
payload.filterHash === state.currentFilterHash && payload.summitId ===
state.currentSummitId); otherwise return state unchanged.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every count request is fired at the exact same moment, with the exact same filter, as the main list request. Any rapid user interaction that could produce stale count responses would produce equally stale main list responses — and those have no guard. Adding a guard to just the new calls isn't solving a real problem that doesn't also exist on the main list, which the rest of the code base accepts without protection. Adding those guards everywhere they are needed would be more appropriately done as a separate project rather than in the context of this PR.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mulldug, that's a fair and well-reasoned point. Since the count request is fired simultaneously with the main list request using the same filter, any stale-response race condition would affect RECEIVE_SPEAKERS_BY_SUMMIT equally — and the existing reducer has no guard there either. Applying the guard selectively only to the new count action would indeed be inconsistent without broader refactoring. I agree this is better addressed as a dedicated follow-up across the codebase rather than piecemeal here.


✏️ Learnings added
Learnt from: mulldug
Repo: fntechgit/summit-admin PR: 921
File: src/reducers/summit_speakers/summit-speakers-list-reducer.js:210-212
Timestamp: 2026-05-12T14:24:56.196Z
Learning: In fntechgit/summit-admin, the existing speaker/submitter list reducers (e.g., `RECEIVE_SPEAKERS_BY_SUMMIT` in `src/reducers/summit_speakers/summit-speakers-list-reducer.js`) have no stale-response guard. The count action (`RECEIVE_SPEAKERS_ACTIVITIES_COUNT`) is dispatched at the same moment and with the same filter as the main list request, so adding a guard to only the count handler would be inconsistent. The team considers adding stale-response guards a separate, broader refactoring project, not in scope for individual feature PRs.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: priscila-moneo
Repo: fntechgit/summit-admin PR: 896
File: src/pages/summits/summit-directory-page.js:220-221
Timestamp: 2026-05-06T22:24:01.344Z
Learning: When using `MuiTable` from `openstack-uicore-foundation/lib/components/mui/table` in this codebase, verify the callback payload types: `onDelete` is called with the primitive row identifier (e.g., `id`/`rowId`), not the full row object, while `onEdit` and `onSelect` receive the full row object. Use `onDelete={(id) => deleteSummit(id)}` (or equivalent) so the delete handler receives only the row ID. Note that the `getName` prop is used by `MuiTable` for displaying the delete dialog, not to transform the `onDelete` argument.

default:
return state;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -38,6 +39,7 @@ const DEFAULT_STATE = {
lastPage: 1,
perPage: 10,
totalItems: 0,
totalActivities: 0,
selectedCount: 0,
selectedItems: [],
excludedItems: [],
Expand Down Expand Up @@ -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 };
}
Comment on lines +198 to +200
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot May 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Guard against stale activity-count responses overwriting current filters.

At Line 199, totalActivities is updated from any arriving response. If users change filters/search quickly, older async responses can land late and replace the latest count, so header totals drift from current results.

Consider attaching query context (e.g., currentSummitId + filter hash/request id) to the count request and ignoring mismatched responses in the reducer.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/reducers/summit_submitters/summit-submitters-list-reducer.js` around
lines 198 - 200, The reducer unconditionally sets totalActivities on
RECEIVE_SUBMITTERS_ACTIVITIES_COUNT allowing stale async responses to overwrite
current totals; modify the flow so the count request includes a query context/id
(e.g., requestId or filterHash and currentSummitId) in the action payload and
store the latestRequestId (or latestFilterHash/currentSummitId) on the reducer
state (e.g., state.latestSubmittersCountRequestId); in the
RECEIVE_SUBMITTERS_ACTIVITIES_COUNT branch, compare payload.requestId (or
payload.filterHash + payload.summitId) with state.latestSubmittersCountRequestId
(or state.currentFilterHash/state.currentSummitId) and only set totalActivities
= payload.response.count when they match, otherwise ignore the action.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every count request is fired at the exact same moment, with the exact same filter, as the main list request. Any rapid user interaction that could produce stale count responses would produce equally stale main list responses — and those have no guard. Adding a guard to just the new calls isn't solving a real problem that doesn't also exist on the main list, which the rest of the code base accepts without protection. Adding those guards everywhere they are needed would be more appropriately done as a separate project rather than in the context of this PR.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mulldug, that's a fair point. Since both the count and list requests are dispatched simultaneously with the same filter, any race condition would affect both equally, and the rest of the codebase doesn't guard the list responses either. Addressing stale-response protection consistently across the board is a reasonable follow-up project rather than a partial fix here. I'll drop this concern.

(ᵔ.ᵔ)
(_/)


✏️ Learnings added
Learnt from: mulldug
Repo: fntechgit/summit-admin PR: 921
File: src/reducers/summit_submitters/summit-submitters-list-reducer.js:198-200
Timestamp: 2026-05-12T14:25:09.422Z
Learning: In `fntechgit/summit-admin`, the `RECEIVE_SUBMITTERS_ACTIVITIES_COUNT` (and analogous `RECEIVE_SPEAKERS_ACTIVITIES_COUNT`) action is always dispatched at the same moment and with the same filter as the corresponding main list request (`RECEIVE_SUBMITTERS_BY_SUMMIT` / `RECEIVE_SPEAKERS_BY_SUMMIT`). The codebase does not guard any of these reducers against stale async responses; adding such guards only to the new count actions would be inconsistent. Codebase-wide stale-response protection is intentionally deferred to a separate future project.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: priscila-moneo
Repo: fntechgit/summit-admin PR: 896
File: src/pages/summits/summit-directory-page.js:220-221
Timestamp: 2026-05-06T22:24:01.344Z
Learning: When using `MuiTable` from `openstack-uicore-foundation/lib/components/mui/table` in this codebase, verify the callback payload types: `onDelete` is called with the primitive row identifier (e.g., `id`/`rowId`), not the full row object, while `onEdit` and `onSelect` receive the full row object. Use `onDelete={(id) => deleteSummit(id)}` (or equivalent) so the delete handler receives only the row ID. Note that the `getName` prop is used by `MuiTable` for displaying the delete dialog, not to transform the `onDelete` argument.

default:
return state;
}
Expand Down
2 changes: 2 additions & 0 deletions src/reducers/summits/current-summit-reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {};

Expand All @@ -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 = {};

Expand Down
2 changes: 1 addition & 1 deletion webpack.dev.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading