diff --git a/.circleci/config.yml b/.circleci/config.yml index d259b85b7..40e916d30 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -179,6 +179,7 @@ workflows: branches: only: - develop + - PM-3686_group-submissions-in-challenge-details - "build-prod": context: org-global diff --git a/src/shared/components/Dashboard/Challenges/index.jsx b/src/shared/components/Dashboard/Challenges/index.jsx index b9ae24631..251c2b3c9 100644 --- a/src/shared/components/Dashboard/Challenges/index.jsx +++ b/src/shared/components/Dashboard/Challenges/index.jsx @@ -38,26 +38,32 @@ export default function ChallengesFeed({ ) : ( - (challenges || []).map(challenge => ( -
- - {challenge.name} - -
- - {`$${_.sum( - challenge.prizeSets - .filter(set => set.type === 'PLACEMENT') - .map(item => _.sum(item.prizes.map(prize => prize.value))), - ).toLocaleString()}`} - + (challenges || []).map((challenge) => { + const placementPrizes = challenge.prizeSets + .filter(set => set.type === 'PLACEMENT') + .flatMap(item => item.prizes); + const prizeTotal = _.sum(placementPrizes.map(prize => prize.value)); + const prizeType = placementPrizes.length > 0 ? placementPrizes[0].type : null; + const isPointBasedPrize = prizeType === 'POINT'; + const prizeSymbol = isPointBasedPrize ? '' : '$'; + + return ( +
+ + {challenge.name} + +
+ + {`${prizeSymbol}${prizeTotal.toLocaleString()}`} + +
-
- )) + ); + }) )}
diff --git a/src/shared/components/ReviewOpportunityDetailsPage/ChallengeSpecTab/index.jsx b/src/shared/components/ReviewOpportunityDetailsPage/ChallengeSpecTab/index.jsx index d501a707a..b9bec8298 100644 --- a/src/shared/components/ReviewOpportunityDetailsPage/ChallengeSpecTab/index.jsx +++ b/src/shared/components/ReviewOpportunityDetailsPage/ChallengeSpecTab/index.jsx @@ -3,6 +3,7 @@ */ import React from 'react'; import PT from 'prop-types'; +import MarkdownRenderer from 'components/MarkdownRenderer'; import './styles.scss'; @@ -19,13 +20,10 @@ const ChallengeSpecTab = ({ challenge }) => ( Challenge Overview
+ > + +
) } diff --git a/src/shared/components/SubmissionManagement/Submission/index.jsx b/src/shared/components/SubmissionManagement/Submission/index.jsx index 2c15defb4..903bc7cc1 100644 --- a/src/shared/components/SubmissionManagement/Submission/index.jsx +++ b/src/shared/components/SubmissionManagement/Submission/index.jsx @@ -46,7 +46,7 @@ export default function Submission(props) { } = props; const formatDate = date => moment(+new Date(date)).format('MMM DD, YYYY hh:mm A'); const onDownloadSubmission = onDownload.bind(1, submissionObject.id); - const safeForDownloadCheck = safeForDownload(submissionObject.url); + const safeForDownloadCheck = safeForDownload(submissionObject); const onDownloadArtifacts = onOpenDownloadArtifactsModal.bind(1, submissionObject.id); const onOpenRatingsList = onOpenRatingsListModal.bind(1, submissionObject.id); const onOpenReviewApp = () => { diff --git a/src/shared/components/challenge-detail/Header/style.scss b/src/shared/components/challenge-detail/Header/style.scss index d75b659b7..d7b79956a 100644 --- a/src/shared/components/challenge-detail/Header/style.scss +++ b/src/shared/components/challenge-detail/Header/style.scss @@ -550,6 +550,7 @@ .block-tags-container { display: flex; + flex-wrap: wrap; gap: 8px; } @@ -559,6 +560,7 @@ background-color: $color-black-10 !important; border: none; margin: 0; + padding: 4px 8px; &:hover { background-color: #d4d4d4 !important; 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 928892351..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); @@ -122,16 +168,30 @@ class SubmissionsComponent extends React.Component { getInitialScore(submission) { let score = 'N/A'; const { challenge } = this.props; - if (!_.isEmpty(submission.review) - && !_.isEmpty(submission) - && submission.initialScore + const parsedScore = Number(_.get(submission, 'initialScore')); + const hasScore = Number.isFinite(parsedScore); + if (hasScore && (challenge.status === 'COMPLETED' || (_.includes(challenge.tags, 'Innovation Challenge') && _.find(challenge.metadata, { name: 'show_data_dashboard' })))) { - score = Number(submission.initialScore).toFixed(2); + score = parsedScore.toFixed(2); } return score; } + getFinalScoreDisplay(submission) { + const { challenge } = this.props; + if (challenge.status !== CHALLENGE_STATUS.COMPLETED) { + return 'N/A'; + } + + const parsedScore = Number(_.get(submission, 'finalScore')); + if (!Number.isFinite(parsedScore)) { + return 'N/A'; + } + + return parsedScore.toFixed(2); + } + /** * Check if it have flag for first try * @param {Object} registrant registrant info @@ -182,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 }); } @@ -198,14 +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(submissions, s => !_.isNil( - s.review && s.finalScore, - )); + + // 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; } @@ -216,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 : ''; @@ -233,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 { @@ -253,23 +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 = !_.isEmpty(a.review) && a.finalScore; - valueB = !_.isEmpty(b.review) && b.finalScore; - } else if (valueA.score || valueB.score) { - // Handle MM formatted scores in a code challenge (PS-295) - valueA = Number(valueA.score); - valueB = Number(valueB.score); + if (hasFinalScore) { + valueA = toScoreValue(_.get(primaryA, 'finalScore')); + valueB = toScoreValue(_.get(primaryB, 'finalScore')); } else { - valueA = !_.isEmpty(a.review) && a.initialScore; - valueB = !_.isEmpty(b.review) && b.initialScore; + valueA = toScoreValue(_.get(primaryA, 'initialScore')); + valueB = toScoreValue(_.get(primaryB, 'initialScore')); } break; } @@ -286,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: @@ -716,6 +808,13 @@ class SubmissionsComponent extends React.Component { >{ finalScoreClicked ? : }
+ { + !isF2F && !isBugHunt && ( +
+ Actions +
+ ) + } ) } @@ -932,52 +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
-

