Skip to content

Add an option to select a timeline template #1072

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Mar 11, 2021
1 change: 1 addition & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ workflows:
branches:
only:
- develop
- feature/timeline-template

# Production builds are exectuted only on tagged commits to the
# master branch.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,16 +52,18 @@
}
}

@-moz-document url-prefix() {
.challengeName {
&::-moz-placeholder { /* Mozilla Firefox 19+ */
line-height: 38px;
}
&::-webkit-input-placeholder { /* Webkit */
line-height: 38px;
}
&:-ms-input-placeholder { /* IE */
line-height: 38px;
}
.challengeName {
&::-moz-placeholder { /* Mozilla Firefox 19+ */
line-height: 38px;
color: $tc-gray-80;
}
&::-webkit-input-placeholder { /* Webkit */
line-height: 38px;
color: $tc-gray-80;
}
&:-ms-input-placeholder { /* IE */
line-height: 38px;
color: $tc-gray-80;
}
}

8 changes: 8 additions & 0 deletions src/components/ChallengeEditor/ChallengeView/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { isBetaMode } from '../../../util/cookie'
import { loadGroupDetails } from '../../../actions/challenges'
import Tooltip from '../../Tooltip'
import { MESSAGE, REVIEW_TYPES } from '../../../config/constants'
import TimelineTemplateField from '../TimelineTemplate-Field'

const ChallengeView = ({
projectDetail,
Expand Down Expand Up @@ -202,6 +203,13 @@ const ChallengeView = ({
{isBetaMode() && (
<UseSchedulingAPIField challenge={challenge} readOnly />
)}
<TimelineTemplateField
challengeTimelines={metadata.challengeTimelines}
timelineTemplates={metadata.timelineTemplates}
challenge={challenge}
onUpdateSelect={() => {}}
readOnly
/>
</>
)}
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
@import "../../../styles/includes";

.row {
box-sizing: border-box;
display: flex;
flex-direction: row;
margin: 30px 30px 0 30px;
align-content: space-between;
justify-content: flex-start;

.field {
@include upto-sm {
display: block;
padding-bottom: 10px;
}

label {
@include roboto-bold();

font-size: 16px;
line-height: 19px;
font-weight: 500;
color: $tc-gray-80;
}

&.col1 {
max-width: 185px;
min-width: 185px;
margin-right: 14px;
white-space: nowrap;
display: flex;
align-items: center;
flex-grow: 1;

span {
color: $tc-red;
}
}

&.col2.error {
color: $tc-red;
margin-top: -25px;
}
&.col2 {
align-self: flex-end;
width: 80%;
margin-bottom: auto;
margin-top: auto;
display: flex;
flex-direction: row;
max-width: 600px;
min-width: 600px;
}
}
}
126 changes: 126 additions & 0 deletions src/components/ChallengeEditor/TimelineTemplate-Field/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import _ from 'lodash'
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import Select from '../../Select'
import cn from 'classnames'
import styles from './TimelineTemplate-Field.module.scss'

class TimelineTemplateField extends Component {
constructor (props) {
super(props)
this.state = {
validOptions: [],
selectedOption: {}
}

this.checkData = this.checkData.bind(this)
this.loadSelectedOption = this.loadSelectedOption.bind(this)
this.getErrorMessage = this.getErrorMessage.bind(this)
}

componentDidMount () {
const { challengeTimelines, timelineTemplates, challenge, currentTemplate } = this.props
this.checkData(challengeTimelines, timelineTemplates, challenge, currentTemplate)
}

componentWillUnmount () {
this.props.onUpdateSelect(this.state.selectedOption.value)
}

loadSelectedOption (validOptions, value) {
const { timelineTemplates, challenge } = this.props
const selectedOption = {}
const selectedTemplate = _.find(timelineTemplates, t => t.id === (value))
this.props.onUpdateSelect(selectedTemplate)

selectedOption.label = selectedTemplate.name
selectedOption.value = selectedTemplate.id
this.setState({
validOptions,
matchString: `${challenge.typeId}-${challenge.trackId}-${value}`,
selectedOption
})
}

checkData (challengeTimelines, timelineTemplates, challenge, currentTemplate) {
const availableTemplates = _.filter(challengeTimelines, ct => ct.typeId === challenge.typeId && ct.trackId === challenge.trackId)
const availableTemplateIds = availableTemplates.map(tt => tt.timelineTemplateId)
const validOptions = _.filter(timelineTemplates, t => _.includes(availableTemplateIds, t.id))
const defaultValue = _.get(_.find(availableTemplates, t => t.isDefault), 'timelineTemplateId')
if (currentTemplate && currentTemplate.id) {
if (!_.includes(_.map(validOptions, o => o.id), currentTemplate.id)) {
this.loadSelectedOption(validOptions, defaultValue)
} else {
this.loadSelectedOption(validOptions, currentTemplate.id)
}
} else if (defaultValue) {
return this.loadSelectedOption(validOptions, defaultValue)
}
}

getErrorMessage () {
if (!this.props.challenge.typeId || !this.props.challenge.trackId) {
return 'Please select a work type and format to enable this field'
} else if (this.props.challenge.submitTriggered && !this.props.currentTemplate) {
return 'Timeline template is required field'
} else if (this.state.validOptions.length === 0) {
return 'Sorry, there are no available timeline templates for the options you have selected'
}
return null
}

render () {
const { challengeTimelines, timelineTemplates, challenge, currentTemplate } = this.props
const hasSelectedTypeAndTrack = !_.isEmpty(challenge.typeId) && !_.isEmpty(challenge.trackId)
if ((hasSelectedTypeAndTrack && this.state.validOptions.length === 0) || this.state.matchString !== `${challenge.typeId}-${challenge.trackId}-${this.state.selectedOption.value}`) {
this.checkData(challengeTimelines, timelineTemplates, challenge, currentTemplate)
}
const error = this.getErrorMessage()
return (
<>
<div className={styles.row}>
<div className={cn(styles.field, styles.col1)}>
<label htmlFor='type'>Timeline Template {!this.props.readOnly && <span>*</span>} :</label>
</div>
<div className={cn(styles.field, styles.col2, { [styles.disabled]: this.state.validOptions.length === 0 })}>
<Select
value={this.state.selectedOption}
name='timelineTemplateId'
options={this.state.validOptions.map(type => ({ label: type.name, value: type.id }))}
placeholder='Timeline Template'
isClearable={false}
onChange={(e) => {
this.loadSelectedOption(this.state.validOptions, e.value)
}}
isDisabled={this.state.validOptions.length === 0 || this.props.readOnly}
/>
</div>
</div>
{ error && <div className={styles.row}>
<div className={cn(styles.field, styles.col1)} />
<div className={cn(styles.field, styles.col2, styles.error)}>
{error}
</div>
</div> }
</>
)
}
}

TimelineTemplateField.defaultProps = {
challengeTimelines: [],
timelineTemplates: [],
readOnly: false,
currentTemplate: null
}

TimelineTemplateField.propTypes = {
challengeTimelines: PropTypes.arrayOf(PropTypes.shape()).isRequired,
timelineTemplates: PropTypes.arrayOf(PropTypes.shape()).isRequired,
challenge: PropTypes.shape().isRequired,
onUpdateSelect: PropTypes.func.isRequired,
readOnly: PropTypes.bool,
currentTemplate: PropTypes.shape()
}

export default TimelineTemplateField
23 changes: 20 additions & 3 deletions src/components/ChallengeEditor/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import Tooltip from '../Tooltip'
import UseSchedulingAPIField from './UseSchedulingAPIField'
import { getResourceRoleByName } from '../../util/tc'
import { isBetaMode } from '../../util/cookie'
import TimelineTemplateField from './TimelineTemplate-Field'

const theme = {
container: styles.modalContainer
Expand Down Expand Up @@ -838,7 +839,7 @@ class ChallengeEditor extends Component {
const STD_DEV_TIMELINE_TEMPLATE = _.find(timelineTemplates, { name: 'Standard Development' })
const avlTemplates = this.getAvailableTimelineTemplates()
// chooses first available timeline template or fallback template for the new challenge
const defaultTemplate = avlTemplates && avlTemplates.length > 0 ? avlTemplates[0] : STD_DEV_TIMELINE_TEMPLATE
const defaultTemplate = _.find(avlTemplates || [], t => t.isDefault) || STD_DEV_TIMELINE_TEMPLATE
const isTask = _.find(metadata.challengeTypes, { id: typeId, isTask: true })
const newChallenge = {
status: 'New',
Expand All @@ -851,7 +852,7 @@ class ChallengeEditor extends Component {
reviewType: isTask || isDesignChallenge ? REVIEW_TYPES.INTERNAL : REVIEW_TYPES.COMMUNITY
},
descriptionFormat: 'markdown',
timelineTemplateId: defaultTemplate.id,
timelineTemplateId: _.get(this.getCurrentTemplate(), 'id', defaultTemplate.id),
terms: [{ id: DEFAULT_TERM_UUID, roleId: SUBMITTER_ROLE_UUID }],
groups: []
// prizeSets: this.getDefaultPrizeSets()
Expand Down Expand Up @@ -1143,8 +1144,10 @@ class ChallengeEditor extends Component {

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

render () {
Expand Down Expand Up @@ -1363,6 +1366,13 @@ class ChallengeEditor extends Component {
<div className={styles.newFormContainer}>
<TrackField tracks={metadata.challengeTracks} challenge={challenge} onUpdateOthers={this.onUpdateOthers} />
<TypeField types={metadata.challengeTypes} onUpdateSelect={this.onUpdateSelect} challenge={challenge} />
<TimelineTemplateField
currentTemplate={this.state.currentTemplate}
challengeTimelines={metadata.challengeTimelines}
timelineTemplates={metadata.timelineTemplates}
challenge={challenge}
onUpdateSelect={this.resetPhase}
/>
<ChallengeNameField challenge={challenge} onUpdateInput={this.onUpdateInput} />
</div>
{ errorContainer }
Expand Down Expand Up @@ -1440,6 +1450,13 @@ class ChallengeEditor extends Component {
{isBetaMode() && (
<UseSchedulingAPIField challenge={challenge} toggleUseSchedulingAPI={this.toggleUseSchedulingAPI} />
)}
<TimelineTemplateField
challengeTimelines={metadata.challengeTimelines}
timelineTemplates={metadata.timelineTemplates}
challenge={challenge}
currentTemplate={this.state.currentTemplate}
onUpdateSelect={this.resetPhase}
/>
</React.Fragment>
)}
{!isTask && (
Expand Down
2 changes: 1 addition & 1 deletion src/services/challenges.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export async function fetchTimelineTemplates () {
* @returns {Promise<*>}
*/
export async function fetchChallengeTimelines () {
const response = await axiosInstance.get(`${CHALLENGE_TIMELINES_URL}?isDefault=true&page=1&perPage=100`)
const response = await axiosInstance.get(`${CHALLENGE_TIMELINES_URL}?page=1&perPage=100`)
return _.get(response, 'data', [])
}

Expand Down