From 1e5ead5921229126d5b616c5c2a7d25bedd2e339 Mon Sep 17 00:00:00 2001 From: jmgasper Date: Tue, 4 Aug 2026 16:54:13 +1000 Subject: [PATCH] PM-5780: Keep My Submissions count stable What was broken The My Submissions badge showed the full attempt count on its own tab, then dropped to the latest-only count after visiting the Submissions tab. Root cause Both tabs replace the same Marathon Match submissions state. The badge read the loaded attempts array length, even though the latest-only response carries the member's total submissionCount separately. What was changed Derive a dedicated My Submissions count from submissionCount with a loaded-attempt fallback, pass it through the challenge header, and use it for desktop and mobile badges. Any added/updated tests Added regression coverage for deriving the total from a latest-only response and rendering that total independently of the loaded attempts array. --- .../challenge-detail/Header/index.jsx | 44 +++++++++++++- .../containers/challenge-detail/index.jsx | 59 +++++++++++++++++++ .../Header/TabSelector/index.jsx | 10 +++- .../challenge-detail/Header/index.jsx | 4 ++ .../containers/challenge-detail/index.jsx | 13 ++++ 5 files changed, 127 insertions(+), 3 deletions(-) diff --git a/__tests__/shared/components/challenge-detail/Header/index.jsx b/__tests__/shared/components/challenge-detail/Header/index.jsx index 8ffdb6648..9a51b14d5 100644 --- a/__tests__/shared/components/challenge-detail/Header/index.jsx +++ b/__tests__/shared/components/challenge-detail/Header/index.jsx @@ -2,9 +2,14 @@ import React from 'react'; import Renderer from 'react-test-renderer/shallow'; import Header from 'components/challenge-detail/Header'; +import TabSelector from 'components/challenge-detail/Header/TabSelector'; + +jest.mock('react-responsive', () => ({ + useMediaQuery: () => true, +})); function collectText(node) { - if (typeof node === 'string') { + if (typeof node === 'string' || typeof node === 'number') { return [node]; } @@ -125,3 +130,40 @@ describe('Challenge detail header actions', () => { expect(collectText(output)).toContain('Submit a solution'); }); }); + +describe('Challenge detail tab counts', () => { + test('renders the MM submission total independently of loaded attempts', () => { + const renderer = new Renderer(); + renderer.render( + , + ); + + const text = collectText(renderer.getRenderOutput()); + const mySubmissionsLabelIndex = text.indexOf('My Submissions'); + + expect(text[mySubmissionsLabelIndex + 1]).toBe(3); + }); +}); diff --git a/__tests__/shared/containers/challenge-detail/index.jsx b/__tests__/shared/containers/challenge-detail/index.jsx index 5fef65aa9..59b7497c4 100644 --- a/__tests__/shared/containers/challenge-detail/index.jsx +++ b/__tests__/shared/containers/challenge-detail/index.jsx @@ -4,6 +4,7 @@ import { isGroupedChallenge, isGroupedChallengeAccessError, isWiproRegistrationBlocked, + mapStateToProps, shouldLoginForGroupedChallenge, shouldLoginForGroupedChallengeError, } from 'containers/challenge-detail'; @@ -76,6 +77,64 @@ describe('Challenge detail winners filter', () => { }); }); +describe('Challenge detail My Submissions count', () => { + test('uses the total attempt count when only the latest MM submission is loaded', () => { + const state = { + auth: { + user: { + handle: 'member', + userId: '123', + }, + }, + challenge: { + checkpoints: {}, + details: { + id: 'challenge-id', + registrants: [{ memberHandle: 'member', memberId: '123' }], + submissions: [], + }, + mmSubmissions: { + challengeId: 'challenge-id', + data: [{ + member: 'member', + memberId: '123', + submissionCount: 3, + submissions: [{ submissionId: 'latest-submission' }], + }], + }, + reviewSummations: [], + statisticsData: [], + }, + challengeListing: {}, + domain: {}, + lookup: { + allCountries: [], + reviewTypes: [], + }, + page: { + challengeDetails: { + feedbackOpen: {}, + }, + }, + tcCommunities: { + list: {}, + }, + terms: {}, + topcoderHeader: {}, + }; + + const props = mapStateToProps(state, { + challengesUrl: '/challenges', + match: { + params: { challengeId: 'challenge-id' }, + }, + }); + + expect(props.mySubmissions).toHaveLength(1); + expect(props.mySubmissionsCount).toBe(3); + }); +}); + describe('Challenge detail grouped challenge login guard', () => { beforeEach(() => { document.cookie = 'tc_utm=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/'; diff --git a/src/shared/components/challenge-detail/Header/TabSelector/index.jsx b/src/shared/components/challenge-detail/Header/TabSelector/index.jsx index a02f2177b..6916a4ac9 100644 --- a/src/shared/components/challenge-detail/Header/TabSelector/index.jsx +++ b/src/shared/components/challenge-detail/Header/TabSelector/index.jsx @@ -39,6 +39,7 @@ export default function ChallengeViewSelector(props) { trackLower, hasRegistered, mySubmissions, + mySubmissionsCount, onSort, viewAsTable, } = props; @@ -133,6 +134,9 @@ export default function ChallengeViewSelector(props) { } const numOfSub = numOfSubmissions + (numOfCheckpointSubmissions || 0); + const mySubmissionsBadgeCount = _.isFinite(mySubmissionsCount) + ? mySubmissionsCount + : mySubmissions.length; const forumId = _.get(challenge, 'legacy.forumId') || 0; const discuss = _.get(challenge, 'discussions', []).filter(d => ( _.toLower(d.type) === 'challenge' && !_.isEmpty(d.url) @@ -247,7 +251,7 @@ export default function ChallengeViewSelector(props) { styleName={getSelectorStyle(selectedView, DETAIL_TABS.MY_SUBMISSIONS)} > My Submissions - {mySubmissions.length} + {mySubmissionsBadgeCount} ) : null } @@ -358,7 +362,7 @@ export default function ChallengeViewSelector(props) { { currentSelected === DETAIL_TABS.MY_SUBMISSIONS && hasRegistered && isMM && mySubmissions && ( - {mySubmissions.length} + {mySubmissionsBadgeCount} ) } { @@ -447,6 +451,7 @@ ChallengeViewSelector.defaultProps = { numOfRegistrants: 0, numOfCheckpointSubmissions: 0, numOfSubmissions: 0, + mySubmissionsCount: null, }; ChallengeViewSelector.propTypes = { @@ -477,6 +482,7 @@ ChallengeViewSelector.propTypes = { trackLower: PT.string.isRequired, hasRegistered: PT.bool.isRequired, mySubmissions: PT.arrayOf(PT.shape()).isRequired, + mySubmissionsCount: PT.number, onSort: PT.func.isRequired, viewAsTable: PT.bool.isRequired, }; diff --git a/src/shared/components/challenge-detail/Header/index.jsx b/src/shared/components/challenge-detail/Header/index.jsx index 77c945f40..345ed44c1 100644 --- a/src/shared/components/challenge-detail/Header/index.jsx +++ b/src/shared/components/challenge-detail/Header/index.jsx @@ -63,6 +63,7 @@ export default function ChallengeHeader(props) { isMenuOpened, submissionEnded, mySubmissions, + mySubmissionsCount, openForRegistrationChallenges, onSort, viewAsTable, @@ -571,6 +572,7 @@ export default function ChallengeHeader(props) { hasRegistered={hasRegistered} checkpointCount={checkpointCount} mySubmissions={mySubmissions} + mySubmissionsCount={mySubmissionsCount} onSort={onSort} viewAsTable={viewAsTable} /> @@ -585,6 +587,7 @@ ChallengeHeader.defaultProps = { isMenuOpened: false, hasThriveArticles: false, hasRecommendedChallenges: false, + mySubmissionsCount: null, }; ChallengeHeader.propTypes = { @@ -639,6 +642,7 @@ ChallengeHeader.propTypes = { hasFirstPlacement: PT.bool.isRequired, isMenuOpened: PT.bool, mySubmissions: PT.arrayOf(PT.shape()).isRequired, + mySubmissionsCount: PT.number, openForRegistrationChallenges: PT.shape().isRequired, onSort: PT.func.isRequired, viewAsTable: PT.bool.isRequired, diff --git a/src/shared/containers/challenge-detail/index.jsx b/src/shared/containers/challenge-detail/index.jsx index dce96908b..8cc7594ed 100644 --- a/src/shared/containers/challenge-detail/index.jsx +++ b/src/shared/containers/challenge-detail/index.jsx @@ -564,6 +564,7 @@ class ChallengeDetailPageContainer extends React.Component { // expandedTags, // expandTag, mySubmissions, + mySubmissionsCount, reviewTypes, openForRegistrationChallenges, statisticsData, @@ -710,6 +711,7 @@ class ChallengeDetailPageContainer extends React.Component { isMenuOpened={isMenuOpened} submissionEnded={submissionEnded} mySubmissions={challenge.isRegistered ? mySubmissions : []} + mySubmissionsCount={challenge.isRegistered ? mySubmissionsCount : 0} openForRegistrationChallenges={openForRegistrationChallenges} viewAsTable={viewAsTable && isMM} onSort={(currenctSelected, sort) => { @@ -941,6 +943,7 @@ ChallengeDetailPageContainer.defaultProps = { loadingMMSubmissionsForChallengeId: null, mmSubmissions: [], mySubmissions: [], + mySubmissionsCount: 0, isLoadingSubmissionInformation: false, submissionInformation: null, // prizeMode: 'money-usd', @@ -993,6 +996,7 @@ ChallengeDetailPageContainer.propTypes = { reviewTypes: PT.arrayOf(PT.shape()), reviewSummations: PT.arrayOf(PT.shape()).isRequired, mySubmissions: PT.arrayOf(PT.shape()), + mySubmissionsCount: PT.number, toggleCheckpointFeedback: PT.func.isRequired, unregisterFromChallenge: PT.func.isRequired, unregistering: PT.bool.isRequired, @@ -1269,6 +1273,7 @@ export function mapStateToProps(state, props) { ? buildReviewSummationLookup(reviewSummations) : null; let mySubmissions = []; + let mySubmissionsCount = 0; if (challenge.registrants) { challenge.registrants = challenge.registrants.map(registrant => ({ ...registrant, @@ -1516,6 +1521,12 @@ export function mapStateToProps(state, props) { ...attempt, id: normalizedAttempts.length - index, })); + const submissionCount = _.isNil(submission.submissionCount) + ? null + : Number(submission.submissionCount); + mySubmissionsCount = Number.isFinite(submissionCount) + ? submissionCount + : mySubmissions.length; } return ({ @@ -1528,6 +1539,7 @@ export function mapStateToProps(state, props) { }); } else if (loggedInUserId) { mySubmissions = _.filter(challenge.submissions, s => (`${s.memberId}` === `${loggedInUserId}`)); + mySubmissionsCount = mySubmissions.length; } } const { page: { challengeDetails: { feedbackOpen } } } = state; @@ -1582,6 +1594,7 @@ export function mapStateToProps(state, props) { reviewSummations, allCountries: state.lookup.allCountries, mySubmissions, + mySubmissionsCount, reviewTypes, openForRegistrationChallenges: state.challengeListing.openForRegistrationChallenges, statisticsData,