Skip to content

Commit 942f682

Browse files
author
vikasrohit
authored
Merge pull request #1072 from topcoder-platform/feature/timeline-template
Add an option to select a timeline template
2 parents 3276c95 + a382c05 commit 942f682

File tree

7 files changed

+224
-15
lines changed

7 files changed

+224
-15
lines changed

.circleci/config.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ workflows:
8383
branches:
8484
only:
8585
- develop
86+
- feature/timeline-template
8687

8788
# Production builds are exectuted only on tagged commits to the
8889
# master branch.

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

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -52,16 +52,18 @@
5252
}
5353
}
5454

55-
@-moz-document url-prefix() {
56-
.challengeName {
57-
&::-moz-placeholder { /* Mozilla Firefox 19+ */
58-
line-height: 38px;
59-
}
60-
&::-webkit-input-placeholder { /* Webkit */
61-
line-height: 38px;
62-
}
63-
&:-ms-input-placeholder { /* IE */
64-
line-height: 38px;
65-
}
55+
.challengeName {
56+
&::-moz-placeholder { /* Mozilla Firefox 19+ */
57+
line-height: 38px;
58+
color: $tc-gray-80;
59+
}
60+
&::-webkit-input-placeholder { /* Webkit */
61+
line-height: 38px;
62+
color: $tc-gray-80;
63+
}
64+
&:-ms-input-placeholder { /* IE */
65+
line-height: 38px;
66+
color: $tc-gray-80;
6667
}
6768
}
69+

src/components/ChallengeEditor/ChallengeView/index.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { isBetaMode } from '../../../util/cookie'
2626
import { loadGroupDetails } from '../../../actions/challenges'
2727
import Tooltip from '../../Tooltip'
2828
import { MESSAGE, REVIEW_TYPES } from '../../../config/constants'
29+
import TimelineTemplateField from '../TimelineTemplate-Field'
2930

3031
const ChallengeView = ({
3132
projectDetail,
@@ -200,6 +201,13 @@ const ChallengeView = ({
200201
{isBetaMode() && (
201202
<UseSchedulingAPIField challenge={challenge} readOnly />
202203
)}
204+
<TimelineTemplateField
205+
challengeTimelines={metadata.challengeTimelines}
206+
timelineTemplates={metadata.timelineTemplates}
207+
challenge={challenge}
208+
onUpdateSelect={() => {}}
209+
readOnly
210+
/>
203211
</>
204212
)}
205213
{
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: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import _ from 'lodash'
2+
import React, { Component } from 'react'
3+
import PropTypes from 'prop-types'
4+
import Select from '../../Select'
5+
import cn from 'classnames'
6+
import styles from './TimelineTemplate-Field.module.scss'
7+
8+
class TimelineTemplateField extends Component {
9+
constructor (props) {
10+
super(props)
11+
this.state = {
12+
validOptions: [],
13+
selectedOption: {}
14+
}
15+
16+
this.checkData = this.checkData.bind(this)
17+
this.loadSelectedOption = this.loadSelectedOption.bind(this)
18+
this.getErrorMessage = this.getErrorMessage.bind(this)
19+
}
20+
21+
componentDidMount () {
22+
const { challengeTimelines, timelineTemplates, challenge, currentTemplate } = this.props
23+
this.checkData(challengeTimelines, timelineTemplates, challenge, currentTemplate)
24+
}
25+
26+
componentWillUnmount () {
27+
this.props.onUpdateSelect(this.state.selectedOption.value)
28+
}
29+
30+
loadSelectedOption (validOptions, value) {
31+
const { timelineTemplates, challenge } = this.props
32+
const selectedOption = {}
33+
const selectedTemplate = _.find(timelineTemplates, t => t.id === (value))
34+
this.props.onUpdateSelect(selectedTemplate)
35+
36+
selectedOption.label = selectedTemplate.name
37+
selectedOption.value = selectedTemplate.id
38+
this.setState({
39+
validOptions,
40+
matchString: `${challenge.typeId}-${challenge.trackId}-${value}`,
41+
selectedOption
42+
})
43+
}
44+
45+
checkData (challengeTimelines, timelineTemplates, challenge, currentTemplate) {
46+
const availableTemplates = _.filter(challengeTimelines, ct => ct.typeId === challenge.typeId && ct.trackId === challenge.trackId)
47+
const availableTemplateIds = availableTemplates.map(tt => tt.timelineTemplateId)
48+
const validOptions = _.filter(timelineTemplates, t => _.includes(availableTemplateIds, t.id))
49+
const defaultValue = _.get(_.find(availableTemplates, t => t.isDefault), 'timelineTemplateId')
50+
if (currentTemplate && currentTemplate.id) {
51+
if (!_.includes(_.map(validOptions, o => o.id), currentTemplate.id)) {
52+
this.loadSelectedOption(validOptions, defaultValue)
53+
} else {
54+
this.loadSelectedOption(validOptions, currentTemplate.id)
55+
}
56+
} else if (defaultValue) {
57+
return this.loadSelectedOption(validOptions, defaultValue)
58+
}
59+
}
60+
61+
getErrorMessage () {
62+
if (!this.props.challenge.typeId || !this.props.challenge.trackId) {
63+
return 'Please select a work type and format to enable this field'
64+
} else if (this.props.challenge.submitTriggered && !this.props.currentTemplate) {
65+
return 'Timeline template is required field'
66+
} else if (this.state.validOptions.length === 0) {
67+
return 'Sorry, there are no available timeline templates for the options you have selected'
68+
}
69+
return null
70+
}
71+
72+
render () {
73+
const { challengeTimelines, timelineTemplates, challenge, currentTemplate } = this.props
74+
const hasSelectedTypeAndTrack = !_.isEmpty(challenge.typeId) && !_.isEmpty(challenge.trackId)
75+
if ((hasSelectedTypeAndTrack && this.state.validOptions.length === 0) || this.state.matchString !== `${challenge.typeId}-${challenge.trackId}-${this.state.selectedOption.value}`) {
76+
this.checkData(challengeTimelines, timelineTemplates, challenge, currentTemplate)
77+
}
78+
const error = this.getErrorMessage()
79+
return (
80+
<>
81+
<div className={styles.row}>
82+
<div className={cn(styles.field, styles.col1)}>
83+
<label htmlFor='type'>Timeline Template {!this.props.readOnly && <span>*</span>} :</label>
84+
</div>
85+
<div className={cn(styles.field, styles.col2, { [styles.disabled]: this.state.validOptions.length === 0 })}>
86+
<Select
87+
value={this.state.selectedOption}
88+
name='timelineTemplateId'
89+
options={this.state.validOptions.map(type => ({ label: type.name, value: type.id }))}
90+
placeholder='Timeline Template'
91+
isClearable={false}
92+
onChange={(e) => {
93+
this.loadSelectedOption(this.state.validOptions, e.value)
94+
}}
95+
isDisabled={this.state.validOptions.length === 0 || this.props.readOnly}
96+
/>
97+
</div>
98+
</div>
99+
{ error && <div className={styles.row}>
100+
<div className={cn(styles.field, styles.col1)} />
101+
<div className={cn(styles.field, styles.col2, styles.error)}>
102+
{error}
103+
</div>
104+
</div> }
105+
</>
106+
)
107+
}
108+
}
109+
110+
TimelineTemplateField.defaultProps = {
111+
challengeTimelines: [],
112+
timelineTemplates: [],
113+
readOnly: false,
114+
currentTemplate: null
115+
}
116+
117+
TimelineTemplateField.propTypes = {
118+
challengeTimelines: PropTypes.arrayOf(PropTypes.shape()).isRequired,
119+
timelineTemplates: PropTypes.arrayOf(PropTypes.shape()).isRequired,
120+
challenge: PropTypes.shape().isRequired,
121+
onUpdateSelect: PropTypes.func.isRequired,
122+
readOnly: PropTypes.bool,
123+
currentTemplate: PropTypes.shape()
124+
}
125+
126+
export default TimelineTemplateField

src/components/ChallengeEditor/index.js

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import Tooltip from '../Tooltip'
5252
import UseSchedulingAPIField from './UseSchedulingAPIField'
5353
import { getResourceRoleByName } from '../../util/tc'
5454
import { isBetaMode } from '../../util/cookie'
55+
import TimelineTemplateField from './TimelineTemplate-Field'
5556

5657
const theme = {
5758
container: styles.modalContainer
@@ -838,7 +839,7 @@ class ChallengeEditor extends Component {
838839
const STD_DEV_TIMELINE_TEMPLATE = _.find(timelineTemplates, { name: 'Standard Development' })
839840
const avlTemplates = this.getAvailableTimelineTemplates()
840841
// chooses first available timeline template or fallback template for the new challenge
841-
const defaultTemplate = avlTemplates && avlTemplates.length > 0 ? avlTemplates[0] : STD_DEV_TIMELINE_TEMPLATE
842+
const defaultTemplate = _.find(avlTemplates || [], t => t.isDefault) || STD_DEV_TIMELINE_TEMPLATE
842843
const isTask = _.find(metadata.challengeTypes, { id: typeId, isTask: true })
843844
const newChallenge = {
844845
status: 'New',
@@ -851,7 +852,7 @@ class ChallengeEditor extends Component {
851852
reviewType: isTask || isDesignChallenge ? REVIEW_TYPES.INTERNAL : REVIEW_TYPES.COMMUNITY
852853
},
853854
descriptionFormat: 'markdown',
854-
timelineTemplateId: defaultTemplate.id,
855+
timelineTemplateId: _.get(this.getCurrentTemplate(), 'id', defaultTemplate.id),
855856
terms: [{ id: DEFAULT_TERM_UUID, roleId: SUBMITTER_ROLE_UUID }],
856857
groups: []
857858
// prizeSets: this.getDefaultPrizeSets()
@@ -1140,8 +1141,10 @@ class ChallengeEditor extends Component {
11401141

11411142
// all timeline template ids available for the challenge type
11421143
const availableTemplateIds = _.filter(challengeTimelines, ct => ct.typeId === challenge.typeId && ct.trackId === challenge.trackId).map(tt => tt.timelineTemplateId)
1144+
const defaultChallengeTimeline = _.find(challengeTimelines, ct => ct.typeId === challenge.typeId && ct.trackId === challenge.trackId && ct.isDefault)
11431145
// filter and return timeline templates that are available for this challenge type
1144-
return _.filter(timelineTemplates, tt => availableTemplateIds.indexOf(tt.id) !== -1)
1146+
const avlTemplates = _.filter(timelineTemplates, tt => availableTemplateIds.indexOf(tt.id) !== -1)
1147+
return _.map(avlTemplates, tt => tt.id === defaultChallengeTimeline.timelineTemplateId ? { ...tt, isDefault: true } : tt)
11451148
}
11461149

11471150
render () {
@@ -1360,6 +1363,13 @@ class ChallengeEditor extends Component {
13601363
<div className={styles.newFormContainer}>
13611364
<TrackField tracks={metadata.challengeTracks} challenge={challenge} onUpdateOthers={this.onUpdateOthers} />
13621365
<TypeField types={metadata.challengeTypes} onUpdateSelect={this.onUpdateSelect} challenge={challenge} />
1366+
<TimelineTemplateField
1367+
currentTemplate={this.state.currentTemplate}
1368+
challengeTimelines={metadata.challengeTimelines}
1369+
timelineTemplates={metadata.timelineTemplates}
1370+
challenge={challenge}
1371+
onUpdateSelect={this.resetPhase}
1372+
/>
13631373
<ChallengeNameField challenge={challenge} onUpdateInput={this.onUpdateInput} />
13641374
</div>
13651375
{ errorContainer }
@@ -1437,6 +1447,13 @@ class ChallengeEditor extends Component {
14371447
{isBetaMode() && (
14381448
<UseSchedulingAPIField challenge={challenge} toggleUseSchedulingAPI={this.toggleUseSchedulingAPI} />
14391449
)}
1450+
<TimelineTemplateField
1451+
challengeTimelines={metadata.challengeTimelines}
1452+
timelineTemplates={metadata.timelineTemplates}
1453+
challenge={challenge}
1454+
currentTemplate={this.state.currentTemplate}
1455+
onUpdateSelect={this.resetPhase}
1456+
/>
14401457
</React.Fragment>
14411458
)}
14421459
{!isTask && (

src/services/challenges.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ export async function fetchTimelineTemplates () {
9191
* @returns {Promise<*>}
9292
*/
9393
export async function fetchChallengeTimelines () {
94-
const response = await axiosInstance.get(`${CHALLENGE_TIMELINES_URL}?isDefault=true&page=1&perPage=100`)
94+
const response = await axiosInstance.get(`${CHALLENGE_TIMELINES_URL}?page=1&perPage=100`)
9595
return _.get(response, 'data', [])
9696
}
9797

0 commit comments

Comments
 (0)