diff --git a/.circleci/config.yml b/.circleci/config.yml index b7220115d5..35f86a19b6 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -230,14 +230,14 @@ workflows: filters: branches: only: - - develop + - i-v5-develop-24Aug # This is alternate dev env for parallel testing - "build-test": context : org-global filters: branches: only: - - integration-v5-challenge-api + - hot-fix # This is alternate dev env for parallel testing - "build-qa": context : org-global @@ -258,7 +258,7 @@ workflows: filters: branches: only: - - develop + - i-v5-develop-24Aug # Production builds are exectuted # when PR is merged to the master # Don't change anything in this configuration diff --git a/package-lock.json b/package-lock.json index 7ffac5e8f9..7d082a3e47 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14573,7 +14573,7 @@ "dev": true }, "navigation-component": { - "version": "github:topcoder-platform/navigation-component#3ff9165a545ba6210ea4cd992d0dd26b52610055", + "version": "github:topcoder-platform/navigation-component#4938269d5610f3f8bff65f23459a37d01121e6f6", "from": "github:topcoder-platform/navigation-component#develop", "requires": { "classnames": "^2.2.6", @@ -22369,7 +22369,7 @@ } }, "tc-accounts": { - "version": "git+https://github.com/appirio-tech/accounts-app.git#9d0daa189dbf5127ad6ca470ed1683eeb2495ac7", + "version": "git+https://github.com/appirio-tech/accounts-app.git#33f4aeb29d07dbe4e6d8206d718830fd54943f47", "from": "git+https://github.com/appirio-tech/accounts-app.git#dev", "requires": { "@uirouter/angularjs": "^1.0.0", @@ -33196,9 +33196,9 @@ "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" }, "topcoder-react-lib": { - "version": "1000.19.44", - "resolved": "https://registry.npmjs.org/topcoder-react-lib/-/topcoder-react-lib-1000.19.44.tgz", - "integrity": "sha512-WoBJbt5w50Hdho9xCzUFwCL/JOQLE0mfMCY3Y0YMUNAToieDpE2RloOwHZpqez+QgE1sxehLGQxlf61M9NZ95A==", + "version": "1000.19.48", + "resolved": "https://registry.npmjs.org/topcoder-react-lib/-/topcoder-react-lib-1000.19.48.tgz", + "integrity": "sha512-TguboxXulPHmE8FGGBgxYlYNjEU7mDZwOT74SYzeAJLl4bZr6z7yQCwGIlksB9dzq6beAvDIm4sYXqTCRuUInw==", "requires": { "auth0-js": "^6.8.4", "config": "^3.2.0", diff --git a/src/shared/components/challenge-detail/Specification/SideBar/index.jsx b/src/shared/components/challenge-detail/Specification/SideBar/index.jsx index c945832dbb..b2c5877bf5 100644 --- a/src/shared/components/challenge-detail/Specification/SideBar/index.jsx +++ b/src/shared/components/challenge-detail/Specification/SideBar/index.jsx @@ -36,13 +36,13 @@ export default function SideBar({ const scorecardURL = `${config.URL.ONLINE_REVIEW}/review/actions/ViewScorecard?scid=`; const faqURL = config.URL.INFO.DESIGN_CHALLENGE_SUBMISSION; let submissionLimitDisplay = 'Unlimited'; - const submissionLimit = _.find(metadata, { type: 'submissionLimit' }); + const submissionLimit = _.find(metadata, { name: 'submissionLimit' }); const fileTypes = _.find(metadata, { name: 'fileTypes' }); if (submissionLimit) { if (submissionLimit.value === 1) { submissionLimitDisplay = '1 submission'; - } else if (submissionLimit > 1) { + } else if (submissionLimit.value > 1) { submissionLimitDisplay = `${submissionLimit.value} submissions`; } } diff --git a/src/shared/components/challenge-detail/Specification/index.jsx b/src/shared/components/challenge-detail/Specification/index.jsx index cbe1c9c619..96d728e4f5 100644 --- a/src/shared/components/challenge-detail/Specification/index.jsx +++ b/src/shared/components/challenge-detail/Specification/index.jsx @@ -59,7 +59,12 @@ export default function ChallengeDetailsView(props) { forumId, } = legacy; - const allowStockArt = _.find(metadata, { type: 'allowStockArt' }); + let stockArtValue = ''; + const allowStockArt = _.find(metadata, { name: 'allowStockArt' }); + if (allowStockArt) { + stockArtValue = allowStockArt.value; + } + let environment = ''; const environmentData = _.find(metadata, { name: 'environment' }); if (environmentData) { @@ -103,7 +108,7 @@ export default function ChallengeDetailsView(props) { const toolbarConnector = new ToolbarConnector(); const isSaving = specsTabState === SPECS_TAB_STATES.SAVING; - const stockArtText = allowStockArt + const stockArtText = stockArtValue ? 'Stock photography is allowed in this challenge.' : 'Stock photography is not allowed in this challenge. All submitted elements must be designed solely by you.'; @@ -170,7 +175,7 @@ export default function ChallengeDetailsView(props) { description && (
-

