diff --git a/.github/workflows/trivy.yaml b/.github/workflows/trivy.yaml
index 7b9fa4839..9cbcf5209 100644
--- a/.github/workflows/trivy.yaml
+++ b/.github/workflows/trivy.yaml
@@ -18,7 +18,7 @@ jobs:
uses: actions/checkout@v4
- name: Run Trivy scanner in repo mode
- uses: aquasecurity/trivy-action@0.33.1
+ uses: aquasecurity/trivy-action@0.35.0
with:
scan-type: "fs"
ignore-unfixed: true
diff --git a/__tests__/shared/components/challenge-detail/Header/index.jsx b/__tests__/shared/components/challenge-detail/Header/index.jsx
new file mode 100644
index 000000000..8ffdb6648
--- /dev/null
+++ b/__tests__/shared/components/challenge-detail/Header/index.jsx
@@ -0,0 +1,127 @@
+import React from 'react';
+import Renderer from 'react-test-renderer/shallow';
+
+import Header from 'components/challenge-detail/Header';
+
+function collectText(node) {
+ if (typeof node === 'string') {
+ return [node];
+ }
+
+ if (!React.isValidElement(node)) {
+ return [];
+ }
+
+ return React.Children.toArray(node.props.children)
+ .reduce((acc, child) => acc.concat(collectText(child)), []);
+}
+
+function renderHeader(challengeOverrides = {}) {
+ const renderer = new Renderer();
+ renderer.render(
+ ,
+ );
+
+ return renderer.getRenderOutput();
+}
+
+describe('Challenge detail header actions', () => {
+ test('hides registration and submission actions for classic task challenges', () => {
+ const output = renderHeader({
+ type: 'Task',
+ });
+
+ expect(collectText(output)).not.toContain('Register');
+ expect(collectText(output)).not.toContain('Unregister');
+ expect(collectText(output)).not.toContain('Submit a solution');
+ });
+
+ test('hides registration and submission actions for work-app task payloads', () => {
+ const output = renderHeader({
+ task: {
+ isTask: true,
+ },
+ });
+
+ expect(collectText(output)).not.toContain('Register');
+ expect(collectText(output)).not.toContain('Unregister');
+ expect(collectText(output)).not.toContain('Submit a solution');
+ });
+
+ test('hides registration and submission actions for pure v5 task payloads', () => {
+ const output = renderHeader({
+ legacy: {
+ pureV5Task: true,
+ },
+ });
+
+ expect(collectText(output)).not.toContain('Register');
+ expect(collectText(output)).not.toContain('Unregister');
+ expect(collectText(output)).not.toContain('Submit a solution');
+ });
+
+ test('shows registration and submission actions for non-task challenges', () => {
+ const output = renderHeader();
+
+ expect(collectText(output)).toContain('Register');
+ expect(collectText(output)).toContain('Submit a solution');
+ });
+});
diff --git a/__tests__/shared/components/challenge-detail/MySubmissions/SubmissionsList/index.jsx b/__tests__/shared/components/challenge-detail/MySubmissions/SubmissionsList/index.jsx
new file mode 100644
index 000000000..02f78f79b
--- /dev/null
+++ b/__tests__/shared/components/challenge-detail/MySubmissions/SubmissionsList/index.jsx
@@ -0,0 +1,47 @@
+import { getDisplayedScores } from '../../../../../../src/shared/components/challenge-detail/MySubmissions/SubmissionsList';
+
+describe('getDisplayedScores', () => {
+ test('shows final scores when a system review already produced one before review completes', () => {
+ expect(getDisplayedScores(
+ {
+ finalScore: 100,
+ initialScore: 100,
+ provisionalScore: 0,
+ },
+ {
+ phases: [
+ {
+ isOpen: true,
+ name: 'Registration',
+ scheduledStartDate: '2030-01-01T00:00:00.000Z',
+ },
+ ],
+ },
+ )).toEqual({
+ finalScore: 100,
+ provisionalScore: 100,
+ });
+ });
+
+ test('shows final scores once the review phase is complete', () => {
+ expect(getDisplayedScores(
+ {
+ finalScore: 100,
+ initialScore: 95,
+ provisionalScore: 0,
+ },
+ {
+ phases: [
+ {
+ isOpen: false,
+ name: 'Review',
+ scheduledStartDate: '2000-01-01T00:00:00.000Z',
+ },
+ ],
+ },
+ )).toEqual({
+ finalScore: 100,
+ provisionalScore: 95,
+ });
+ });
+});
diff --git a/__tests__/shared/containers/challenge-detail/index.jsx b/__tests__/shared/containers/challenge-detail/index.jsx
index cff13840e..19de44a21 100644
--- a/__tests__/shared/containers/challenge-detail/index.jsx
+++ b/__tests__/shared/containers/challenge-detail/index.jsx
@@ -2,19 +2,38 @@ import { getDisplayWinners, isWiproRegistrationBlocked } from 'containers/challe
describe('Challenge detail Wipro registration guard', () => {
test('blocks Wipro members when challenge disallows Wipro participation', () => {
- expect(isWiproRegistrationBlocked('member@wipro.com', false)).toBe(true);
+ expect(isWiproRegistrationBlocked('member@wipro.com', {
+ wiproAllowed: false,
+ type: 'Challenge',
+ })).toBe(true);
});
test('does not block Wipro members when challenge allows Wipro participation', () => {
- expect(isWiproRegistrationBlocked('member@wipro.com', true)).toBe(false);
+ expect(isWiproRegistrationBlocked('member@wipro.com', {
+ wiproAllowed: true,
+ type: 'Challenge',
+ })).toBe(false);
});
test('does not block non-Wipro members when challenge disallows Wipro participation', () => {
- expect(isWiproRegistrationBlocked('member@example.com', false)).toBe(false);
+ expect(isWiproRegistrationBlocked('member@example.com', {
+ wiproAllowed: false,
+ type: 'Challenge',
+ })).toBe(false);
});
test('matches Wipro domain case-insensitively and ignores surrounding spaces', () => {
- expect(isWiproRegistrationBlocked(' MEMBER@WIPRO.COM ', false)).toBe(true);
+ expect(isWiproRegistrationBlocked(' MEMBER@WIPRO.COM ', {
+ wiproAllowed: false,
+ type: 'Challenge',
+ })).toBe(true);
+ });
+
+ test('does not block Wipro members for Topgear Task even when the flag is false', () => {
+ expect(isWiproRegistrationBlocked('member@wipro.com', {
+ wiproAllowed: false,
+ type: 'Topgear Task',
+ })).toBe(false);
});
});
diff --git a/__tests__/shared/utils/challenge-detail/mm-final-results.test.js b/__tests__/shared/utils/challenge-detail/mm-final-results.test.js
new file mode 100644
index 000000000..b8e83b757
--- /dev/null
+++ b/__tests__/shared/utils/challenge-detail/mm-final-results.test.js
@@ -0,0 +1,59 @@
+/* eslint-env jest */
+import {
+ hasVisibleMmFinalResults,
+ isReviewPhaseComplete,
+ shouldShowFinalMmResults,
+} from '../../../../src/shared/utils/challenge-detail/mm-final-results';
+
+describe('mm-final-results utilities', () => {
+ const activeReviewChallenge = {
+ phases: [
+ {
+ isOpen: true,
+ name: 'Review',
+ scheduledStartDate: '2030-01-01T00:00:00.000Z',
+ },
+ ],
+ };
+
+ it('detects when the review phase has completed', () => {
+ expect(isReviewPhaseComplete({
+ phases: [
+ {
+ isOpen: false,
+ name: 'Review',
+ scheduledStartDate: '2000-01-01T00:00:00.000Z',
+ },
+ ],
+ })).toBe(true);
+ });
+
+ it('keeps final Marathon Match results hidden while review is active and no final score exists', () => {
+ expect(shouldShowFinalMmResults(activeReviewChallenge, [
+ {
+ finalRank: null,
+ submissions: [
+ {
+ finalScore: null,
+ },
+ ],
+ },
+ ])).toBe(false);
+ });
+
+ it('shows final Marathon Match results as soon as a final score is available', () => {
+ const mmSubmissions = [
+ {
+ finalRank: 1,
+ submissions: [
+ {
+ finalScore: 100,
+ },
+ ],
+ },
+ ];
+
+ expect(hasVisibleMmFinalResults(mmSubmissions)).toBe(true);
+ expect(shouldShowFinalMmResults(activeReviewChallenge, mmSubmissions)).toBe(true);
+ });
+});
diff --git a/__tests__/shared/utils/challenge-detail/my-submission-scores.test.js b/__tests__/shared/utils/challenge-detail/my-submission-scores.test.js
new file mode 100644
index 000000000..512e1b91b
--- /dev/null
+++ b/__tests__/shared/utils/challenge-detail/my-submission-scores.test.js
@@ -0,0 +1,70 @@
+/* eslint-env jest */
+import { getDisplayedScores } from '../../../../src/shared/components/challenge-detail/MySubmissions/SubmissionsList';
+
+describe('getDisplayedScores', () => {
+ it('shows final scores when a system review has already produced one', () => {
+ expect(getDisplayedScores(
+ {
+ finalScore: 100,
+ initialScore: 100,
+ provisionalScore: 0,
+ },
+ {
+ phases: [
+ {
+ isOpen: true,
+ name: 'Registration',
+ scheduledStartDate: '2030-01-01T00:00:00.000Z',
+ },
+ ],
+ },
+ )).toEqual({
+ finalScore: 100,
+ provisionalScore: 100,
+ });
+ });
+
+ it('hides final scores while review is active and no final result exists yet', () => {
+ expect(getDisplayedScores(
+ {
+ finalScore: null,
+ initialScore: 95,
+ provisionalScore: 0,
+ },
+ {
+ phases: [
+ {
+ isOpen: true,
+ name: 'Registration',
+ scheduledStartDate: '2030-01-01T00:00:00.000Z',
+ },
+ ],
+ },
+ )).toEqual({
+ finalScore: null,
+ provisionalScore: 95,
+ });
+ });
+
+ it('shows final scores once the review phase is complete', () => {
+ expect(getDisplayedScores(
+ {
+ finalScore: 100,
+ initialScore: 95,
+ provisionalScore: 0,
+ },
+ {
+ phases: [
+ {
+ isOpen: false,
+ name: 'Review',
+ scheduledStartDate: '2000-01-01T00:00:00.000Z',
+ },
+ ],
+ },
+ )).toEqual({
+ finalScore: 100,
+ provisionalScore: 95,
+ });
+ });
+});
diff --git a/__tests__/shared/utils/mm-review-summations.test.js b/__tests__/shared/utils/mm-review-summations.test.js
new file mode 100644
index 000000000..a6d721896
--- /dev/null
+++ b/__tests__/shared/utils/mm-review-summations.test.js
@@ -0,0 +1,128 @@
+/* eslint-env jest */
+import { buildMmSubmissionData } from '../../../src/shared/utils/mm-review-summations';
+
+describe('buildMmSubmissionData', () => {
+ it('keeps newer raw submissions that do not have review summations yet', () => {
+ const reviewSummations = [
+ {
+ aggregateScore: 100,
+ id: 'summation-old',
+ isProvisional: true,
+ reviewedDate: '2026-03-30T11:06:00.000Z',
+ submissionId: 'submission-old',
+ submitterHandle: 'alpha',
+ submitterId: '1001',
+ submitterMaxRating: 1800,
+ },
+ ];
+
+ const rawSubmissions = [
+ {
+ createdAt: '2026-03-30T11:06:00.000Z',
+ id: 'submission-old',
+ memberId: '1001',
+ registrant: {
+ memberHandle: 'alpha',
+ memberId: '1001',
+ rating: 1800,
+ },
+ status: 'completed',
+ },
+ {
+ createdAt: '2026-03-30T11:21:00.000Z',
+ id: 'submission-new',
+ memberId: '1001',
+ registrant: {
+ memberHandle: 'alpha',
+ memberId: '1001',
+ rating: 1800,
+ },
+ status: 'queued',
+ },
+ ];
+
+ const result = buildMmSubmissionData(reviewSummations, rawSubmissions);
+
+ expect(result).toHaveLength(1);
+ expect(result[0].member).toBe('alpha');
+ expect(result[0].provisionalRank).toBeNull();
+ expect(result[0].submissions).toHaveLength(2);
+ expect(result[0].submissions[0]).toEqual(expect.objectContaining({
+ provisionalScore: null,
+ status: 'queued',
+ submissionId: 'submission-new',
+ }));
+ expect(result[0].submissions[1]).toEqual(expect.objectContaining({
+ provisionalScore: 100,
+ status: 'completed',
+ submissionId: 'submission-old',
+ }));
+ });
+
+ it('builds submission rows from raw marathon match submissions when no summations exist', () => {
+ const rawSubmissions = [
+ {
+ createdAt: '2026-03-30T11:13:00.000Z',
+ id: 'submission-only',
+ memberId: '1002',
+ registrant: {
+ memberHandle: 'beta',
+ memberId: '1002',
+ rating: 1500,
+ },
+ status: 'failed',
+ },
+ ];
+
+ const result = buildMmSubmissionData([], rawSubmissions);
+
+ expect(result).toHaveLength(1);
+ expect(result[0]).toEqual(expect.objectContaining({
+ member: 'beta',
+ memberId: '1002',
+ provisionalRank: null,
+ finalRank: null,
+ registrant: expect.objectContaining({
+ memberHandle: 'beta',
+ userId: '1002',
+ }),
+ }));
+ expect(result[0].submissions).toEqual([
+ expect.objectContaining({
+ status: 'failed',
+ submissionId: 'submission-only',
+ }),
+ ]);
+ });
+
+ it('prefers initial scores over stale provisional scores from raw submissions', () => {
+ const rawSubmissions = [
+ {
+ createdAt: '2026-04-01T00:01:03.000Z',
+ finalScore: 100,
+ id: 'submission-stale-provisional',
+ initialScore: 100,
+ memberId: '1003',
+ provisionalScore: 0,
+ registrant: {
+ memberHandle: 'gamma',
+ memberId: '1003',
+ rating: 1700,
+ },
+ status: 'queued',
+ },
+ ];
+
+ const result = buildMmSubmissionData([], rawSubmissions);
+
+ expect(result).toHaveLength(1);
+ expect(result[0].submissions).toEqual([
+ expect.objectContaining({
+ finalScore: 100,
+ provisionalScore: 100,
+ status: 'queued',
+ submissionId: 'submission-stale-provisional',
+ }),
+ ]);
+ });
+});
diff --git a/src/shared/components/challenge-detail/Header/index.jsx b/src/shared/components/challenge-detail/Header/index.jsx
index 4779b90c4..77c945f40 100644
--- a/src/shared/components/challenge-detail/Header/index.jsx
+++ b/src/shared/components/challenge-detail/Header/index.jsx
@@ -148,6 +148,9 @@ export default function ChallengeHeader(props) {
const trackName = getTrackName(track);
const typeName = getTypeName(type);
+ const isTaskChallenge = typeName === 'Task'
+ || _.get(challenge, 'task.isTask') === true
+ || _.get(challenge, 'legacy.pureV5Task') === true;
const trackLower = trackName ? trackName.replace(' ', '-').toLowerCase() : 'design';
const eventNames = (events || []).map((event => (event.eventName || '').toUpperCase()));
@@ -184,9 +187,14 @@ export default function ChallengeHeader(props) {
const deadlineEnd = moment(nextPhase && phaseEndDate(nextPhase));
const currentTime = moment();
- const timeDiff = getTimeLeft(currentPhases || allOpenPhases[0], 'to go', true);
+ const activePhase = currentPhases || allOpenPhases[0];
+ const shouldShowCurrentPhase = !isTaskChallenge
+ || (activePhase && !isRegistrationPhase(activePhase));
+ const timeDiff = shouldShowCurrentPhase
+ ? getTimeLeft(activePhase, 'to go', true)
+ : null;
- if (!timeDiff.late) {
+ if (timeDiff && !timeDiff.late) {
timeDiff.text = timeDiff.text.replace('to go', '');
}
@@ -316,6 +324,72 @@ export default function ChallengeHeader(props) {
|| !isActivedChallenge;
const unregisterButtonDisabled = unregistering
|| registrationEnded || hasSubmissions || isLegacyMM;
+ let challengeActions = null;
+
+ if (!isTopCrowdChallenge && !isTaskChallenge) {
+ challengeActions = (
+
+ {hasRegistered ? (
+
+ Unregister
+
+ ) : (
+
+ Register
+
+ )}
+
+
+ Submit a solution
+
+ {
+ trackName === COMPETITION_TRACKS.DES && hasRegistered && !unregistering
+ && hasSubmissions && (
+
+ View Submissions
+
+ )
+ }
+
+ );
+ } else if (isTopCrowdChallenge) {
+ challengeActions = (
+
+ View details on Topcoder platform
+
+
+ );
+ }
return (
@@ -435,66 +509,7 @@ export default function ChallengeHeader(props) {
}
- {!isTopCrowdChallenge ? (
-
- {hasRegistered ? (
-
- Unregister
-
- ) : (
-
- Register
-
- )}
-
-
- Submit a solution
-
- {
- trackName === COMPETITION_TRACKS.DES && hasRegistered && !unregistering
- && hasSubmissions && (
-
- View Submissions
-
- )
- }
-
- ) : (
-
-
View details on Topcoder platform
-
-
- )}
+ {challengeActions}
@@ -503,6 +518,7 @@ export default function ChallengeHeader(props) {
{nextDeadlineMsg}
{
(status || '').toLowerCase() === 'active'
+ && timeDiff
&& (
{currentPhases && `${currentPhases.name} Ends In: `}
diff --git a/src/shared/components/challenge-detail/MySubmissions/SubmissionsList/index.jsx b/src/shared/components/challenge-detail/MySubmissions/SubmissionsList/index.jsx
index a17e8b913..5bae961d3 100644
--- a/src/shared/components/challenge-detail/MySubmissions/SubmissionsList/index.jsx
+++ b/src/shared/components/challenge-detail/MySubmissions/SubmissionsList/index.jsx
@@ -9,6 +9,7 @@ import moment from 'moment';
import { PrimaryButton, Modal } from 'topcoder-react-ui-kit';
import PT from 'prop-types';
import { services } from 'topcoder-react-lib';
+import { isReviewPhaseComplete } from 'utils/challenge-detail/mm-final-results';
import sortList from 'utils/challenge-detail/sort';
import { getSubmissionStatus } from 'utils/challenge-detail/submission-status';
@@ -74,6 +75,37 @@ const getSubmissionCreatedTime = (submission) => {
);
};
+/**
+ * Returns the scores that should be displayed for a Marathon Match submission row.
+ * Initial score is the authoritative provisional score for MM submissions, and
+ * final scores become visible once review is complete or the payload already
+ * includes a final result during review.
+ *
+ * @param {Object} submission submission attempt shown in My Submissions.
+ * @param {Object} challenge challenge that owns the submission.
+ * @returns {{ finalScore: number|null, provisionalScore: number|null }} display-ready scores.
+ */
+export function getDisplayedScores(submission = {}, challenge = {}) {
+ const toNumericScore = (value) => {
+ if (_.isNil(value) || value === '' || value === '-') {
+ return null;
+ }
+
+ const numeric = Number(value);
+ return Number.isFinite(numeric) ? numeric : null;
+ };
+
+ const initialScore = toNumericScore(_.get(submission, 'initialScore'));
+ const provisionalScore = toNumericScore(_.get(submission, 'provisionalScore'));
+ const finalScore = toNumericScore(_.get(submission, 'finalScore'));
+ const showFinalScore = isReviewPhaseComplete(challenge) || !_.isNil(finalScore);
+
+ return {
+ finalScore: showFinalScore ? finalScore : null,
+ provisionalScore: !_.isNil(initialScore) ? initialScore : provisionalScore,
+ };
+}
+
class SubmissionsListView extends React.Component {
constructor(props) {
super(props);
@@ -435,7 +467,7 @@ class SubmissionsListView extends React.Component {
{
sortedSubmissions.map((mySubmission) => {
- let { finalScore, provisionalScore } = mySubmission;
+ let { finalScore, provisionalScore } = getDisplayedScores(mySubmission, challenge);
if (_.isNumber(finalScore)) {
if (finalScore > 0) {
finalScore = finalScore.toFixed(2);
diff --git a/src/shared/components/challenge-detail/Submissions/SubmissionInformationModal/index.jsx b/src/shared/components/challenge-detail/Submissions/SubmissionInformationModal/index.jsx
index 328dcf0cf..ea235fe43 100644
--- a/src/shared/components/challenge-detail/Submissions/SubmissionInformationModal/index.jsx
+++ b/src/shared/components/challenge-detail/Submissions/SubmissionInformationModal/index.jsx
@@ -61,7 +61,7 @@ class SubmissionInformationModal extends React.Component {
render() {
const {
toggleTestcase, onClose, isLoadingSubmissionInformation,
- submissionInformation, isReviewPhaseComplete,
+ submissionInformation, showFinalResults,
} = this.props;
const submissionBasicInfo = isLoadingSubmissionInformation
? null : this.getSubmissionBasicInfo();
@@ -90,7 +90,7 @@ class SubmissionInformationModal extends React.Component {
- {(!submissionBasicInfo.finalScore && submissionBasicInfo.finalScore !== 0) || !isReviewPhaseComplete ? '-' : submissionBasicInfo.finalScore}
+ {(!submissionBasicInfo.finalScore && submissionBasicInfo.finalScore !== 0) || !showFinalResults ? '-' : submissionBasicInfo.finalScore}
{
- if (!isReviewPhaseComplete) {
+ if (!showFinalResults) {
return 'N/A';
}
if (finalScoreValue === null) {
@@ -139,7 +139,7 @@ export default function SubmissionHistoryRow({
SubmissionHistoryRow.defaultProps = {
finalScore: null,
provisionalScore: null,
- isReviewPhaseComplete: false,
+ showFinalResults: false,
isLoggedIn: false,
createdAt: null,
submissionTime: null,
@@ -169,7 +169,7 @@ SubmissionHistoryRow.propTypes = {
PT.oneOf([null]),
]),
challengeStatus: PT.string.isRequired,
- isReviewPhaseComplete: PT.bool,
+ showFinalResults: PT.bool,
auth: PT.shape().isRequired,
numWinners: PT.number.isRequired,
submissionId: PT.string.isRequired,
diff --git a/src/shared/components/challenge-detail/Submissions/SubmissionRow/index.jsx b/src/shared/components/challenge-detail/Submissions/SubmissionRow/index.jsx
index 65fd838b0..921c73d2b 100644
--- a/src/shared/components/challenge-detail/Submissions/SubmissionRow/index.jsx
+++ b/src/shared/components/challenge-detail/Submissions/SubmissionRow/index.jsx
@@ -20,7 +20,7 @@ import style from './style.scss';
export default function SubmissionRow({
isMM, isRDM, openHistory, member, submissions, toggleHistory, challengeStatus,
- isReviewPhaseComplete, finalRank, provisionalRank, onShowPopup, rating, viewAsTable,
+ showFinalResults, finalRank, provisionalRank, onShowPopup, rating, viewAsTable,
numWinners, auth, isLoggedIn, isF2F, isBugHunt,
}) {
const submissionList = Array.isArray(submissions) ? submissions : [];
@@ -66,7 +66,7 @@ export default function SubmissionRow({
};
const getFinalReviewResult = () => {
- if (!isReviewPhaseComplete) {
+ if (!showFinalResults) {
return 'N/A';
}
if (_.isNil(finalScore)) {
@@ -103,7 +103,7 @@ export default function SubmissionRow({
? submissionMoment.format('MMM DD, YYYY HH:mm')
: 'N/A';
- const finalRankDisplay = (isReviewPhaseComplete && _.isFinite(finalRank)) ? finalRank : 'N/A';
+ const finalRankDisplay = (showFinalResults && _.isFinite(finalRank)) ? finalRank : 'N/A';
const provisionalRankDisplay = _.isFinite(provisionalRank) ? provisionalRank : 'N/A';
const ratingDisplay = _.isFinite(rating) ? rating : '-';
const ratingLevelStyle = `col level-${getRatingLevel(rating)}`;
@@ -306,7 +306,7 @@ export default function SubmissionRow({
{
submissionList.map((submissionHistory, index) => (
{},
- isReviewPhaseComplete: false,
+ showFinalResults: false,
finalRank: null,
provisionalRank: null,
rating: null,
@@ -390,7 +390,7 @@ SubmissionRow.propTypes = {
})).isRequired,
rating: PT.number,
toggleHistory: PT.func,
- isReviewPhaseComplete: PT.bool,
+ showFinalResults: PT.bool,
finalRank: PT.number,
provisionalRank: PT.number,
onShowPopup: PT.func.isRequired,
diff --git a/src/shared/components/challenge-detail/Submissions/index.jsx b/src/shared/components/challenge-detail/Submissions/index.jsx
index 6082a0e94..61330f78d 100644
--- a/src/shared/components/challenge-detail/Submissions/index.jsx
+++ b/src/shared/components/challenge-detail/Submissions/index.jsx
@@ -21,6 +21,7 @@ import cn from 'classnames';
import { Button } from 'topcoder-react-ui-kit';
import DateSortIcon from 'assets/images/icon-date-sort.svg';
import SortIcon from 'assets/images/icon-sort.svg';
+import { shouldShowFinalMmResults as resolveShouldShowFinalMmResults } from 'utils/challenge-detail/mm-final-results';
import { getSubmissionId } from 'utils/submissions';
import { compressFiles } from 'utils/files';
@@ -109,7 +110,7 @@ class SubmissionsComponent extends React.Component {
this.getFlagFirstTry = this.getFlagFirstTry.bind(this);
this.updateSortedSubmissions = this.updateSortedSubmissions.bind(this);
this.sortSubmissions = this.sortSubmissions.bind(this);
- this.checkIsReviewPhaseComplete = this.checkIsReviewPhaseComplete.bind(this);
+ this.shouldShowFinalMmResults = this.shouldShowFinalMmResults.bind(this);
}
componentDidMount() {
@@ -208,7 +209,7 @@ class SubmissionsComponent extends React.Component {
/**
* Get submission sort parameter
*/
- getSubmissionsSortParam(isMM, isReviewPhaseComplete) {
+ getSubmissionsSortParam(isMM, showFinalMmResults) {
const {
submissionsSort,
} = this.props;
@@ -216,7 +217,7 @@ class SubmissionsComponent extends React.Component {
if (!field) {
field = 'Submission Date'; // default field for submission sorting
if (isMM) {
- if (isReviewPhaseComplete) {
+ if (showFinalMmResults) {
field = 'Final Rank';
} else {
field = 'Provisional Rank';
@@ -263,8 +264,8 @@ class SubmissionsComponent extends React.Component {
return;
}
const isMM = this.isMM();
- const isReviewPhaseComplete = this.checkIsReviewPhaseComplete();
- const { field, sort } = this.getSubmissionsSortParam(isMM, isReviewPhaseComplete);
+ const showFinalMmResults = this.shouldShowFinalMmResults();
+ const { field, sort } = this.getSubmissionsSortParam(isMM, showFinalMmResults);
// 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;
@@ -366,7 +367,7 @@ class SubmissionsComponent extends React.Component {
break;
}
case 'Final Rank': {
- if (isReviewPhaseComplete) {
+ if (showFinalMmResults) {
valueA = toRankValue(_.get(a, 'finalRank'));
valueB = toRankValue(_.get(b, 'finalRank'));
}
@@ -409,22 +410,15 @@ class SubmissionsComponent extends React.Component {
}
/**
- * Check if review phase complete
- */
- checkIsReviewPhaseComplete() {
+ * Returns whether Marathon Match final results should be shown.
+ */
+ shouldShowFinalMmResults() {
const {
challenge,
+ mmSubmissions,
} = this.props;
- const allPhases = challenge.phases || [];
-
- let isReviewPhaseComplete = false;
- _.forEach(allPhases, (phase) => {
- if (phase.name === 'Review' && !phase.isOpen && moment(phase.scheduledStartDate).isBefore()) {
- isReviewPhaseComplete = true;
- }
- });
- return isReviewPhaseComplete;
+ return resolveShouldShowFinalMmResults(challenge, mmSubmissions);
}
render() {
@@ -465,9 +459,9 @@ class SubmissionsComponent extends React.Component {
const isMM = this.isMM();
const isRDM = checkIsRDM(challenge);
const isLoggedIn = !_.isEmpty(auth.tokenV3);
- const isReviewPhaseComplete = this.checkIsReviewPhaseComplete();
+ const showFinalMmResults = this.shouldShowFinalMmResults();
- const { field, sort } = this.getSubmissionsSortParam(isMM, isReviewPhaseComplete);
+ const { field, sort } = this.getSubmissionsSortParam(isMM, showFinalMmResults);
const revertSort = (sort === 'desc') ? 'asc' : 'desc';
const {
@@ -1007,7 +1001,7 @@ class SubmissionsComponent extends React.Component {
sortedSubmissions.map((submission, index) => (
(
)
}
diff --git a/src/shared/containers/challenge-detail/index.jsx b/src/shared/containers/challenge-detail/index.jsx
index 3327d436b..9bd40228c 100644
--- a/src/shared/containers/challenge-detail/index.jsx
+++ b/src/shared/containers/challenge-detail/index.jsx
@@ -105,10 +105,12 @@ const WIPRO_REGISTRATION_SUPPORT_MESSAGE = 'If you think this is an error, pleas
/**
* Checks whether challenge registration should be blocked for Wipro members.
* @param {String} email User email.
- * @param {Boolean} wiproAllowed Challenge-level flag.
+ * @param {Object} challenge Challenge details used to determine registration policy.
* @return {Boolean}
*/
-export function isWiproRegistrationBlocked(email, wiproAllowed) {
+export function isWiproRegistrationBlocked(email, challenge = {}) {
+ if (getTypeName(challenge) === 'Topgear Task') return false;
+ const wiproAllowed = _.get(challenge, 'wiproAllowed');
if (wiproAllowed !== false) return false;
return /@wipro\.com$/i.test(_.trim(email || ''));
}
@@ -412,9 +414,7 @@ class ChallengeDetailPageContainer extends React.Component {
communityId,
} = this.props;
const userEmail = _.get(auth, 'user.email');
- const wiproAllowed = _.get(challenge, 'wiproAllowed');
-
- if (isWiproRegistrationBlocked(userEmail, wiproAllowed)) {
+ if (isWiproRegistrationBlocked(userEmail, challenge)) {
fireErrorMessage(
WIPRO_REGISTRATION_BLOCKED_MESSAGE,
WIPRO_REGISTRATION_SUPPORT_MESSAGE,
@@ -1150,20 +1150,23 @@ function mapStateToProps(state, props) {
const challengeId = String(props.match.params.challengeId);
const cl = state.challengeListing;
const { lookup: { allCountries, reviewTypes } } = state;
+ const challenge = state.challenge.details || {};
const reviewSummations = extractArrayFromStateSlice(
state.challenge.reviewSummations,
challengeId,
);
+ const rawChallengeSubmissions = Array.isArray(challenge.submissions)
+ ? challenge.submissions
+ : (_.get(challenge, 'submissions.data') || []);
let mmSubmissions = extractArrayFromStateSlice(state.challenge.mmSubmissions, challengeId);
- if (!mmSubmissions.length && reviewSummations.length) {
- mmSubmissions = buildMmSubmissionData(reviewSummations);
+ if (reviewSummations.length || rawChallengeSubmissions.length) {
+ mmSubmissions = buildMmSubmissionData(reviewSummations, rawChallengeSubmissions);
}
const { auth } = state;
let statisticsData = extractArrayFromStateSlice(state.challenge.statisticsData, challengeId);
if (!hasRenderableStatisticsData(statisticsData) && reviewSummations.length) {
statisticsData = buildStatisticsData(reviewSummations);
}
- const challenge = state.challenge.details || {};
const reviewSummationLookup = reviewSummations.length
? buildReviewSummationLookup(reviewSummations)
: null;
@@ -1304,6 +1307,12 @@ function mapStateToProps(state, props) {
return toNumericScore(match.aggregateScore);
};
+ const initialScore = toNumericScore(normalizedAttempt.initialScore);
+ if (!_.isNil(initialScore)) {
+ normalizedAttempt.initialScore = initialScore;
+ normalizedAttempt.provisionalScore = initialScore;
+ }
+
const hasProvisionalScore = !_.isNil(normalizedAttempt.provisionalScore);
const hasFinalScore = !_.isNil(normalizedAttempt.finalScore);
diff --git a/src/shared/services/copilotOpportunities.js b/src/shared/services/copilotOpportunities.js
index b26de4d50..eaaa3b0e6 100644
--- a/src/shared/services/copilotOpportunities.js
+++ b/src/shared/services/copilotOpportunities.js
@@ -1,18 +1,20 @@
import { config } from 'topcoder-react-utils';
-const v5ApiUrl = config.API.V5;
+const v6ApiUrl = config.API.V6;
/**
* Fetches copilot opportunities.
*
- * @param {number} page - Page number (1-based).
+ * @param {number} page - Page number (1-based; invalid or below 1 is sent as 1 for v6 API).
* @param {number} pageSize - Number of items per page.
* @param {string} sort - Sort order (e.g., 'createdAt desc').
* @returns {Promise