diff --git a/__tests__/shared/components/challenge-detail/Header/index.jsx b/__tests__/shared/components/challenge-detail/Header/index.jsx
index 8ffdb6648..5270b4962 100644
--- a/__tests__/shared/components/challenge-detail/Header/index.jsx
+++ b/__tests__/shared/components/challenge-detail/Header/index.jsx
@@ -1,10 +1,39 @@
import React from 'react';
import Renderer from 'react-test-renderer/shallow';
+import { errors as mockedErrors } from 'topcoder-react-lib';
import Header from 'components/challenge-detail/Header';
+import TabSelector from 'components/challenge-detail/Header/TabSelector';
+
+jest.mock('topcoder-react-lib', () => ({
+ challenge: {
+ filter: {},
+ },
+ errors: {
+ fireErrorMessage: jest.fn(),
+ },
+ services: {
+ api: {},
+ },
+ tc: {
+ CHALLENGE_STATUS: {
+ ACTIVE: 'ACTIVE',
+ COMPLETED: 'COMPLETED',
+ },
+ OLD_COMPETITION_TRACKS: {},
+ },
+}));
+
+jest.mock('topcoder-react-ui-kit', () => ({
+ PrimaryButton: () => null,
+}));
+
+jest.mock('react-responsive', () => ({
+ useMediaQuery: () => true,
+}));
function collectText(node) {
- if (typeof node === 'string') {
+ if (typeof node === 'string' || typeof node === 'number') {
return [node];
}
@@ -16,7 +45,28 @@ function collectText(node) {
.reduce((acc, child) => acc.concat(collectText(child)), []);
}
-function renderHeader(challengeOverrides = {}) {
+function findSubmitAction(node) {
+ if (!React.isValidElement(node)) {
+ return null;
+ }
+
+ if ((node.props.to || node.props.onClick)
+ && collectText(node).includes('Submit a solution')) {
+ return node;
+ }
+
+ const children = React.Children.toArray(node.props.children);
+ for (let index = 0; index < children.length; index += 1) {
+ const match = findSubmitAction(children[index]);
+ if (match) {
+ return match;
+ }
+ }
+
+ return null;
+}
+
+function renderHeader(challengeOverrides = {}, propOverrides = {}) {
const renderer = new Renderer();
renderer.render(
,
);
@@ -84,6 +135,10 @@ function renderHeader(challengeOverrides = {}) {
}
describe('Challenge detail header actions', () => {
+ beforeEach(() => {
+ mockedErrors.fireErrorMessage.mockClear();
+ });
+
test('hides registration and submission actions for classic task challenges', () => {
const output = renderHeader({
type: 'Task',
@@ -124,4 +179,88 @@ describe('Challenge detail header actions', () => {
expect(collectText(output)).toContain('Register');
expect(collectText(output)).toContain('Submit a solution');
});
+
+ test('shows the limit-reached message instead of opening the submission page', () => {
+ const output = renderHeader({
+ metadata: [{
+ name: 'submissionLimit',
+ value: JSON.stringify({
+ count: '1',
+ limit: 'true',
+ unlimited: 'false',
+ }),
+ }],
+ }, {
+ hasRegistered: true,
+ mySubmissions: [{ id: 'submission-id' }],
+ });
+ const submitAction = findSubmitAction(output);
+
+ expect(submitAction.props.to).toBeUndefined();
+ submitAction.props.onClick();
+
+ expect(mockedErrors.fireErrorMessage).toHaveBeenCalledWith(
+ 'Submission Limit Reached',
+ 'This challenge allows only one submission, and you\'ve already submitted.'
+ + ' To replace it, delete your existing submission first.',
+ );
+ });
+
+ test('keeps the submission page available while slots remain', () => {
+ const output = renderHeader({
+ metadata: [{
+ name: 'submissionLimit',
+ value: JSON.stringify({
+ count: '2',
+ limit: 'true',
+ unlimited: 'false',
+ }),
+ }],
+ }, {
+ hasRegistered: true,
+ mySubmissions: [{ id: 'submission-id' }],
+ });
+ const submitAction = findSubmitAction(output);
+
+ expect(submitAction.props.to).toBe('/challenges/challenge-id/submit');
+ expect(submitAction.props.onClick).toBeUndefined();
+ expect(mockedErrors.fireErrorMessage).not.toHaveBeenCalled();
+ });
+});
+
+describe('Challenge detail tab counts', () => {
+ test('renders the MM submission total independently of loaded attempts', () => {
+ const renderer = new Renderer();
+ renderer.render(
+ ,
+ );
+
+ const text = collectText(renderer.getRenderOutput());
+ const mySubmissionsLabelIndex = text.indexOf('My Submissions');
+
+ expect(text[mySubmissionsLabelIndex + 1]).toBe(3);
+ });
});
diff --git a/__tests__/shared/components/challenge-detail/MySubmissions/SubmissionsList/index.jsx b/__tests__/shared/components/challenge-detail/MySubmissions/SubmissionsList/index.jsx
index bb70ec89f..6f39e5a31 100644
--- a/__tests__/shared/components/challenge-detail/MySubmissions/SubmissionsList/index.jsx
+++ b/__tests__/shared/components/challenge-detail/MySubmissions/SubmissionsList/index.jsx
@@ -1,9 +1,68 @@
-import {
+import { shallow } from 'enzyme';
+import React from 'react';
+import { getSubmissionDownloadUrl as mockedGetSubmissionDownloadUrl } from 'services/submissions';
+
+import SubmissionsListView, {
getDisplayedScores,
isActiveTestStatus,
getSubmissionTestProgress,
} from '../../../../../../src/shared/components/challenge-detail/MySubmissions/SubmissionsList';
+jest.mock('services/submissions', () => ({
+ getSubmissionDownloadUrl: jest.fn(),
+}));
+
+/**
+ * Renders a My Submissions row and returns its visible provisional score.
+ * Tests use this to compare score display behavior across scorer processes.
+ *
+ * @param {String} testProcess Review API test process metadata.
+ * @returns {String} provisional score displayed in the submission row.
+ * @throws {Error} Propagates errors raised while shallow-rendering SubmissionsListView.
+ */
+function renderProvisionalScore(testProcess) {
+ const wrapper = shallow(
+ ,
+ );
+ const scoreColumn = wrapper.find('div')
+ .filterWhere((node) => {
+ const firstChild = node.children().at(0);
+ return firstChild.type() === 'div'
+ && firstChild.text() === 'Provisional Score';
+ })
+ .first();
+
+ return scoreColumn.find('span').last().text();
+}
+
describe('getDisplayedScores', () => {
test('shows final scores when a system review already produced one before review completes', () => {
expect(getDisplayedScores(
@@ -77,3 +136,68 @@ describe('isActiveTestStatus', () => {
expect(isActiveTestStatus('FAILED')).toBe(false);
});
});
+
+describe('Marathon Match provisional score display', () => {
+ it('keeps the completed provisional score visible while system tests are running', () => {
+ expect(renderProvisionalScore('system')).toBe('75.82');
+ });
+
+ it('keeps the provisional score hidden while provisional tests are running', () => {
+ expect(renderProvisionalScore('provisional')).toBe('-');
+ });
+});
+
+describe('Marathon Match submission download', () => {
+ let originalCreateObjectURL;
+
+ beforeEach(() => {
+ originalCreateObjectURL = window.URL.createObjectURL;
+ });
+
+ afterEach(() => {
+ window.URL.createObjectURL = originalCreateObjectURL;
+ jest.restoreAllMocks();
+ jest.clearAllMocks();
+ });
+
+ it('opens the browser-safe signed URL for an opaque submission id', async () => {
+ const submissionId = 'BKzPfVv24EcINT';
+ const downloadUrl = 'https://storage.example.test/signed-mm-submission';
+ const link = document.createElement('a');
+ link.click = jest.fn();
+ const createObjectURL = jest.fn();
+ window.URL.createObjectURL = createObjectURL;
+ jest.spyOn(document, 'createElement').mockReturnValue(link);
+ mockedGetSubmissionDownloadUrl.mockResolvedValue(downloadUrl);
+
+ const wrapper = shallow(
+ ,
+ );
+
+ wrapper.find('button[aria-label="Download submission"]').prop('onClick')();
+ await Promise.resolve();
+
+ expect(mockedGetSubmissionDownloadUrl).toHaveBeenCalledWith('token-v3', submissionId);
+ expect(link.href).toBe(downloadUrl);
+ expect(link.getAttribute('download')).toBe(`submission-${submissionId}.zip`);
+ expect(link.click).toHaveBeenCalledTimes(1);
+ expect(document.body.contains(link)).toBe(false);
+ expect(createObjectURL).not.toHaveBeenCalled();
+ });
+});
diff --git a/__tests__/shared/components/challenge-detail/Submissions/SubmissionRow/index.jsx b/__tests__/shared/components/challenge-detail/Submissions/SubmissionRow/index.jsx
index 688e3b884..15f744a3d 100644
--- a/__tests__/shared/components/challenge-detail/Submissions/SubmissionRow/index.jsx
+++ b/__tests__/shared/components/challenge-detail/Submissions/SubmissionRow/index.jsx
@@ -53,15 +53,17 @@ function collectText(node) {
}
/**
- * Shallow-renders an MM row and returns its provisional score column text.
+ * Shallow-renders an MM row and returns the selected score column text.
* Tests use this to compare score display behavior across scorer process states.
*
+ * @param {String} scoreHeader Header for the score column to inspect.
* @param {String} testProcess Review API test process metadata.
* @param {String} testStatus Review API test status metadata.
- * @returns {Array} Provisional score column label and displayed value.
+ * @param {Number|null} finalScore Final score supplied by Review API.
+ * @returns {Array} Score column label and displayed value.
* @throws {Error} Propagates errors raised while shallow-rendering SubmissionRow.
*/
-function renderProvisionalScore(testProcess, testStatus) {
+function renderScore(scoreHeader, testProcess, testStatus, finalScore = null) {
const renderer = new Renderer();
renderer.render(
,
);
- const column = findColumnByHeader(renderer.getRenderOutput(), 'PROVISIONAL SCORE');
+ const column = findColumnByHeader(renderer.getRenderOutput(), scoreHeader);
return collectText(column);
}
describe('Marathon Match provisional score', () => {
it('shows N/A while provisional tests are still running', () => {
- expect(renderProvisionalScore('provisional', 'IN PROGRESS'))
+ expect(renderScore('PROVISIONAL SCORE', 'provisional', 'IN PROGRESS'))
.toEqual(['PROVISIONAL SCORE', 'N/A']);
});
it('keeps a completed zero provisional score visible', () => {
- expect(renderProvisionalScore('provisional', 'SUCCESS'))
+ expect(renderScore('PROVISIONAL SCORE', 'provisional', 'SUCCESS'))
.toEqual(['PROVISIONAL SCORE', 0]);
});
it('keeps the provisional score visible while system tests are running', () => {
- expect(renderProvisionalScore('system', 'IN PROGRESS'))
+ expect(renderScore('PROVISIONAL SCORE', 'system', 'IN PROGRESS'))
.toEqual(['PROVISIONAL SCORE', 0]);
});
});
+
+describe('Marathon Match final score', () => {
+ it('shows N/A while system tests are still running', () => {
+ expect(renderScore('FINAL SCORE', 'system', 'IN PROGRESS', 0))
+ .toEqual(['FINAL SCORE', 'N/A']);
+ });
+
+ it('keeps a completed zero final score visible', () => {
+ expect(renderScore('FINAL SCORE', 'system', 'SUCCESS', 0))
+ .toEqual(['FINAL SCORE', 0]);
+ });
+});
diff --git a/__tests__/shared/containers/SubmissionPage.jsx b/__tests__/shared/containers/SubmissionPage.jsx
new file mode 100644
index 000000000..9d8541e1b
--- /dev/null
+++ b/__tests__/shared/containers/SubmissionPage.jsx
@@ -0,0 +1,157 @@
+import { errors as mockedErrors } from 'topcoder-react-lib';
+import { getChallengeSubmissions as mockedGetChallengeSubmissions } from 'services/submissions';
+
+import { SubmissionsPageContainer } from 'containers/SubmissionPage';
+
+jest.mock('topcoder-react-lib', () => ({
+ actions: {
+ challenge: {},
+ },
+ errors: {
+ fireErrorMessage: jest.fn(),
+ },
+}));
+
+jest.mock('services/submissions', () => ({
+ getChallengeSubmissions: jest.fn(),
+}));
+
+jest.mock('actions/page/submission', () => ({
+ page: {
+ submission: {},
+ },
+}));
+jest.mock('actions/page/challenge-details', () => ({
+ page: {
+ challengeDetails: {},
+ },
+}));
+jest.mock('actions/tc-communities', () => ({
+ tcCommunity: {},
+}));
+jest.mock('components/SubmissionPage', () => () => null);
+jest.mock('components/tc-communities/AccessDenied', () => ({
+ __esModule: true,
+ CAUSE: {
+ NOT_AUTHORIZED: 'NOT_AUTHORIZED',
+ },
+ default: () => null,
+}));
+jest.mock('components/LoadingIndicator', () => () => null);
+jest.mock('topcoder-react-ui-kit', () => ({
+ PrimaryButton: () => null,
+}));
+
+function createContainerProps(overrides = {}) {
+ return {
+ challenge: {},
+ challengeId: 'challenge-id',
+ metadata: [],
+ submit: jest.fn(),
+ tokenV2: 'token-v2',
+ tokenV3: 'token-v3',
+ track: 'Design',
+ userId: 'member-id',
+ ...overrides,
+ };
+}
+
+describe('SubmissionsPageContainer submission limits', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ test('skips the limit lookup for unlimited challenges', async () => {
+ const props = createContainerProps();
+ const container = new SubmissionsPageContainer(props);
+ const body = {};
+
+ await container.handleSubmit(body);
+
+ expect(mockedGetChallengeSubmissions).not.toHaveBeenCalled();
+ expect(props.submit).toHaveBeenCalledWith(
+ 'token-v3',
+ 'token-v2',
+ 'challenge-id',
+ body,
+ 'Design',
+ );
+ });
+
+ test('submits while a limited challenge still has an available slot', async () => {
+ mockedGetChallengeSubmissions.mockResolvedValue({
+ data: [{ id: 'submission-1' }],
+ });
+ const props = createContainerProps({
+ metadata: [{
+ name: 'submissionLimit',
+ value: JSON.stringify({
+ count: '2',
+ limit: 'true',
+ unlimited: 'false',
+ }),
+ }],
+ });
+ const container = new SubmissionsPageContainer(props);
+ const body = {};
+
+ await container.handleSubmit(body);
+
+ expect(mockedGetChallengeSubmissions).toHaveBeenCalledWith(
+ 'token-v3',
+ 'challenge-id',
+ { memberId: 'member-id' },
+ );
+ expect(props.submit).toHaveBeenCalled();
+ expect(mockedErrors.fireErrorMessage).not.toHaveBeenCalled();
+ });
+
+ test('shows the limit message and does not submit when the limit is reached', async () => {
+ mockedGetChallengeSubmissions.mockResolvedValue({
+ data: [{ id: 'submission-1' }],
+ });
+ const props = createContainerProps({
+ metadata: [{
+ name: 'submissionLimit',
+ value: JSON.stringify({
+ count: '1',
+ limit: 'true',
+ unlimited: 'false',
+ }),
+ }],
+ });
+ const container = new SubmissionsPageContainer(props);
+
+ await container.handleSubmit({});
+
+ expect(props.submit).not.toHaveBeenCalled();
+ expect(mockedErrors.fireErrorMessage).toHaveBeenCalledWith(
+ 'Submission Limit Reached',
+ 'This challenge allows only one submission, and you\'ve already submitted.'
+ + ' To replace it, delete your existing submission first.',
+ );
+ });
+
+ test('does not submit when the existing-submission lookup fails', async () => {
+ mockedGetChallengeSubmissions.mockRejectedValue(new Error('network error'));
+ const props = createContainerProps({
+ metadata: [{
+ name: 'submissionLimit',
+ value: JSON.stringify({
+ count: '1',
+ limit: 'true',
+ unlimited: 'false',
+ }),
+ }],
+ });
+ const container = new SubmissionsPageContainer(props);
+
+ await container.handleSubmit({});
+
+ expect(props.submit).not.toHaveBeenCalled();
+ expect(mockedErrors.fireErrorMessage).toHaveBeenCalledWith(
+ 'Unable to Verify Submission Limit',
+ 'We could not verify your existing submissions. Please try again.',
+ );
+ });
+});
diff --git a/__tests__/shared/containers/challenge-detail/index.jsx b/__tests__/shared/containers/challenge-detail/index.jsx
index 5fef65aa9..59b7497c4 100644
--- a/__tests__/shared/containers/challenge-detail/index.jsx
+++ b/__tests__/shared/containers/challenge-detail/index.jsx
@@ -4,6 +4,7 @@ import {
isGroupedChallenge,
isGroupedChallengeAccessError,
isWiproRegistrationBlocked,
+ mapStateToProps,
shouldLoginForGroupedChallenge,
shouldLoginForGroupedChallengeError,
} from 'containers/challenge-detail';
@@ -76,6 +77,64 @@ describe('Challenge detail winners filter', () => {
});
});
+describe('Challenge detail My Submissions count', () => {
+ test('uses the total attempt count when only the latest MM submission is loaded', () => {
+ const state = {
+ auth: {
+ user: {
+ handle: 'member',
+ userId: '123',
+ },
+ },
+ challenge: {
+ checkpoints: {},
+ details: {
+ id: 'challenge-id',
+ registrants: [{ memberHandle: 'member', memberId: '123' }],
+ submissions: [],
+ },
+ mmSubmissions: {
+ challengeId: 'challenge-id',
+ data: [{
+ member: 'member',
+ memberId: '123',
+ submissionCount: 3,
+ submissions: [{ submissionId: 'latest-submission' }],
+ }],
+ },
+ reviewSummations: [],
+ statisticsData: [],
+ },
+ challengeListing: {},
+ domain: {},
+ lookup: {
+ allCountries: [],
+ reviewTypes: [],
+ },
+ page: {
+ challengeDetails: {
+ feedbackOpen: {},
+ },
+ },
+ tcCommunities: {
+ list: {},
+ },
+ terms: {},
+ topcoderHeader: {},
+ };
+
+ const props = mapStateToProps(state, {
+ challengesUrl: '/challenges',
+ match: {
+ params: { challengeId: 'challenge-id' },
+ },
+ });
+
+ expect(props.mySubmissions).toHaveLength(1);
+ expect(props.mySubmissionsCount).toBe(3);
+ });
+});
+
describe('Challenge detail grouped challenge login guard', () => {
beforeEach(() => {
document.cookie = 'tc_utm=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/';
diff --git a/__tests__/shared/services/submissions.js b/__tests__/shared/services/submissions.js
index 9e2060b22..bacffb1db 100644
--- a/__tests__/shared/services/submissions.js
+++ b/__tests__/shared/services/submissions.js
@@ -1,6 +1,9 @@
/* eslint-env jest */
import { config } from 'topcoder-react-utils';
-import { getChallengeSubmissions } from '../../../src/shared/services/submissions';
+import {
+ getChallengeSubmissions,
+ getSubmissionDownloadUrl,
+} from '../../../src/shared/services/submissions';
const baseUrl = `${config.API.V6}/submissions`;
@@ -35,6 +38,36 @@ describe('submissions service', () => {
global.fetch = jest.fn();
});
+ it('returns a signed submission URL without following the storage redirect', async () => {
+ global.fetch.mockResolvedValueOnce({
+ ok: true,
+ json: () => Promise.resolve({
+ url: ' https://storage.example.test/signed-submission ',
+ }),
+ });
+
+ const result = await getSubmissionDownloadUrl('token-v3', 'submission/id');
+
+ expect(result).toBe('https://storage.example.test/signed-submission');
+ expect(global.fetch).toHaveBeenCalledTimes(1);
+ expect(global.fetch).toHaveBeenCalledWith(
+ `${baseUrl}/submission%2Fid/download-url`,
+ expect.objectContaining({ method: 'GET' }),
+ );
+ expect(global.fetch.mock.calls[0][1].headers.get('Authorization'))
+ .toBe('Bearer token-v3');
+ });
+
+ it('rejects a submission download response without a signed URL', async () => {
+ global.fetch.mockResolvedValueOnce({
+ ok: true,
+ json: () => Promise.resolve({}),
+ });
+
+ await expect(getSubmissionDownloadUrl('token-v3', 'submission-id'))
+ .rejects.toThrow('Submission download URL is missing');
+ });
+
it('loads every submissions page reported by metadata', async () => {
global.fetch
.mockResolvedValueOnce({
diff --git a/__tests__/shared/utils/challenge-detail/submission-limit.test.js b/__tests__/shared/utils/challenge-detail/submission-limit.test.js
new file mode 100644
index 000000000..80281bf86
--- /dev/null
+++ b/__tests__/shared/utils/challenge-detail/submission-limit.test.js
@@ -0,0 +1,75 @@
+/* eslint-env jest */
+import {
+ getSubmissionLimit,
+ getSubmissionLimitReachedMessage,
+} from '../../../../src/shared/utils/challenge-detail/submission-limit';
+
+describe('getSubmissionLimit', () => {
+ test('returns null when submission-limit metadata is missing', () => {
+ expect(getSubmissionLimit([])).toBeNull();
+ });
+
+ test('returns null for the current unlimited payload', () => {
+ expect(getSubmissionLimit([{
+ name: 'submissionLimit',
+ value: JSON.stringify({
+ count: '',
+ limit: 'false',
+ unlimited: 'true',
+ }),
+ }])).toBeNull();
+ });
+
+ test('returns the count for the current limited payload', () => {
+ expect(getSubmissionLimit([{
+ name: 'submissionLimit',
+ value: JSON.stringify({
+ count: '3',
+ limit: 'true',
+ unlimited: 'false',
+ }),
+ }])).toBe(3);
+ });
+
+ test('supports legacy numeric values', () => {
+ expect(getSubmissionLimit([{
+ name: 'submissionLimit',
+ value: 1,
+ }])).toBe(1);
+ expect(getSubmissionLimit([{
+ name: 'submissionLimit',
+ value: '2',
+ }])).toBe(2);
+ });
+
+ test('returns null for malformed and invalid counts', () => {
+ expect(getSubmissionLimit([{
+ name: 'submissionLimit',
+ value: '{invalid',
+ }])).toBeNull();
+ expect(getSubmissionLimit([{
+ name: 'submissionLimit',
+ value: JSON.stringify({
+ count: '0',
+ limit: 'true',
+ unlimited: 'false',
+ }),
+ }])).toBeNull();
+ });
+});
+
+describe('getSubmissionLimitReachedMessage', () => {
+ test('uses the requested singular limit message', () => {
+ expect(getSubmissionLimitReachedMessage(1)).toBe(
+ 'This challenge allows only one submission, and you\'ve already submitted.'
+ + ' To replace it, delete your existing submission first.',
+ );
+ });
+
+ test('uses a plural message for larger limits', () => {
+ expect(getSubmissionLimitReachedMessage(3)).toBe(
+ 'This challenge allows only 3 submissions, and you\'ve already reached that limit.'
+ + ' To replace one, delete an existing submission first.',
+ );
+ });
+});
diff --git a/src/shared/components/challenge-detail/Header/TabSelector/index.jsx b/src/shared/components/challenge-detail/Header/TabSelector/index.jsx
index a02f2177b..6916a4ac9 100644
--- a/src/shared/components/challenge-detail/Header/TabSelector/index.jsx
+++ b/src/shared/components/challenge-detail/Header/TabSelector/index.jsx
@@ -39,6 +39,7 @@ export default function ChallengeViewSelector(props) {
trackLower,
hasRegistered,
mySubmissions,
+ mySubmissionsCount,
onSort,
viewAsTable,
} = props;
@@ -133,6 +134,9 @@ export default function ChallengeViewSelector(props) {
}
const numOfSub = numOfSubmissions + (numOfCheckpointSubmissions || 0);
+ const mySubmissionsBadgeCount = _.isFinite(mySubmissionsCount)
+ ? mySubmissionsCount
+ : mySubmissions.length;
const forumId = _.get(challenge, 'legacy.forumId') || 0;
const discuss = _.get(challenge, 'discussions', []).filter(d => (
_.toLower(d.type) === 'challenge' && !_.isEmpty(d.url)
@@ -247,7 +251,7 @@ export default function ChallengeViewSelector(props) {
styleName={getSelectorStyle(selectedView, DETAIL_TABS.MY_SUBMISSIONS)}
>
My Submissions
- {mySubmissions.length}
+ {mySubmissionsBadgeCount}
) : null
}
@@ -358,7 +362,7 @@ export default function ChallengeViewSelector(props) {
{
currentSelected === DETAIL_TABS.MY_SUBMISSIONS && hasRegistered
&& isMM && mySubmissions && (
- {mySubmissions.length}
+ {mySubmissionsBadgeCount}
)
}
{
@@ -447,6 +451,7 @@ ChallengeViewSelector.defaultProps = {
numOfRegistrants: 0,
numOfCheckpointSubmissions: 0,
numOfSubmissions: 0,
+ mySubmissionsCount: null,
};
ChallengeViewSelector.propTypes = {
@@ -477,6 +482,7 @@ ChallengeViewSelector.propTypes = {
trackLower: PT.string.isRequired,
hasRegistered: PT.bool.isRequired,
mySubmissions: PT.arrayOf(PT.shape()).isRequired,
+ mySubmissionsCount: PT.number,
onSort: PT.func.isRequired,
viewAsTable: PT.bool.isRequired,
};
diff --git a/src/shared/components/challenge-detail/Header/index.jsx b/src/shared/components/challenge-detail/Header/index.jsx
index 77c945f40..9f6786431 100644
--- a/src/shared/components/challenge-detail/Header/index.jsx
+++ b/src/shared/components/challenge-detail/Header/index.jsx
@@ -8,6 +8,7 @@
import _ from 'lodash';
import moment from 'moment';
import 'moment-duration-format';
+import { errors } from 'topcoder-react-lib';
import { isMM, getTrackName, getTypeName } from 'utils/challenge';
import PT from 'prop-types';
@@ -20,6 +21,10 @@ import {
getTimeLeft,
isRegistrationPhase,
} from 'utils/challenge-detail/helper';
+import {
+ getSubmissionLimit,
+ getSubmissionLimitReachedMessage,
+} from 'utils/challenge-detail/submission-limit';
import LeftArrow from 'assets/images/arrow-prev-blue.svg';
import IconsOpenInNew from 'assets/images/open_in_new.svg';
@@ -38,6 +43,7 @@ import style from './style.scss';
/* Holds day and hour range in ms. */
const HOUR_MS = 60 * 60 * 1000;
const DAY_MS = 24 * HOUR_MS;
+const { fireErrorMessage } = errors;
export default function ChallengeHeader(props) {
const {
@@ -63,6 +69,7 @@ export default function ChallengeHeader(props) {
isMenuOpened,
submissionEnded,
mySubmissions,
+ mySubmissionsCount,
openForRegistrationChallenges,
onSort,
viewAsTable,
@@ -100,6 +107,9 @@ export default function ChallengeHeader(props) {
}
const showDeadlineDetail = showDeadlineDetailProp;
const isActivedChallenge = `${status}`.indexOf(CHALLENGE_STATUS.ACTIVE) >= 0;
+ const submissionLimit = getSubmissionLimit(metadata);
+ const isSubmissionLimitReached = submissionLimit !== null
+ && mySubmissions.length >= submissionLimit;
const allPhases = _.filter(challenge.phases || [], p => p.name !== 'Post-Mortem');
const sortedAllPhases = _.cloneDeep(allPhases)
@@ -359,7 +369,15 @@ export default function ChallengeHeader(props) {
fireErrorMessage(
+ 'Submission Limit Reached',
+ getSubmissionLimitReachedMessage(submissionLimit),
+ )
+ : undefined}
+ to={isSubmissionLimitReached
+ ? undefined
+ : `${challengesUrl}/${challengeId}/submit`}
forceA
>
@@ -571,6 +589,7 @@ export default function ChallengeHeader(props) {
hasRegistered={hasRegistered}
checkpointCount={checkpointCount}
mySubmissions={mySubmissions}
+ mySubmissionsCount={mySubmissionsCount}
onSort={onSort}
viewAsTable={viewAsTable}
/>
@@ -585,6 +604,7 @@ ChallengeHeader.defaultProps = {
isMenuOpened: false,
hasThriveArticles: false,
hasRecommendedChallenges: false,
+ mySubmissionsCount: null,
};
ChallengeHeader.propTypes = {
@@ -639,6 +659,7 @@ ChallengeHeader.propTypes = {
hasFirstPlacement: PT.bool.isRequired,
isMenuOpened: PT.bool,
mySubmissions: PT.arrayOf(PT.shape()).isRequired,
+ mySubmissionsCount: PT.number,
openForRegistrationChallenges: PT.shape().isRequired,
onSort: PT.func.isRequired,
viewAsTable: PT.bool.isRequired,
diff --git a/src/shared/components/challenge-detail/MySubmissions/SubmissionsList/index.jsx b/src/shared/components/challenge-detail/MySubmissions/SubmissionsList/index.jsx
index 512e20fed..647c82f7c 100644
--- a/src/shared/components/challenge-detail/MySubmissions/SubmissionsList/index.jsx
+++ b/src/shared/components/challenge-detail/MySubmissions/SubmissionsList/index.jsx
@@ -8,9 +8,9 @@ import _ from 'lodash';
import moment from 'moment';
import { PrimaryButton, Modal } from 'topcoder-react-ui-kit';
import PT from 'prop-types';
-import { services } from 'topcoder-react-lib';
import sortList from 'utils/challenge-detail/sort';
import { getSubmissionStatus } from 'utils/challenge-detail/submission-status';
+import { getSubmissionDownloadUrl } from 'services/submissions';
import IconClose from 'assets/images/icon-close-green.svg';
import DateSortIcon from 'assets/images/icon-date-sort.svg';
@@ -26,8 +26,6 @@ import ArtifactsDownloadIcon from '../../../SubmissionManagement/Icons/IconDownl
// import SearchIcon from '../../../SubmissionManagement/Icons/IconSearch.svg';
import style from './styles.scss';
-const { getService } = services.submissions;
-
const collectReviewSummations = (submission) => {
const summations = [];
if (!submission) {
@@ -646,7 +644,8 @@ class SubmissionsListView extends React.Component {
sortedSubmissions.map((mySubmission) => {
let { finalScore, provisionalScore } = getDisplayedScores(mySubmission);
const testProgress = getSubmissionTestProgress(mySubmission);
- const hideProvisionalScore = isActiveTestStatus(testProgress.status);
+ const hideProvisionalScore = testProgress.process !== 'system'
+ && isActiveTestStatus(testProgress.status);
if (_.isNumber(finalScore)) {
if (finalScore > 0) {
finalScore = finalScore.toFixed(2);
@@ -763,14 +762,16 @@ class SubmissionsListView extends React.Component {
? (
Download Submission
}>