+

Challenge Overview

{ @@ -195,7 +200,7 @@ export default function ChallengeDetailsView(props) { finalSubmissionGuidelines && (
-

+

Final Submission Guidelines

{ @@ -228,7 +233,7 @@ export default function ChallengeDetailsView(props) { description && (
-

+

Challenge Summary

{ @@ -246,8 +251,8 @@ export default function ChallengeDetailsView(props) { /> ) } -

-

+

+

Please read the challenge specification carefully and watch the forums for any questions or feedback concerning this challenge. It is important that you @@ -259,10 +264,10 @@ export default function ChallengeDetailsView(props) { ) }

-

+

How To Submit

-
    +
    • New to Studio? ‌ @@ -311,10 +316,10 @@ export default function ChallengeDetailsView(props) {
-

+

Winner Selection

-

+

Submissions are viewable to the client as they are entered into the challenge. Winners are selected by the client and are chosen solely at the client's discretion. @@ -324,13 +329,13 @@ export default function ChallengeDetailsView(props) { ) }

-

+

Payments

{ isWipro ? (
-

+

For employees of Wipro Technologies, following are the payment terms. Winner/s would be awarded the prize money on successful completion and acceptance of the submission by @@ -354,7 +359,7 @@ export default function ChallengeDetailsView(props) {

) : ( -

+

Topcoder will compensate members in accordance with our standard payment policies, unless otherwise specified in this challenge. For information on payment policies, setting up your profile to receive payments, and general payment questions, please refer to diff --git a/src/shared/components/challenge-detail/Specification/styles.scss b/src/shared/components/challenge-detail/Specification/styles.scss index e7ad57a1e0..04b3f00171 100644 --- a/src/shared/components/challenge-detail/Specification/styles.scss +++ b/src/shared/components/challenge-detail/Specification/styles.scss @@ -78,6 +78,8 @@ $tc-link-visited: #0c4e98; align-items: baseline; word-break: break-word; + @include linkStyle; + @include xs-to-sm { flex-direction: column; } @@ -118,7 +120,7 @@ $tc-link-visited: #0c4e98; } } -.h2 { +h2 { @include roboto-bold; font-size: 20px; @@ -127,7 +129,7 @@ $tc-link-visited: #0c4e98; margin: (6 * $base-unit) 0 (2 * $base-unit); } -.h3 { +h3 { @include roboto-bold; font-size: 15px; @@ -137,7 +139,17 @@ $tc-link-visited: #0c4e98; text-transform: uppercase; } -.p { +p, +em, +strong { + @include roboto-regular; + + font-size: 15px; + color: $tc-gray-90; + line-height: 25px; +} + +p { @include roboto-regular; font-size: 15px; @@ -149,7 +161,7 @@ $tc-link-visited: #0c4e98; margin: (2 * $base-unit) 0 (3 * $base-unit); } -.ul { +ul { @include roboto-regular; margin: 0 0 5px 20px; diff --git a/src/shared/components/challenge-detail/Submissions/index.jsx b/src/shared/components/challenge-detail/Submissions/index.jsx index 4f54b38aff..b8e2e61c02 100644 --- a/src/shared/components/challenge-detail/Submissions/index.jsx +++ b/src/shared/components/challenge-detail/Submissions/index.jsx @@ -175,8 +175,8 @@ class SubmissionsComponent extends React.Component { valueA = `${a.member || ''}`.toLowerCase(); valueB = `${b.member || ''}`.toLowerCase(); } else { - valueA = `${a.createdBy}`.toLowerCase(); - valueB = `${b.createdBy}`.toLowerCase(); + valueA = _.get(a.registrant, 'memberHandle', ''); + valueB = _.get(b.registrant, 'memberHandle', ''); } valueIsString = true; break; @@ -314,12 +314,12 @@ class SubmissionsComponent extends React.Component { {`#${s.id}`} - {s.createdBy} + {_.get(s.registrant, 'memberHandle', '')}

@@ -705,7 +705,7 @@ class SubmissionsComponent extends React.Component { { !isMM && ( sortedSubmissions.map(s => ( -
+
{ !isF2F && !isBugHunt && (
@@ -715,12 +715,12 @@ class SubmissionsComponent extends React.Component { }
diff --git a/src/shared/components/challenge-listing/ChallengeCard/Status/index.jsx b/src/shared/components/challenge-listing/ChallengeCard/Status/index.jsx index 3cb06f6c46..d3315863aa 100644 --- a/src/shared/components/challenge-listing/ChallengeCard/Status/index.jsx +++ b/src/shared/components/challenge-listing/ChallengeCard/Status/index.jsx @@ -6,6 +6,7 @@ import LeaderboardAvatar from 'components/challenge-listing/LeaderboardAvatar'; import { config, Link } from 'topcoder-react-utils'; import { TABS as DETAIL_TABS } from 'actions/page/challenge-details'; import 'moment-duration-format'; +import { phaseEndDate } from 'utils/challenge-listing/helper'; import { getTimeLeft, } from 'utils/challenge-detail/helper'; @@ -271,7 +272,7 @@ export default function ChallengeStatus(props) {
{getTimeLeft(statusPhase, 'to go').text} diff --git a/src/shared/components/challenge-listing/Tooltips/ProgressBarTooltip/index.jsx b/src/shared/components/challenge-listing/Tooltips/ProgressBarTooltip/index.jsx index 675e181bc4..432afcde8b 100644 --- a/src/shared/components/challenge-listing/Tooltips/ProgressBarTooltip/index.jsx +++ b/src/shared/components/challenge-listing/Tooltips/ProgressBarTooltip/index.jsx @@ -17,6 +17,7 @@ import _ from 'lodash'; import React from 'react'; import PT from 'prop-types'; import Tooltip from 'components/Tooltip'; +import { phaseStartDate, phaseEndDate } from 'utils/challenge-listing/helper'; import LoaderIcon from '../../../Loader/Loader'; import './style.scss'; @@ -95,31 +96,32 @@ function Tip(props) { if (!c || _.isEmpty(c)) return
; const allPhases = c.phases || []; - const endPhaseDate = Math.max(...allPhases.map(d => new Date(d.scheduledEndDate))); + const endPhaseDate = Math.max(...allPhases.map(d => phaseEndDate(d))); const registrationPhase = allPhases.find(phase => phase.name === 'Registration'); const submissionPhase = allPhases.find(phase => phase.name === 'Submission'); + const checkpointPhase = allPhases.find(phase => phase.name === 'Checkpoint Submission'); if (registrationPhase) { steps.push({ - date: new Date(registrationPhase.scheduledStartDate), + date: phaseStartDate(registrationPhase), name: 'Start', }); } - if (c.checkpointSubmissionEndDate) { + if (checkpointPhase) { steps.push({ - date: new Date(c.checkpointSubmissionEndDate), + date: phaseEndDate(checkpointPhase), name: 'Checkpoint', }); } const iterativeReviewPhase = allPhases.find(phase => phase.isOpen && phase.name === 'Iterative Review'); if (iterativeReviewPhase) { steps.push({ - date: new Date(iterativeReviewPhase.scheduledEndDate), + date: phaseEndDate(iterativeReviewPhase), name: 'Iterative Review', }); } else if (submissionPhase) { steps.push({ - date: new Date(submissionPhase.scheduledEndDate), + date: phaseEndDate(submissionPhase), name: 'Submission', }); } diff --git a/src/shared/components/tc-communities/Header/index.jsx b/src/shared/components/tc-communities/Header/index.jsx index ffc2e0862d..c610f947e6 100644 --- a/src/shared/components/tc-communities/Header/index.jsx +++ b/src/shared/components/tc-communities/Header/index.jsx @@ -55,7 +55,7 @@ function Header(props) { const AUTH_URL = config.URL.AUTH; const normalizedProfile = profile && _.clone(profile); const isZurichCompetitor = (profile && profile.groups) ? _.intersection( - _.map(profile.groups, 'id'), + _.map(profile.groups, 'oldId'), meta.competitorsGroupIds, ) : []; diff --git a/src/shared/containers/SubmissionPage.jsx b/src/shared/containers/SubmissionPage.jsx index da4e84aeba..80579b39ab 100644 --- a/src/shared/containers/SubmissionPage.jsx +++ b/src/shared/containers/SubmissionPage.jsx @@ -40,6 +40,19 @@ class SubmissionsPageContainer extends React.Component { getCommunitiesList(auth); } + componentWillReceiveProps() { + const { + challenge, + history, + } = this.props; + + const { details } = challenge; + + if (details && details.isLegacyChallenge && !history.location.pathname.includes(details.id)) { + history.push(`/challenges/${details.id}/submit`, history.state); + } + } + /* A child component has called their submitForm() prop, prepare the passed form data for submission and create a submit action */ handleSubmit(body) { @@ -150,6 +163,7 @@ SubmissionsPageContainer.propTypes = { submissionFilestackData: filestackDataProp.isRequired, winners: PT.arrayOf(PT.object).isRequired, loadChallengeDetails: PT.func.isRequired, + history: PT.shape().isRequired, }; /** diff --git a/src/shared/containers/challenge-detail/index.jsx b/src/shared/containers/challenge-detail/index.jsx index 65cb2e339f..334758cd55 100644 --- a/src/shared/containers/challenge-detail/index.jsx +++ b/src/shared/containers/challenge-detail/index.jsx @@ -721,7 +721,7 @@ function mapStateToProps(state, props) { if (challenge.submissions) { challenge.submissions = challenge.submissions.map(submission => ({ ...submission, - registrant: _.find(challenge.registrants, { memberHandle: submission.createdBy }), + registrant: _.find(challenge.registrants, r => (`${r.memberId}` === `${submission.memberId}`)), })); } diff --git a/src/shared/utils/challenge-detail/helper.jsx b/src/shared/utils/challenge-detail/helper.jsx index 2a6fb4b8be..38b8317f90 100644 --- a/src/shared/utils/challenge-detail/helper.jsx +++ b/src/shared/utils/challenge-detail/helper.jsx @@ -9,6 +9,7 @@ import { challenge as challengeUtils } from 'topcoder-react-lib'; import { config } from 'topcoder-react-utils'; import Prize from 'components/challenge-listing/ChallengeCard/Prize'; import { BUCKETS, getBuckets } from 'utils/challenge-listing/buckets'; +import { phaseEndDate } from 'utils/challenge-listing/helper'; const Filter = challengeUtils.filter; @@ -42,7 +43,7 @@ export function getEndDate(challenge) { if (type === 'First2Finish' && challenge.status === 'Completed') { phases = challenge.phases.filter(p => p.phaseType === 'Iterative Review' && p.phaseStatus === 'Closed'); } - const endPhaseDate = Math.max(...phases.map(d => new Date(d.scheduledEndDate))); + const endPhaseDate = Math.max(...phases.map(d => phaseEndDate(d))); return moment(endPhaseDate).format('MMM DD'); } @@ -65,7 +66,7 @@ export function getTimeLeft( return { late: false, text: FF_TIME_LEFT_MSG }; } - let time = moment(phase.scheduledEndDate).diff(); + let time = moment(phaseEndDate(phase)).diff(); const late = time < 0; if (late) time = -time; diff --git a/src/shared/utils/challenge-listing/helper.js b/src/shared/utils/challenge-listing/helper.js new file mode 100644 index 0000000000..192dafcc04 --- /dev/null +++ b/src/shared/utils/challenge-listing/helper.js @@ -0,0 +1,31 @@ +import moment from 'moment'; + +/** + * Returns phase's end date. + * @param {Object} phase + * @return {Date} + */ +export function phaseEndDate(phase) { + // Case 1: phase is still open. take the `scheduledEndDate` + // Case 2: phase is not open but `scheduledStartDate` is a future date. + // This means phase is not yet started. So take the `scheduledEndDate` + if (phase.isOpen || moment(phase.scheduledStartDate).isAfter()) { + return new Date(phase.scheduledEndDate); + } + // for other cases, take the `actualEndDate` as phase is already closed + return new Date(phase.actualEndDate); +} + +/** + * Returns phase's start date. + * @param {Object} phase + * @return {Date} + */ +export function phaseStartDate(phase) { + // Case 1: Phase is not yet started. take the `scheduledStartDate` + if (phase.isOpen !== true && moment(phase.scheduledStartDate).isAfter()) { + return new Date(phase.scheduledStartDate); + } + // For all other cases, take the `actualStartDate` as phase is already started + return new Date(phase.actualStartDate); +} diff --git a/src/shared/utils/challenge-listing/sort.js b/src/shared/utils/challenge-listing/sort.js index 8588f29048..65e6803f0a 100644 --- a/src/shared/utils/challenge-listing/sort.js +++ b/src/shared/utils/challenge-listing/sort.js @@ -3,7 +3,8 @@ */ import moment from 'moment'; -import { sumBy } from 'lodash'; +import { find, sumBy } from 'lodash'; +import { phaseStartDate, phaseEndDate } from './helper'; export const SORTS = { CURRENT_PHASE: 'current-phase', @@ -21,11 +22,53 @@ export const SORTS = { export default { [SORTS.CURRENT_PHASE]: { - func: (a, b) => a.status.localeCompare(b.status), + func: (a, b) => { + const aPhases = a.phases || []; + const bPhases = b.phases || []; + const aPhase = aPhases + .filter(p => p.name !== 'Registration' && p.isOpen) + .sort((p1, p2) => moment(p1.scheduledEndDate).diff(p2.scheduledEndDate))[0]; + const bPhase = bPhases + .filter(p => p.name !== 'Registration' && p.isOpen) + .sort((p1, p2) => moment(p1.scheduledEndDate).diff(p2.scheduledEndDate))[0]; + + let aPhaseName = 'Stalled'; + let bPhaseName = 'Stalled'; + if (!aPhase && a.type === 'First2Finish' && aPhases.length) { + aPhaseName = 'Submission'; + } + if (!bPhase && b.type === 'First2Finish' && bPhases.length) { + bPhaseName = 'Submission'; + } + + if (aPhase) aPhaseName = aPhase.name; + else if (a.status === 'Draft') aPhaseName = 'Draft'; + + if (bPhase) bPhaseName = bPhase.name; + else if (b.status === 'Draft') bPhaseName = 'Draft'; + + return aPhaseName.localeCompare(bPhaseName); + }, name: 'Current phase', }, [SORTS.MOST_RECENT]: { - func: (a, b) => moment(b.registrationStartDate).diff(a.registrationStartDate), + func: (a, b) => { + const getChallengeStartDate = (challenge) => { + // extract the phases from `challenge.phases`, + // as `challenge.registrationStartDate` returned from API is not reliable + const registrationPhase = find(challenge.phases, p => p.name === 'Registration'); + const submissionPhase = find(challenge.phases, p => p.name === 'Submission'); + // registration phase exists + if (registrationPhase) { + return moment(phaseStartDate(registrationPhase)); + } + // registration phase doesnt exist, This is possibly a F2F or TSK. Take submission phase + return moment(phaseStartDate(submissionPhase)); + }; + const aChallengeStartDate = getChallengeStartDate(a); + const bChallengeStartDate = getChallengeStartDate(b); + return bChallengeStartDate.diff(aChallengeStartDate); + }, name: 'Most recent', }, [SORTS.NUM_REGISTRANTS]: { @@ -42,8 +85,21 @@ export default { }, [SORTS.TIME_TO_REGISTER]: { func: (a, b) => { - const aDate = moment(a.registrationEndDate || a.submissionEndTimestamp); - const bDate = moment(b.registrationEndDate || b.submissionEndTimestamp); + const getRegistrationEndDate = (challenge) => { + // extract the registration phase from `challenge.phases`, + // as `challenge.registrationEndDate` returned from API is not reliable + const registrationPhase = find(challenge.phases, p => p.name === 'Registration'); + const submissionPhase = find(challenge.phases, p => p.name === 'Submission'); + // case 1: registration phase exists + if (registrationPhase) { + return moment(phaseEndDate(registrationPhase)); + } + // case 2: registration phase doesn't exist. Take submission phase instead. + return moment(phaseEndDate(submissionPhase)); + }; + + const aDate = getRegistrationEndDate(a); + const bDate = getRegistrationEndDate(b); if (aDate.isBefore() && bDate.isAfter()) return 1; if (aDate.isAfter() && bDate.isBefore()) return -1; @@ -55,11 +111,23 @@ export default { }, [SORTS.TIME_TO_SUBMIT]: { func: (a, b) => { - function nextSubEndDate(o) { - if (o.checkpointSubmissionEndDate && moment(o.checkpointSubmissionEndDate).isAfter()) { - return moment(o.checkpointSubmissionEndDate); + function nextSubEndDate(challenge) { + // extract the submission and checkpoint (if any) phases from `challenge.phases`, + // as `challenge.submissionEndDate` returned from API is not reliable + const checkpointPhase = find(challenge.phases, p => p.name === 'Checkpoint Submission'); + const submissionPhase = find(challenge.phases, p => p.name === 'Submission'); + // Case 1: challenge has checkpoint submission phase + if (!!checkpointPhase === true) { + // Case 1.1: checkpoint submission phase is still open. + // then take the `scheduledEndDate` of this phase. + // Case 1.2: checkpoint submission phase is closed + // but its `scheduledStartDate` is a future date. + // This means this phase is not yet started. Take the `scheduledEndDate` of this phase. + if (checkpointPhase.isOpen || moment(checkpointPhase.scheduledStartDate).isAfter()) { + return moment(checkpointPhase.scheduledEndDate); + } } - return moment(o.submissionEndTimestamp); + return moment(phaseEndDate(submissionPhase)); } const aDate = nextSubEndDate(a);