diff --git a/.circleci/config.yml b/.circleci/config.yml index 4bcd34dfe..40e916d30 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -179,8 +179,7 @@ workflows: branches: only: - develop - - PM-3087_virus-scan-fix - - PM-3541_home-points-challenge + - PM-3686_group-submissions-in-challenge-details - "build-prod": context: org-global diff --git a/src/shared/components/challenge-detail/Submissions/SubmissionRow/SubmissionHistoryRow/index.jsx b/src/shared/components/challenge-detail/Submissions/SubmissionRow/SubmissionHistoryRow/index.jsx index 54b7289dd..27a15a6f8 100644 --- a/src/shared/components/challenge-detail/Submissions/SubmissionRow/SubmissionHistoryRow/index.jsx +++ b/src/shared/components/challenge-detail/Submissions/SubmissionRow/SubmissionHistoryRow/index.jsx @@ -26,6 +26,7 @@ export default function SubmissionHistoryRow({ finalScore, provisionalScore, submissionTime, + createdAt, isReviewPhaseComplete, status, challengeStatus, @@ -42,9 +43,11 @@ export default function SubmissionHistoryRow({ }; const provisionalScoreValue = parseScore(provisionalScore); const finalScoreValue = parseScore(finalScore); - const submissionMoment = submissionTime ? moment(submissionTime) : null; + + const timeField = isMM ? submissionTime : createdAt; + const submissionMoment = timeField ? moment(timeField) : null; const submissionTimeDisplay = submissionMoment - ? `${submissionMoment.format('DD MMM YYYY')} ${submissionMoment.format('HH:mm:ss')}` + ? submissionMoment.format('MMM DD, YYYY HH:mm') : 'N/A'; const getInitialReviewResult = () => { if (status === 'failed') return ; @@ -85,13 +88,17 @@ export default function SubmissionHistoryRow({ {getFinalScore()} -
-
PROVISIONAL SCORE
-
- {getInitialReviewResult()} -
-
-
+ { + isMM && ( +
+
PROVISIONAL SCORE
+
+ {getInitialReviewResult()} +
+
+ ) + } +
TIME
{submissionTimeDisplay} @@ -134,6 +141,8 @@ SubmissionHistoryRow.defaultProps = { provisionalScore: null, isReviewPhaseComplete: false, isLoggedIn: false, + createdAt: null, + submissionTime: null, }; SubmissionHistoryRow.propTypes = { @@ -154,7 +163,11 @@ SubmissionHistoryRow.propTypes = { submissionTime: PT.oneOfType([ PT.string, PT.oneOf([null]), - ]).isRequired, + ]), + createdAt: PT.oneOfType([ + PT.string, + PT.oneOf([null]), + ]), challengeStatus: PT.string.isRequired, isReviewPhaseComplete: PT.bool, auth: PT.shape().isRequired, diff --git a/src/shared/components/challenge-detail/Submissions/SubmissionRow/index.jsx b/src/shared/components/challenge-detail/Submissions/SubmissionRow/index.jsx index 9ee00ea4e..65fd838b0 100644 --- a/src/shared/components/challenge-detail/Submissions/SubmissionRow/index.jsx +++ b/src/shared/components/challenge-detail/Submissions/SubmissionRow/index.jsx @@ -21,14 +21,17 @@ import style from './style.scss'; export default function SubmissionRow({ isMM, isRDM, openHistory, member, submissions, toggleHistory, challengeStatus, isReviewPhaseComplete, finalRank, provisionalRank, onShowPopup, rating, viewAsTable, - numWinners, auth, isLoggedIn, + numWinners, auth, isLoggedIn, isF2F, isBugHunt, }) { const submissionList = Array.isArray(submissions) ? submissions : []; const latestSubmission = submissionList[0] || {}; const { status, - submissionId, submissionTime, + created, + createdAt, + initialScore, + finalScore: submissionFinalScore, } = latestSubmission; const parseScore = (value) => { @@ -36,8 +39,12 @@ export default function SubmissionRow({ return Number.isFinite(numeric) ? numeric : null; }; + // For non-MM challenges, use createdAt field for submission date + const submissionDateField = isMM ? submissionTime : (created || createdAt); + const provisionalScore = parseScore(_.get(latestSubmission, 'provisionalScore')); - const finalScore = parseScore(_.get(latestSubmission, 'finalScore')); + const finalScore = parseScore(submissionFinalScore); + const initialScoreValue = parseScore(initialScore); const getInitialReviewResult = () => { if (status === 'failed') { @@ -71,12 +78,29 @@ export default function SubmissionRow({ return finalScore; }; + const getInitialScoreDisplay = () => { + if (_.isNil(initialScoreValue)) { + return 'N/A'; + } + return initialScoreValue.toFixed(2); + }; + + const getFinalScoreDisplay = () => { + if (challengeStatus !== CHALLENGE_STATUS.COMPLETED) { + return 'N/A'; + } + if (_.isNil(finalScore)) { + return 'N/A'; + } + return finalScore.toFixed(2); + }; + const initialReviewResult = getInitialReviewResult(); const finalReviewResult = getFinalReviewResult(); - const submissionMoment = submissionTime ? moment(submissionTime) : null; + const submissionMoment = submissionDateField ? moment(submissionDateField) : null; const submissionDateDisplay = submissionMoment - ? `${submissionMoment.format('DD MMM YYYY')} ${submissionMoment.format('HH:mm:ss')}` + ? submissionMoment.format('MMM DD, YYYY HH:mm') : 'N/A'; const finalRankDisplay = (isReviewPhaseComplete && _.isFinite(finalRank)) ? finalRank : 'N/A'; @@ -88,7 +112,7 @@ export default function SubmissionRow({ const memberProfileUrl = memberHandle ? `${window.origin}/members/${memberHandle}` : null; const memberLinkTarget = `${_.includes(window.origin, 'www') ? '_self' : '_blank'}`; const memberForHistory = memberHandle || memberDisplay; - const latestSubmissionId = submissionId || 'N/A'; + const latestSubmissionId = latestSubmission.submissionId || latestSubmission.id || 'N/A'; const submissionCount = submissionList.length; return ( @@ -107,63 +131,121 @@ export default function SubmissionRow({ { provisionalRankDisplay }
+
+
RATING
+ + {ratingDisplay} + +
+
+
USERNAME
+ { + memberProfileUrl ? ( + + {memberDisplay} + + ) : ( + {memberDisplay} + ) + } +
+
+
FINAL SCORE
+
+ {finalReviewResult} +
+
+
+
PROVISIONAL SCORE
+
+ {initialReviewResult} +
+
+
+
SUBMISSION DATE
+
+ {submissionDateDisplay} +
+
+
+
ACTIONS
+ + + History ( + {submissionCount} + ) + + +
- ) : null + ) : ( + + { + !isF2F && !isBugHunt && ( +
+
RATING
+ + {ratingDisplay} + +
+ ) + } +
+
USERNAME
+ { + memberProfileUrl ? ( + + {memberDisplay} + + ) : ( + {memberDisplay} + ) + } +
+
+
SUBMISSION DATE
+

{submissionDateDisplay}

+
+
+
INITIAL SCORE
+

{getInitialScoreDisplay()}

+
+
+
FINAL SCORE
+

{getFinalScoreDisplay()}

+
+ { + !isF2F && !isBugHunt && ( +
+ + + History ( + {submissionCount} + ) + + +
+ ) + } +
+ ) } -
-
RATING
- - {ratingDisplay} - -
-
-
USERNAME
- { - memberProfileUrl ? ( - - {memberDisplay} - - ) : ( - {memberDisplay} - ) - } -
-
-
FINAL SCORE
-
- {finalReviewResult} -
-
-
-
PROVISIONAL SCORE
-
- {initialReviewResult} -
-
-
-
SUBMISSION DATE
-
- {submissionDateDisplay} -
-
-
-
ACTIONS
- - - History ( - {submissionCount} - ) - - -
{ openHistory && ( -
-
- Provisional Score -
-
-
+ { + isMM && ( +
+
+ Provisional Score +
+
+ ) + } +
Time
{ @@ -233,6 +319,7 @@ export default function SubmissionRow({ auth={auth} isLoggedIn={isLoggedIn} submissionId={submissionHistory.submissionId} + createdAt={submissionHistory.created || submissionHistory.createdAt} /> )) } @@ -257,6 +344,8 @@ SubmissionRow.defaultProps = { provisionalRank: null, rating: null, isLoggedIn: false, + isF2F: false, + isBugHunt: false, }; SubmissionRow.propTypes = { @@ -265,6 +354,8 @@ SubmissionRow.propTypes = { openHistory: PT.bool.isRequired, member: PT.string.isRequired, challengeStatus: PT.string.isRequired, + isF2F: PT.bool, + isBugHunt: PT.bool, submissions: PT.arrayOf(PT.shape({ provisionalScore: PT.oneOfType([ PT.number, @@ -276,12 +367,26 @@ SubmissionRow.propTypes = { PT.string, PT.oneOf([null]), ]), + initialScore: PT.oneOfType([ + PT.number, + PT.string, + PT.oneOf([null]), + ]), status: PT.string.isRequired, + id: PT.string.isRequired, submissionId: PT.string.isRequired, submissionTime: PT.oneOfType([ PT.string, PT.oneOf([null]), - ]).isRequired, + ]), + created: PT.oneOfType([ + PT.string, + PT.oneOf([null]), + ]), + createdAt: PT.oneOfType([ + PT.string, + PT.oneOf([null]), + ]), })).isRequired, rating: PT.number, toggleHistory: PT.func, diff --git a/src/shared/components/challenge-detail/Submissions/index.jsx b/src/shared/components/challenge-detail/Submissions/index.jsx index c1165ac63..6082a0e94 100644 --- a/src/shared/components/challenge-detail/Submissions/index.jsx +++ b/src/shared/components/challenge-detail/Submissions/index.jsx @@ -41,6 +41,52 @@ const { getProvisionalScore, getFinalScore } = submissionUtils; const { getService } = services.submissions; +/** + * Groups submissions by member + * @param {Array} submissions all submissions + * @return {Array} grouped submissions by member + */ +function groupSubmissionsByMember(submissions) { + if (!Array.isArray(submissions)) { + return []; + } + + const memberMap = new Map(); + + submissions.forEach((submission) => { + const memberHandle = _.get(submission, 'registrant.memberHandle', ''); + if (!memberHandle) { + return; + } + + if (!memberMap.has(memberHandle)) { + memberMap.set(memberHandle, { + member: memberHandle, + registrant: submission.registrant, + submissions: [], + rating: submission.rating, + }); + } + + const memberEntry = memberMap.get(memberHandle); + memberEntry.submissions.push(submission); + // Update rating to the latest + if (submission.rating !== undefined) { + memberEntry.rating = submission.rating; + } + }); + + // Convert map to array and sort submissions within each member by date (newest first) + return Array.from(memberMap.values()).map(memberGroup => ({ + ...memberGroup, + submissions: memberGroup.submissions.sort((a, b) => { + const timeA = new Date(a.created || a.createdAt).getTime(); + const timeB = new Date(b.created || b.createdAt).getTime(); + return timeB - timeA; // Newest first + }), + })); +} + class SubmissionsComponent extends React.Component { constructor(props) { super(props); @@ -196,7 +242,14 @@ class SubmissionsComponent extends React.Component { const { submissions, mmSubmissions } = this.props; const source = isMM ? mmSubmissions : submissions; const sourceList = Array.isArray(source) ? source : []; - const sortedSubmissions = _.cloneDeep(sourceList); + + let sortedSubmissions = _.cloneDeep(sourceList); + + // Group submissions by member for non-MM challenges + if (!isMM) { + sortedSubmissions = groupSubmissionsByMember(sortedSubmissions); + } + this.sortSubmissions(sortedSubmissions); this.setState({ sortedSubmissions }); } @@ -212,15 +265,30 @@ class SubmissionsComponent extends React.Component { const isMM = this.isMM(); const isReviewPhaseComplete = this.checkIsReviewPhaseComplete(); const { field, sort } = this.getSubmissionsSortParam(isMM, isReviewPhaseComplete); - let isHaveFinalScore = false; - if (field === 'Initial Score' || field === 'Final Score') { - isHaveFinalScore = _.some( + + // For non-MM submissions that are grouped by member, we need to adjust the sorting logic + const isGrouped = !isMM && submissions.length > 0 && submissions[0].submissions; + + let hasFinalScore = false; + if (!isGrouped && (field === 'Initial Score' || field === 'Final Score')) { + hasFinalScore = _.some( submissions, s => Number.isFinite(Number(_.get(s, 'finalScore'))), ); + } else if (isGrouped && (field === 'Initial Score' || field === 'Final Score')) { + // For grouped submissions, check in the submissions array + hasFinalScore = _.some( + submissions, + group => _.some( + group.submissions, + s => Number.isFinite(Number(_.get(s, 'finalScore'))), + ), + ); } + const toSubmissionTime = (entry) => { - const latest = _.get(entry, ['submissions', 0]); + const entrySubmissions = entry.submissions || [entry]; + const latest = _.get(entrySubmissions, [0]); if (!latest) { return null; } @@ -231,15 +299,28 @@ class SubmissionsComponent extends React.Component { const timestamp = new Date(submissionTime).getTime(); return Number.isFinite(timestamp) ? timestamp : null; }; + const toRankValue = rank => (_.isFinite(rank) ? rank : Number.MAX_SAFE_INTEGER); const toScoreValue = (score) => { const numeric = Number(score); return Number.isFinite(numeric) ? numeric : null; }; + sortList(submissions, field, sort, (a, b) => { let valueA = 0; let valueB = 0; let valueIsString = false; + + const getPrimarySubmission = (entry) => { + if (isGrouped) { + return _.get(entry, ['submissions', 0]); + } + return entry; + }; + + const primaryA = getPrimarySubmission(a); + const primaryB = getPrimarySubmission(b); + switch (field) { case 'Country': { valueA = a.registrant ? a.registrant.countryCode : ''; @@ -248,12 +329,12 @@ class SubmissionsComponent extends React.Component { break; } case 'Rating': { - valueA = a.registrant ? a.registrant.rating : 0; - valueB = b.registrant ? b.registrant.rating : 0; + valueA = a.rating || (a.registrant ? a.registrant.rating : 0); + valueB = b.rating || (b.registrant ? b.registrant.rating : 0); break; } case 'Username': { - if (isMM) { + if (isMM || isGrouped) { valueA = `${a.member || ''}`.toLowerCase(); valueB = `${b.member || ''}`.toLowerCase(); } else { @@ -268,19 +349,19 @@ class SubmissionsComponent extends React.Component { valueB = toSubmissionTime(b); break; case 'Submission Date': { - const createdA = a.created || a.createdAt; - const createdB = b.created || b.createdAt; + const createdA = primaryA ? (primaryA.created || primaryA.createdAt) : null; + const createdB = primaryB ? (primaryB.created || primaryB.createdAt) : null; valueA = createdA ? new Date(createdA).getTime() : null; valueB = createdB ? new Date(createdB).getTime() : null; break; } case 'Initial Score': { - if (isHaveFinalScore) { - valueA = toScoreValue(_.get(a, 'finalScore')); - valueB = toScoreValue(_.get(b, 'finalScore')); + if (hasFinalScore) { + valueA = toScoreValue(_.get(primaryA, 'finalScore')); + valueB = toScoreValue(_.get(primaryB, 'finalScore')); } else { - valueA = toScoreValue(_.get(a, 'initialScore')); - valueB = toScoreValue(_.get(b, 'initialScore')); + valueA = toScoreValue(_.get(primaryA, 'initialScore')); + valueB = toScoreValue(_.get(primaryB, 'initialScore')); } break; } @@ -297,13 +378,13 @@ class SubmissionsComponent extends React.Component { break; } case 'Final Score': { - valueA = toScoreValue(getFinalScore(a)); - valueB = toScoreValue(getFinalScore(b)); + valueA = toScoreValue(getFinalScore(primaryA)); + valueB = toScoreValue(getFinalScore(primaryB)); break; } case 'Provisional Score': { - valueA = toScoreValue(getProvisionalScore(a)); - valueB = toScoreValue(getProvisionalScore(b)); + valueA = toScoreValue(getProvisionalScore(primaryA)); + valueB = toScoreValue(getProvisionalScore(primaryB)); break; } default: @@ -727,6 +808,13 @@ class SubmissionsComponent extends React.Component { >{ finalScoreClicked ? : }
+ { + !isF2F && !isBugHunt && ( +
+ Actions +
+ ) + } ) } @@ -943,48 +1031,31 @@ class SubmissionsComponent extends React.Component { } { !isMM && ( - sortedSubmissions.map(s => ( -
- { - !isF2F && !isBugHunt && ( - -
RATING
-
- { (s.registrant && !_.isNil(s.registrant.rating)) ? s.registrant.rating : '-'} -
-
- ) - } -
-
USERNAME
- - {_.get(s.registrant, 'memberHandle', '')} - -
-
-
SUBMISSION DATE
-

- {moment(s.created || s.createdAt).format('MMM DD, YYYY HH:mm')} -

-
-
-
INITIAL SCORE
-

- {this.getInitialScore(s)} -

-
-
-
FINAL SCORE
-

- {this.getFinalScoreDisplay(s)} -

-
-
+ sortedSubmissions.map((memberGroup, index) => ( + { toggleSubmissionHistory(index); }} + openHistory={(submissionHistoryOpen[index.toString()] || false)} + isLoadingSubmissionInformation={isLoadingSubmissionInformation} + submissionInformation={submissionInformation} + onShowPopup={this.onHandleInformationPopup} + getFlagFirstTry={this.getFlagFirstTry} + onGetFlagImageFail={onGetFlagImageFail} + submissionDetail={memberGroup} + viewAsTable={viewAsTable} + numWinners={numWinners} + auth={auth} + isLoggedIn={isLoggedIn} + isF2F={isF2F} + isBugHunt={isBugHunt} + /> )) ) } diff --git a/src/shared/components/challenge-detail/Submissions/style.scss b/src/shared/components/challenge-detail/Submissions/style.scss index 665c95579..5387d72ae 100644 --- a/src/shared/components/challenge-detail/Submissions/style.scss +++ b/src/shared/components/challenge-detail/Submissions/style.scss @@ -372,7 +372,7 @@ .col-1 { padding-left: 30px; - width: 10%; + flex: 20; justify-content: flex-start; display: flex; min-width: 140px; @@ -383,13 +383,13 @@ } .col-2 { - width: 15%; + flex: 20; justify-content: flex-start; display: flex; } .col-3 { - width: 20.5%; + flex: 20; margin-right: auto; a { @@ -406,7 +406,7 @@ } .col-4 { - width: 20.5%; + flex: 20; margin-right: auto; p { @@ -423,7 +423,7 @@ } .col-5 { - width: 22%; + flex: 20; p { padding-right: 60px; @@ -439,7 +439,7 @@ } .col-6 { - width: 22%; + flex: 20; p { padding-right: 55px; @@ -454,6 +454,16 @@ } } + .col-8 { + flex: 13; + text-align: left; + color: $tc-gray-50; + + @include xs-to-sm { + display: none; + } + } + .handle { color: $tc-black; }