- { - (s.review && s.finalScore && challenge.status === 'COMPLETED') - ? Number(s.finalScore).toFixed(2) - : 'N/A' - } -

-
-
+ 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; } diff --git a/src/shared/components/engagement-listing/EngagementCard/index.jsx b/src/shared/components/engagement-listing/EngagementCard/index.jsx index 2378a419e..090d3503c 100644 --- a/src/shared/components/engagement-listing/EngagementCard/index.jsx +++ b/src/shared/components/engagement-listing/EngagementCard/index.jsx @@ -6,7 +6,6 @@ import IconBlackDuration from 'assets/images/icon-black-calendar.svg'; import IconBlackLocation from 'assets/images/icon-black-location.svg'; import IconBlackPayment from 'assets/images/icon-black-payment.svg'; import iconBlackSkills from 'assets/images/icon-skills.png'; -import IconTimezone from 'assets/images/icon-timezone.svg'; import './style.scss'; @@ -22,12 +21,6 @@ const WORKLOAD_LABELS = { FRACTIONAL: 'Fractional', }; -const ANTICIPATED_START_LABELS = { - IMMEDIATE: 'Immediate', - FEW_DAYS: 'Few Days', - FEW_WEEKS: 'Few Weeks', -}; - const STATUS_LABELS = { OPEN: 'Open', PENDING_ASSIGNMENT: 'Pending Assignment', @@ -36,62 +29,20 @@ const STATUS_LABELS = { CLOSED: 'Closed', }; -const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; -const UNKNOWN_SKILL_LABEL = 'Unknown skill'; const DEFAULT_LOCALE = 'en-US'; -const SIMPLE_TZ_PATTERN = /^[A-Za-z]{2,6}$/; -const OFFSET_TZ_PATTERN = /^(?:UTC|GMT)?\s*([+-])\s*(\d{1,2})(?::?(\d{2}))?$/i; -const BARE_OFFSET_PATTERN = /^([+-])(\d{2})(?::?(\d{2}))$/; -const TIMEZONE_ABBREVIATION_LONG_NAMES = { - ACDT: 'Australian Central Daylight Time', - ACST: 'Australian Central Standard Time', - AEDT: 'Australian Eastern Daylight Time', - AEST: 'Australian Eastern Standard Time', - AKDT: 'Alaska Daylight Time', - AKST: 'Alaska Standard Time', - AWST: 'Australian Western Standard Time', - BST: 'British Summer Time', - CDT: 'Central Daylight Time', - CEST: 'Central European Summer Time', - CET: 'Central European Standard Time', - CST: 'Central Standard Time', - EDT: 'Eastern Daylight Time', - EEST: 'Eastern European Summer Time', - EET: 'Eastern European Standard Time', - EST: 'Eastern Standard Time', - GMT: 'Greenwich Mean Time', - HST: 'Hawaii-Aleutian Standard Time', - IST: 'India Standard Time', - JST: 'Japan Standard Time', - KST: 'Korea Standard Time', - MDT: 'Mountain Daylight Time', - MST: 'Mountain Standard Time', - NZDT: 'New Zealand Daylight Time', - NZST: 'New Zealand Standard Time', - PDT: 'Pacific Daylight Time', - PST: 'Pacific Standard Time', - SAST: 'South Africa Standard Time', - UTC: 'Coordinated Universal Time', - WEST: 'Western European Summer Time', - WET: 'Western European Standard Time', -}; + const REGION_NAME_OVERRIDES = { UK: 'United Kingdom', }; const regionDisplayNames = typeof Intl !== 'undefined' && typeof Intl.DisplayNames === 'function' ? new Intl.DisplayNames([DEFAULT_LOCALE], { type: 'region' }) : null; -let timezoneAbbreviationMap; function asArray(value) { if (!value) return []; return Array.isArray(value) ? value : [value]; } -function isUuid(value) { - return typeof value === 'string' && UUID_PATTERN.test(value); -} - function toTitleCase(value) { return value .toLowerCase() @@ -127,21 +78,6 @@ function normalizeLabel(value, normalizedMap) { return spaced || raw; } -function normalizeSkillLabel(skill) { - if (!skill) return null; - - if (typeof skill === 'object' && skill !== null) { - const label = skill.name || skill.title; - if (label) return String(label); - const skillId = skill.id || skill.value; - if (isUuid(skillId)) return UNKNOWN_SKILL_LABEL; - return skillId ? String(skillId) : null; - } - - if (isUuid(skill)) return UNKNOWN_SKILL_LABEL; - return String(skill); -} - function normalizeLocationValue(value) { if (!value) return null; if (typeof value === 'object' && value !== null) { @@ -173,154 +109,6 @@ function normalizeRegionValue(value) { return trimmed; } -function getIntlTimeZoneName(timeZone, style) { - if (typeof Intl === 'undefined' || typeof Intl.DateTimeFormat !== 'function') { - return null; - } - - try { - const formatter = new Intl.DateTimeFormat(DEFAULT_LOCALE, { - timeZone, - timeZoneName: style, - }); - - if (typeof formatter.formatToParts !== 'function') { - return null; - } - - const parts = formatter.formatToParts(new Date()); - const namePart = parts.find(part => part.type === 'timeZoneName'); - return namePart && namePart.value ? namePart.value : null; - } catch (error) { - return null; - } -} - -function getMomentTimeZoneName(timeZone) { - if (!moment || !moment.tz || !moment.tz.zone) { - return null; - } - - if (!moment.tz.zone(timeZone)) { - return null; - } - - try { - return moment.tz(new Date(), timeZone).format('z'); - } catch (error) { - return null; - } -} - -function getTimeZoneAbbreviationMap() { - if (timezoneAbbreviationMap) return timezoneAbbreviationMap; - timezoneAbbreviationMap = new Map(); - if (!moment || !moment.tz || typeof moment.tz.names !== 'function') { - return timezoneAbbreviationMap; - } - - moment.tz.names().forEach((zoneName) => { - try { - const abbr = moment.tz(new Date(), zoneName).format('z'); - if (!abbr) return; - const normalized = abbr.toUpperCase(); - if (!timezoneAbbreviationMap.has(normalized)) { - timezoneAbbreviationMap.set(normalized, zoneName); - } - } catch (error) { - // ignore invalid timezone data - } - }); - - return timezoneAbbreviationMap; -} - -function resolveTimeZoneAbbreviationName(abbreviation) { - const normalized = abbreviation.toUpperCase(); - if (TIMEZONE_ABBREVIATION_LONG_NAMES[normalized]) { - return TIMEZONE_ABBREVIATION_LONG_NAMES[normalized]; - } - - const map = getTimeZoneAbbreviationMap(); - const zoneName = map.get(normalized); - if (!zoneName) return null; - - return ( - getIntlTimeZoneName(zoneName, 'long') - || getIntlTimeZoneName(zoneName, 'longGeneric') - ); -} - -function formatUtcOffset(sign, hours, minutes) { - const hourValue = Number(hours); - const minuteValue = Number(minutes || 0); - - if (Number.isNaN(hourValue) || Number.isNaN(minuteValue)) { - return null; - } - - const normalizedHours = String(Math.abs(hourValue)).padStart(2, '0'); - const normalizedMinutes = String(Math.abs(minuteValue)).padStart(2, '0'); - const suffix = normalizedMinutes !== '00' ? `:${normalizedMinutes}` : ''; - - return `UTC${sign}${normalizedHours}${suffix}`; -} - -function normalizeUtcOffset(value) { - if (!value) return null; - const normalized = String(value).trim(); - if (!normalized) return null; - - if (/^(utc|gmt)$/i.test(normalized)) { - return 'UTC'; - } - - const offsetMatch = normalized.match(OFFSET_TZ_PATTERN); - if (offsetMatch) { - return formatUtcOffset(offsetMatch[1], offsetMatch[2], offsetMatch[3]); - } - - const bareMatch = normalized.match(BARE_OFFSET_PATTERN); - if (bareMatch) { - return formatUtcOffset(bareMatch[1], bareMatch[2], bareMatch[3]); - } - - return null; -} - -function normalizeTimezoneValue(value) { - const normalizedValue = normalizeLocationValue(value); - if (!normalizedValue) return null; - - const trimmed = normalizedValue.trim(); - if (!trimmed) return null; - - if (trimmed.toLowerCase() === 'any') { - return 'Any'; - } - - const longName = getIntlTimeZoneName(trimmed, 'long') || getIntlTimeZoneName(trimmed, 'longGeneric'); - if (longName) { - return longName; - } - - const offset = normalizeUtcOffset(trimmed); - if (offset) { - return offset; - } - - if (SIMPLE_TZ_PATTERN.test(trimmed)) { - return resolveTimeZoneAbbreviationName(trimmed) || trimmed.toUpperCase(); - } - - const fallbackShortName = getMomentTimeZoneName(trimmed) || getIntlTimeZoneName(trimmed, 'short'); - if (fallbackShortName) { - return fallbackShortName; - } - - return trimmed; -} - function uniqNormalizedStrings(values) { const seen = new Set(); return values.reduce((acc, value) => { @@ -365,10 +153,6 @@ function getWorkloadDisplay(workload) { return normalizeLabel(workload, WORKLOAD_LABELS); } -function getAnticipatedStartDisplay(value) { - return normalizeLabel(value, ANTICIPATED_START_LABELS); -} - function getCompensationDisplay(compensationRange) { if (typeof compensationRange === 'object' && compensationRange !== null) { const label = compensationRange.name || compensationRange.title; @@ -406,17 +190,10 @@ function EngagementCard({ engagement }) { role, workload, compensationRange, - skills: engagementSkills, - requiredSkills, - skillsets, location, locations: engagementLocations, - timezone, - timezones, - timeZones, countries, status, - anticipatedStart, nanoId, id, engagementId, @@ -431,24 +208,6 @@ function EngagementCard({ engagement }) { durationWeeks, durationMonths, ); - const anticipatedStartText = getAnticipatedStartDisplay(anticipatedStart); - - const skillsSource = [engagementSkills, requiredSkills, skillsets] - .find(value => Array.isArray(value) && value.length) - || engagementSkills - || requiredSkills - || skillsets; - const skills = Array.from(new Set( - asArray(skillsSource) - .map(normalizeSkillLabel) - .filter(Boolean), - )); - const skillsText = skills.length - ? skills.slice(0, 2).join(', ') - : 'Not Specified'; - const limitedSkillsText = skills.length > 2 - ? `${skillsText},...` - : skillsText; const baseLocations = [ ...asArray(location), @@ -456,19 +215,11 @@ function EngagementCard({ engagement }) { ] .map(normalizeRegionValue) .filter(Boolean); - const timezoneValues = [ - ...asArray(timezone), - ...asArray(timezones), - ...asArray(timeZones), - ] - .map(normalizeTimezoneValue) - .filter(Boolean); const countryValues = asArray(countries) .map(normalizeRegionValue) .filter(Boolean); const isAnyValue = value => value.trim().toLowerCase() === 'any'; const hasAnyLocation = [...baseLocations, ...countryValues].some(isAnyValue); - const hasAnyTimezone = timezoneValues.some(isAnyValue); const filteredBaseLocations = baseLocations.filter(value => !isAnyValue(value)); const filteredCountries = countryValues.filter(value => !isAnyValue(value)); const locations = uniqNormalizedStrings([ @@ -477,15 +228,6 @@ function EngagementCard({ engagement }) { ...filteredCountries, ]); const locationText = locations.length ? locations.join(', ') : 'Remote'; - const filteredTimezones = uniqNormalizedStrings( - timezoneValues.filter(value => !isAnyValue(value)), - ); - let timezoneText = 'Not Specified'; - if (filteredTimezones.length) { - timezoneText = filteredTimezones.join(', '); - } else if (hasAnyTimezone) { - timezoneText = 'Any'; - } const resolvedEngagementId = nanoId || id || engagementId; const engagementLink = resolvedEngagementId @@ -505,12 +247,6 @@ function EngagementCard({ engagement }) {
role-icon {getRoleDisplay(role)}
-
- skills-icon {limitedSkillsText} -
-
- {timezoneText} -
{locationText}
@@ -523,9 +259,6 @@ function EngagementCard({ engagement }) {
{durationText}
-
- {`Anticipated start: ${anticipatedStartText}`} -
VIEW DETAILS diff --git a/src/shared/components/engagement-listing/EngagementCard/style.scss b/src/shared/components/engagement-listing/EngagementCard/style.scss index d7c740e41..b6f4bedae 100644 --- a/src/shared/components/engagement-listing/EngagementCard/style.scss +++ b/src/shared/components/engagement-listing/EngagementCard/style.scss @@ -66,7 +66,7 @@ .job-infos { display: grid; - grid-template-columns: 1.2fr 1fr 1.2fr 1.2fr 1fr 1.3fr 0.9fr 1fr 141px; + grid-template-columns: 1.2fr 1.2fr 1fr 1.3fr 0.9fr 141px; column-gap: 20px; align-items: center; diff --git a/src/shared/containers/SubmissionManagement/index.jsx b/src/shared/containers/SubmissionManagement/index.jsx index 3a05e7369..91fe8117e 100644 --- a/src/shared/containers/SubmissionManagement/index.jsx +++ b/src/shared/containers/SubmissionManagement/index.jsx @@ -192,7 +192,7 @@ class SubmissionManagementPageContainer extends React.Component { const { needReload } = this.state; if (needReload === false && mySubmissions) { - if (mySubmissions.find(item => safeForDownload(item.url) !== true)) { + if (mySubmissions.find(item => safeForDownload(item) !== true)) { this.setState({ needReload: true }); setTimeout(() => { loadMySubmissions(authTokens, challengeId); diff --git a/src/shared/containers/challenge-detail/index.jsx b/src/shared/containers/challenge-detail/index.jsx index af21ce89f..bd6586cfb 100644 --- a/src/shared/containers/challenge-detail/index.jsx +++ b/src/shared/containers/challenge-detail/index.jsx @@ -271,8 +271,7 @@ class ChallengeDetailPageContainer extends React.Component { const nextChallengeId = _.get(nextProps.challenge, 'id'); if (nextChallengeId - && nextChallengeId !== previousChallengeId - && checkIsMM(nextProps.challenge)) { + && nextChallengeId !== previousChallengeId) { nextProps.fetchChallengeStatistics(nextProps.auth, nextProps.challenge); } @@ -874,6 +873,206 @@ function extractArrayFromStateSlice(slice, challengeId) { return []; } +function normalizeScoreValue(score) { + if (_.isNil(score) || score === '' || score === '-') { + return null; + } + const parsed = Number(score); + return Number.isFinite(parsed) ? parsed : null; +} + +function getReviewSummationTimestampValue(summation) { + const timestampRaw = _.get(summation, 'reviewedDate') + || _.get(summation, 'updatedAt') + || _.get(summation, 'createdAt') + || _.get(summation, 'created') + || null; + if (!timestampRaw) { + return 0; + } + const timestamp = new Date(timestampRaw).getTime(); + return Number.isFinite(timestamp) ? timestamp : 0; +} + +function getReviewSummationType(summation) { + const metadata = _.isObject(_.get(summation, 'metadata')) + ? _.get(summation, 'metadata') + : {}; + const type = _.toLower(_.toString(_.get(summation, 'type', '')).trim()); + const stage = _.toLower(_.toString(_.get(metadata, 'stage', '')).trim()); + const testType = _.toLower(_.toString(_.get(metadata, 'testType', '')).trim()); + const isProvisional = Boolean( + _.get(summation, 'isProvisional') + || _.get(summation, 'is_provisional') + || type === 'provisional' + || testType === 'provisional', + ); + const isFinal = Boolean( + _.get(summation, 'isFinal') + || _.get(summation, 'is_final') + || type === 'final' + || stage === 'final', + ); + + if (isFinal) { + return 'final'; + } + if (isProvisional) { + return 'provisional'; + } + return 'other'; +} + +function dedupeReviewSummations(summations = []) { + const deduped = []; + const seen = new Set(); + + summations.forEach((summation, index) => { + if (!summation) { + return; + } + + const dedupeKey = _.toString(_.get(summation, 'id', '')).trim() + || `${_.toString(_.get(summation, 'submissionId', '')).trim()}::${_.toString(_.get(summation, 'legacySubmissionId', '')).trim()}::${_.toString(_.get(summation, 'submitterId', '')).trim()}::${_.toString(_.get(summation, 'aggregateScore', '')).trim()}::${_.toString(_.get(summation, 'reviewedDate', '')).trim()}::${index}`; + + if (seen.has(dedupeKey)) { + return; + } + seen.add(dedupeKey); + deduped.push(summation); + }); + + return deduped; +} + +function pushSummationByKey(map, rawKey, summation) { + const key = _.toString(rawKey || '').trim(); + if (!key) { + return; + } + if (!map.has(key)) { + map.set(key, []); + } + map.get(key).push(summation); +} + +function buildReviewSummationLookup(reviewSummations = []) { + const bySubmissionId = new Map(); + const byLegacySubmissionId = new Map(); + const byMemberId = new Map(); + const byHandle = new Map(); + + reviewSummations.forEach((summation) => { + if (!summation) { + return; + } + + pushSummationByKey(bySubmissionId, _.get(summation, 'submissionId'), summation); + pushSummationByKey(byLegacySubmissionId, _.get(summation, 'legacySubmissionId'), summation); + pushSummationByKey(byMemberId, _.get(summation, 'submitterId'), summation); + + const handle = _.toLower(_.toString(_.get(summation, 'submitterHandle', '')).trim()); + if (handle) { + pushSummationByKey(byHandle, handle, summation); + } + }); + + return { + bySubmissionId, + byLegacySubmissionId, + byMemberId, + byHandle, + }; +} + +function collectMatchedReviewSummations(submission, lookup) { + if (!submission || !lookup) { + return []; + } + + const matchesBySubmissionId = []; + const matchesByMemberOrHandle = []; + const appendMatches = (items) => { + if (Array.isArray(items) && items.length) { + matchesBySubmissionId.push(...items); + } + }; + + [ + _.get(submission, 'submissionId'), + _.get(submission, 'id'), + ].forEach((id) => { + const key = _.toString(id || '').trim(); + if (key) { + appendMatches(lookup.bySubmissionId.get(key)); + } + }); + + const legacySubmissionId = _.toString(_.get(submission, 'legacySubmissionId', '')).trim(); + if (legacySubmissionId) { + appendMatches(lookup.byLegacySubmissionId.get(legacySubmissionId)); + } + + const appendFallbackMatches = (items) => { + if (Array.isArray(items) && items.length) { + matchesByMemberOrHandle.push(...items); + } + }; + + const memberId = _.toString(_.get(submission, 'memberId', '')).trim(); + if (memberId) { + appendFallbackMatches(lookup.byMemberId.get(memberId)); + } + + const handle = _.toLower(_.toString( + _.get(submission, 'registrant.memberHandle') + || _.get(submission, 'memberHandle') + || _.get(submission, 'createdBy') + || '', + ).trim()); + if (handle) { + appendFallbackMatches(lookup.byHandle.get(handle)); + } + + const selectedMatches = matchesBySubmissionId.length + ? matchesBySubmissionId + : matchesByMemberOrHandle; + + return dedupeReviewSummations(selectedMatches); +} + +function getLatestReviewSummationScore(summations = [], targetType = null) { + let latest = null; + + summations.forEach((summation, index) => { + if (!summation) { + return; + } + + const score = normalizeScoreValue(_.get(summation, 'aggregateScore')); + if (_.isNil(score)) { + return; + } + + if (targetType && getReviewSummationType(summation) !== targetType) { + return; + } + + const timestampValue = getReviewSummationTimestampValue(summation); + if (!latest + || timestampValue > latest.timestampValue + || (timestampValue === latest.timestampValue && index > latest.index)) { + latest = { + score, + timestampValue, + index, + }; + } + }); + + return latest ? latest.score : null; +} + function mapStateToProps(state, props) { const challengeId = String(props.match.params.challengeId); const cl = state.challengeListing; @@ -892,6 +1091,9 @@ function mapStateToProps(state, props) { statisticsData = buildStatisticsData(reviewSummations); } const challenge = state.challenge.details || {}; + const reviewSummationLookup = reviewSummations.length + ? buildReviewSummationLookup(reviewSummations) + : null; let mySubmissions = []; if (challenge.registrants) { challenge.registrants = challenge.registrants.map(registrant => ({ @@ -910,11 +1112,50 @@ function mapStateToProps(state, props) { challenge.registrants, r => (`${r.memberId}` === `${submission.memberId}`), ); + const matchedReviewSummations = collectMatchedReviewSummations( + submission, + reviewSummationLookup, + ); + const existingReviewSummations = dedupeReviewSummations([ + ...(Array.isArray(submission.reviewSummations) ? submission.reviewSummations : []), + ...(Array.isArray(submission.reviewSummation) ? submission.reviewSummation : []), + ]); + const mergedReviewSummations = dedupeReviewSummations([ + ...existingReviewSummations, + ...matchedReviewSummations, + ]); + + const existingFinalScore = normalizeScoreValue(submission.finalScore); + const finalScoreFromSummations = getLatestReviewSummationScore( + mergedReviewSummations, + 'final', + ); + const fallbackSummationScore = _.isNil(finalScoreFromSummations) + ? getLatestReviewSummationScore(mergedReviewSummations) + : finalScoreFromSummations; + const resolvedFinalScore = !_.isNil(existingFinalScore) + ? existingFinalScore + : fallbackSummationScore; + + const existingInitialScore = normalizeScoreValue(submission.initialScore); + const provisionalScoreFromSummations = getLatestReviewSummationScore( + mergedReviewSummations, + 'provisional', + ); + const resolvedInitialScore = !_.isNil(existingInitialScore) + ? existingInitialScore + : provisionalScoreFromSummations; // Ensure legacy fields used in UI exist const created = submission.created || submission.createdAt || null; const updated = submission.updated || submission.updatedAt || null; return ({ ...submission, + finalScore: _.isNil(resolvedFinalScore) ? submission.finalScore : resolvedFinalScore, + initialScore: _.isNil(resolvedInitialScore) + ? submission.initialScore + : resolvedInitialScore, + reviewSummations: mergedReviewSummations, + reviewSummation: mergedReviewSummations, created, updated, registrant, @@ -1169,7 +1410,11 @@ const mapDispatchToProps = (dispatch) => { const ca = communityActions.tcCommunity; const lookupActions = actions.lookup; - const dispatchReviewSummations = (challengeId, tokenV3) => { + const dispatchReviewSummations = (challengeId, tokenV3, options = {}) => { + const { + includeMmSubmissions = true, + includeStatistics = includeMmSubmissions, + } = options; const challengeIdStr = _.toString(challengeId); if (!challengeIdStr) { return; @@ -1179,33 +1424,39 @@ const mapDispatchToProps = (dispatch) => { type: 'CHALLENGE/GET_REVIEW_SUMMATIONS_INIT', payload: challengeIdStr, }); - dispatch({ - type: 'CHALLENGE/GET_MM_SUBMISSIONS_INIT', - payload: challengeIdStr, - }); + if (includeMmSubmissions) { + dispatch({ + type: 'CHALLENGE/GET_MM_SUBMISSIONS_INIT', + payload: challengeIdStr, + }); + } getReviewSummationsService(tokenV3, challengeIdStr) .then(({ data }) => { const reviewSummations = Array.isArray(data) ? data : []; - const mmSubmissions = buildMmSubmissionData(reviewSummations); - const statisticsData = buildStatisticsData(reviewSummations); dispatch({ type: 'CHALLENGE/GET_REVIEW_SUMMATIONS_DONE', payload: reviewSummations, meta: { challengeId: challengeIdStr }, }); - dispatch({ - type: 'CHALLENGE/GET_MM_SUBMISSIONS_DONE', - payload: { - challengeId: challengeIdStr, - submissions: mmSubmissions, - }, - }); - dispatch({ - type: 'CHALLENGE/FETCH_CHALLENGE_STATISTICS_DONE', - payload: statisticsData, - }); + if (includeMmSubmissions) { + const mmSubmissions = buildMmSubmissionData(reviewSummations); + dispatch({ + type: 'CHALLENGE/GET_MM_SUBMISSIONS_DONE', + payload: { + challengeId: challengeIdStr, + submissions: mmSubmissions, + }, + }); + } + if (includeStatistics) { + const statisticsData = buildStatisticsData(reviewSummations); + dispatch({ + type: 'CHALLENGE/FETCH_CHALLENGE_STATISTICS_DONE', + payload: statisticsData, + }); + } }) .catch((error) => { dispatch({ @@ -1214,16 +1465,20 @@ const mapDispatchToProps = (dispatch) => { payload: { challengeId: challengeIdStr, error }, meta: { challengeId: challengeIdStr }, }); - dispatch({ - type: 'CHALLENGE/GET_MM_SUBMISSIONS_DONE', - error: true, - payload: { challengeId: challengeIdStr, error }, - }); - dispatch({ - type: 'CHALLENGE/FETCH_CHALLENGE_STATISTICS_DONE', - error: true, - payload: error, - }); + if (includeMmSubmissions) { + dispatch({ + type: 'CHALLENGE/GET_MM_SUBMISSIONS_DONE', + error: true, + payload: { challengeId: challengeIdStr, error }, + }); + } + if (includeStatistics) { + dispatch({ + type: 'CHALLENGE/FETCH_CHALLENGE_STATISTICS_DONE', + error: true, + payload: error, + }); + } }); }; @@ -1410,7 +1665,7 @@ const mapDispatchToProps = (dispatch) => { dispatch(a.expandTag(id)); }, fetchChallengeStatistics: (tokens, challengeDetails) => { - if (!tokens || !tokens.tokenV3 || !challengeDetails || !checkIsMM(challengeDetails)) { + if (!challengeDetails) { return; } @@ -1419,7 +1674,22 @@ const mapDispatchToProps = (dispatch) => { return; } - dispatchReviewSummations(challengeId, tokens.tokenV3); + const isMMChallenge = checkIsMM(challengeDetails); + const isCompletedNonMMChallenge = !isMMChallenge + && _.get(challengeDetails, 'status') === CHALLENGE_STATUS.COMPLETED; + + if (!isMMChallenge && !isCompletedNonMMChallenge) { + return; + } + + dispatchReviewSummations( + challengeId, + _.get(tokens, 'tokenV3'), + { + includeMmSubmissions: isMMChallenge, + includeStatistics: isMMChallenge, + }, + ); }, }; }; diff --git a/src/shared/services/reviewSummations.js b/src/shared/services/reviewSummations.js index 02ae2a40f..24308df33 100644 --- a/src/shared/services/reviewSummations.js +++ b/src/shared/services/reviewSummations.js @@ -50,9 +50,10 @@ async function fetchReviewSummationsPage({ } export default async function getReviewSummations(tokenV3, challengeId) { - const headers = new Headers({ - Authorization: `Bearer ${tokenV3}`, - }); + const headers = new Headers(); + if (tokenV3) { + headers.set('Authorization', `Bearer ${tokenV3}`); + } const { aggregated, meta } = await fetchReviewSummationsPage({ challengeId, diff --git a/src/shared/utils/tc.js b/src/shared/utils/tc.js index 0b1f3f3d7..d40eef66c 100644 --- a/src/shared/utils/tc.js +++ b/src/shared/utils/tc.js @@ -376,21 +376,20 @@ export function isValidEmail(email) { } /** - * Test if the file is safe for download. This patch currently checks the location of the submission - * to determine if the file is infected or not. This is an immedaite patch, and should be updated to - * check the review scan score for review type virus scan. + * Test if the file is safe for download. This function can accept the full submission object. * * @returns {String|Boolean} true if submission is safe for download, * otherwise string describing reason for not being safe for download */ -export function safeForDownload(url) { - if (url == null) return 'Download link unavailable'; +export function safeForDownload(submission) { + if (submission == null || !submission.url) return 'Download link unavailable'; - if (url.toLowerCase().indexOf('submissions-quarantine/') !== -1) { + const { url } = submission; + if (url.toLowerCase().indexOf('submissions-quarantine/') !== -1 || submission.virusScan === false) { return 'Malware found in submission'; } - if (url.toLowerCase().indexOf('submissions-dmz/') !== -1) { + if (url.toLowerCase().indexOf('submissions-dmz/') !== -1 || !submission.virusScan) { return 'AV Scan in progress'; }