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
}>