Skip to content

Commit 4242acb

Browse files
committed
Enable multi-round design challenges in WM
https://topcoder.atlassian.net/browse/PROD-2894
1 parent 6d85168 commit 4242acb

File tree

12 files changed

+491
-28
lines changed

12 files changed

+491
-28
lines changed

config/constants/development.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,5 +43,6 @@ module.exports = {
4343
// if idle for this many minutes, show user a prompt saying they'll be logged out
4444
IDLE_TIMEOUT_MINUTES: 10,
4545
// duration to show the prompt saying user will be logged out, before actually logging out the user
46-
IDLE_TIMEOUT_GRACE_MINUTES: 5
46+
IDLE_TIMEOUT_GRACE_MINUTES: 5,
47+
MULTI_ROUND_CHALLENGE_TEMPLATE_ID: 'd4201ca4-8437-4d63-9957-3f7708184b07'
4748
}

config/constants/production.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,5 +41,6 @@ module.exports = {
4141
FILE_PICKER_REGION: 'us-east-1',
4242
FILE_PICKER_CNAME: 'fs.topcoder.com',
4343
IDLE_TIMEOUT_MINUTES: 10,
44-
IDLE_TIMEOUT_GRACE_MINUTES: 5
44+
IDLE_TIMEOUT_GRACE_MINUTES: 5,
45+
MULTI_ROUND_CHALLENGE_TEMPLATE_ID: 'd4201ca4-8437-4d63-9957-3f7708184b07'
4546
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
@import "../../../styles/includes";
2+
3+
.row {
4+
box-sizing: border-box;
5+
display: flex;
6+
flex-direction: row;
7+
margin: 30px 30px 0 30px;
8+
align-content: space-between;
9+
justify-content: flex-start;
10+
11+
.field {
12+
@include upto-sm {
13+
display: block;
14+
padding-bottom: 10px;
15+
}
16+
17+
label {
18+
@include roboto-bold();
19+
20+
font-size: 16px;
21+
line-height: 19px;
22+
font-weight: 500;
23+
color: $tc-gray-80;
24+
}
25+
26+
&.col1 {
27+
max-width: 185px;
28+
min-width: 185px;
29+
margin-right: 14px;
30+
white-space: nowrap;
31+
display: flex;
32+
align-items: center;
33+
flex-grow: 1;
34+
35+
span {
36+
color: $tc-red;
37+
}
38+
}
39+
40+
&.col2.error {
41+
color: $tc-red;
42+
margin-top: -25px;
43+
}
44+
&.col2 {
45+
align-self: flex-end;
46+
width: 80%;
47+
margin-bottom: auto;
48+
margin-top: auto;
49+
display: flex;
50+
flex-direction: row;
51+
max-width: 600px;
52+
min-width: 600px;
53+
}
54+
}
55+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import _ from 'lodash'
2+
import React from 'react'
3+
import PropTypes from 'prop-types'
4+
import Select from '../../Select'
5+
import cn from 'classnames'
6+
import styles from './ChallengeType-Field.module.scss'
7+
8+
const ChallengeTypeField = ({ types, onUpdateSelect, challenge, disabled }) => {
9+
return (
10+
<>
11+
<div className={styles.row}>
12+
<div className={cn(styles.field, styles.col1)}>
13+
<label htmlFor='type'>Challenge Type <span>*</span> :</label>
14+
</div>
15+
<div className={cn(styles.field, styles.col2, { [styles.disabled]: disabled })}>
16+
<Select
17+
name='challenge_type'
18+
options={_.map(types, type => ({ label: type, value: type }))}
19+
placeholder='Challenge Type'
20+
isClearable={false}
21+
onChange={(e) => onUpdateSelect(e.value, false, 'challengeType')}
22+
isDisabled={disabled}
23+
/>
24+
</div>
25+
</div>
26+
{ challenge.submitTriggered && !challenge.challengeType && <div className={styles.row}>
27+
<div className={cn(styles.field, styles.col1)} />
28+
<div className={cn(styles.field, styles.col2, styles.error)}>
29+
Challenge Type is required field
30+
</div>
31+
</div> }
32+
</>
33+
)
34+
}
35+
36+
ChallengeTypeField.defaultProps = {
37+
types: [],
38+
disabled: false
39+
}
40+
41+
ChallengeTypeField.propTypes = {
42+
types: PropTypes.arrayOf(PropTypes.shape()).isRequired,
43+
onUpdateSelect: PropTypes.func.isRequired,
44+
challenge: PropTypes.shape().isRequired,
45+
disabled: PropTypes.bool
46+
}
47+
48+
export default ChallengeTypeField

src/components/ChallengeEditor/ChallengeView/index.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,14 @@ import AssignedMemberField from '../AssignedMember-Field'
2020
import { getResourceRoleByName } from '../../../util/tc'
2121
import { isBetaMode } from '../../../util/cookie'
2222
import { loadGroupDetails } from '../../../actions/challenges'
23-
import { REVIEW_TYPES, CONNECT_APP_URL, PHASE_PRODUCT_CHALLENGE_ID_FIELD } from '../../../config/constants'
23+
import {
24+
REVIEW_TYPES,
25+
CONNECT_APP_URL,
26+
PHASE_PRODUCT_CHALLENGE_ID_FIELD,
27+
MULTI_ROUND_CHALLENGE_TEMPLATE_ID
28+
} from '../../../config/constants'
2429
import PhaseInput from '../../PhaseInput'
30+
import CheckpointPrizesField from '../CheckpointPrizes-Field'
2531

2632
const ChallengeView = ({
2733
projectDetail,
@@ -91,6 +97,7 @@ const ChallengeView = ({
9197
const showTimeline = false // disables the timeline for time being https://github.com/topcoder-platform/challenge-engine-ui/issues/706
9298
const isTask = _.get(challenge, 'task.isTask', false)
9399
const phases = _.get(challenge, 'phases', [])
100+
const showCheckpointPrizes = _.get(challenge, 'timelineTemplateId') === MULTI_ROUND_CHALLENGE_TEMPLATE_ID
94101

95102
return (
96103
<div className={styles.wrapper}>
@@ -224,6 +231,11 @@ const ChallengeView = ({
224231
readOnly
225232
/>}
226233
<ChallengePrizesField challenge={challenge} readOnly />
234+
{
235+
showCheckpointPrizes && (
236+
<CheckpointPrizesField challenge={challenge} readOnly />
237+
)
238+
}
227239
<CopilotFeeField challenge={challenge} readOnly />
228240
<ChallengeTotalField challenge={challenge} />
229241
</div>

src/components/ChallengeEditor/CheckpointPrizes-Field/CheckpointPrizes-Field.module.scss

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,42 @@
6161
color: $tc-red;
6262
cursor: pointer;
6363
}
64+
65+
.dollarIcon {
66+
margin-left: 10px;
67+
color: $tc-black;
68+
cursor: pointer;
69+
}
70+
71+
.checkpointPrizeContainer {
72+
margin: 30px 0 0 60px;
73+
}
74+
75+
.checkpointLabel {
76+
font-weight: 500 !important;
77+
padding-left: 0 !important;
78+
}
79+
80+
.checkpointPrizeContainer > div {
81+
display: inline-block;
82+
margin-right: 10px;
83+
}
84+
85+
.checkpointPrizeInputContainer {
86+
box-sizing: border-box;
87+
display: flex;
88+
height: 40px;
89+
}
90+
91+
.checkpointPrizeAmountContainer {
92+
box-sizing: border-box;
93+
display: flex;
94+
justify-content: center;
95+
align-items: center;
96+
background-color: $tc-prize-bg;
97+
border: 1px solid $tc-gray-40;
98+
max-width: 50px;
99+
min-width: 50px;
100+
width: 50px;
101+
border-right-width: 0;
102+
}

src/components/ChallengeEditor/CheckpointPrizes-Field/index.js

Lines changed: 72 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,37 +2,91 @@ import React from 'react'
22
import PropTypes from 'prop-types'
33
import styles from './CheckpointPrizes-Field.module.scss'
44
import cn from 'classnames'
5-
import { range } from 'lodash'
5+
import _ from 'lodash'
66
import { validateValue } from '../../../util/input-check'
7-
import { VALIDATION_VALUE_TYPE, PRIZE_SETS_TYPE, CHALLENGE_PRIZE_TYPE } from '../../../config/constants'
7+
import {
8+
VALIDATION_VALUE_TYPE,
9+
PRIZE_SETS_TYPE,
10+
CHALLENGE_PRIZE_TYPE,
11+
MAX_CHECKPOINT_PRIZE_COUNT,
12+
DEFAULT_CHECKPOINT_PRIZE
13+
} from '../../../config/constants'
14+
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
15+
import { faDollarSign } from '@fortawesome/free-solid-svg-icons'
16+
import Select from '../../Select'
817

9-
const CheckpointPrizesField = ({ challenge, onUpdateOthers }) => {
18+
const CheckpointPrizesField = ({ challenge, onUpdateOthers, readOnly }) => {
1019
const type = PRIZE_SETS_TYPE.CHECKPOINT_PRIZES
11-
const checkpointPrize = challenge.prizeSets.find(p => p.type === type) || { type: CHALLENGE_PRIZE_TYPE.USD, prizes: [] }
12-
const number = checkpointPrize.prizes.length
13-
const amount = checkpointPrize.prizes.length ? checkpointPrize.prizes[0].value : 0
20+
const prizeSets = _.get(challenge, 'prizeSets') || []
21+
const checkpointPrize = prizeSets.find(p => p.type === type) || { type: PRIZE_SETS_TYPE.CHECKPOINT_PRIZES, prizes: [], 'description': 'Checkpoint Prizes' }
22+
const number = _.get(checkpointPrize, 'prizes.length') || MAX_CHECKPOINT_PRIZE_COUNT
23+
const amount = _.get(checkpointPrize, 'prizes.length') ? checkpointPrize.prizes[0].value : DEFAULT_CHECKPOINT_PRIZE
24+
25+
// update the check point prize with default values if it's not already defined
26+
if (_.get(checkpointPrize, 'prizes.length') === 0) {
27+
onChange(number, amount)
28+
}
1429

1530
function onChange (number, amount) {
16-
checkpointPrize.prizes = range(validateValue(number, VALIDATION_VALUE_TYPE.INTEGER))
17-
.map(i => ({ type: CHALLENGE_PRIZE_TYPE.USD, value: validateValue(amount, VALIDATION_VALUE_TYPE.INTEGER, '$') }))
18-
onUpdateOthers({ field: 'prizeSets', value: [...challenge.prizeSets.filter(p => p.type !== type), +number && checkpointPrize].filter(p => p) })
31+
checkpointPrize.prizes = _.range(validateValue(number, VALIDATION_VALUE_TYPE.INTEGER))
32+
.map(i => ({ type: CHALLENGE_PRIZE_TYPE.USD, value: +validateValue(amount, VALIDATION_VALUE_TYPE.INTEGER) }))
33+
onUpdateOthers({ field: 'prizeSets', value: [...prizeSets.filter(p => p.type !== type), +number && checkpointPrize].filter(p => p) })
1934
}
35+
2036
return (
21-
<div className={styles.row}>
22-
<div className={cn(styles.field, styles.col1)}>
23-
<label htmlFor='checkpointPrizes'>Checkpoint Prizes :</label>
24-
</div>
25-
<div className={cn(styles.field, styles.col2)}>
26-
<input id='checkNumber' name='checkNumber' type='text' placeholder='Number of checkpoint prizes' value={number} maxLength='200' onChange={e => onChange(e.target.value, amount)} />
27-
<input id='checkAmount' name='checkAmount' type='text' placeholder='Amount per prizes' value={amount} maxLength='200' onChange={e => onChange(number, e.target.value)} />
37+
<>
38+
<div className={cn(styles.row)}>
39+
<div className={cn(styles.field, styles.col1)}>
40+
<label htmlFor={`checkpointPrizes`} className={styles.checkpointLabel}>Checkpoint Prizes :</label>
41+
</div>
2842
</div>
29-
</div>
43+
{
44+
readOnly ? (
45+
<div className={styles.checkpointPrizeContainer}>
46+
${amount} for each submission up to {number} submissions
47+
</div>
48+
) : (
49+
<div className={styles.checkpointPrizeContainer}>
50+
<div>
51+
Pay
52+
</div>
53+
<div>
54+
<div className={styles.checkpointPrizeInputContainer}>
55+
<div className={styles.checkpointPrizeAmountContainer}>
56+
<FontAwesomeIcon className={styles.dollarIcon} icon={faDollarSign} />
57+
</div>
58+
<input id='checkpointPrize' name='checkpointPrize' type='text' placeholder='' value={amount} maxLength='7' required onChange={(e) => onChange(number, e.target.value)} />
59+
</div>
60+
</div>
61+
<div>
62+
for each submission up to
63+
</div>
64+
<div>
65+
<Select
66+
name='submissions'
67+
options={_.range(1, MAX_CHECKPOINT_PRIZE_COUNT + 1).map((v) => ({ label: v, value: v }))}
68+
value={{ label: number, value: number }}
69+
isClearable={false}
70+
onChange={e => onChange(e.value, amount)}
71+
isDisabled={false}
72+
/>
73+
</div>
74+
75+
</div>
76+
)
77+
}
78+
</>
3079
)
3180
}
3281

82+
CheckpointPrizesField.defaultProps = {
83+
readOnly: false
84+
}
85+
3386
CheckpointPrizesField.propTypes = {
3487
challenge: PropTypes.shape().isRequired,
35-
onUpdateOthers: PropTypes.func.isRequired
88+
onUpdateOthers: PropTypes.func,
89+
readOnly: PropTypes.bool
3690
}
3791

3892
export default CheckpointPrizesField

src/components/ChallengeEditor/Description-Field/index.js

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import SimpleMDE from 'simplemde'
55
import marked from 'marked'
66
import cn from 'classnames'
77
import _ from 'lodash'
8+
import { MULTI_ROUND_CHALLENGE_DESC_TEMPLATE, MULTI_ROUND_CHALLENGE_TEMPLATE_ID } from '../../../config/constants'
89

910
class DescriptionField extends Component {
1011
constructor (props) {
@@ -15,6 +16,7 @@ class DescriptionField extends Component {
1516
}
1617
this.blurTheField = this.blurTheField.bind(this)
1718
this.updateDescriptionThrottled = _.throttle(this.updateDescription.bind(this), 10000) // 10s
19+
this.onChange = this.onChange.bind(this)
1820
}
1921

2022
blurTheField () {
@@ -27,13 +29,29 @@ class DescriptionField extends Component {
2729
onUpdateDescription(this.simplemde.value(), type)
2830
}
2931

32+
onChange (type) {
33+
this.setState({ isChanged: true })
34+
this.updateDescriptionThrottled(this.simplemde.value(), type)
35+
}
36+
3037
componentDidMount () {
3138
const { challenge, type, readOnly } = this.props
39+
3240
if (!readOnly) {
33-
this.simplemde = new SimpleMDE({ element: this.ref.current, initialValue: challenge[type] })
41+
let initialValue = challenge[type]
42+
const updateInitialValue = challenge.timelineTemplateId === MULTI_ROUND_CHALLENGE_TEMPLATE_ID && (
43+
!initialValue || initialValue.length === 0
44+
)
45+
if (updateInitialValue) {
46+
initialValue = MULTI_ROUND_CHALLENGE_DESC_TEMPLATE
47+
}
48+
49+
this.simplemde = new SimpleMDE({ element: this.ref.current, initialValue })
50+
if (updateInitialValue) {
51+
this.onChange(type)
52+
}
3453
this.simplemde.codemirror.on('change', () => {
35-
this.setState({ isChanged: true })
36-
this.updateDescriptionThrottled(this.simplemde.value(), type)
54+
this.onChange(type)
3755
})
3856
this.simplemde.codemirror.on('blur', () => {
3957
if (this.state.isChanged) {

0 commit comments

Comments
 (0)