diff --git a/.circleci/config.yml b/.circleci/config.yml index 6d96e22b..34ee3dfc 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -83,6 +83,7 @@ workflows: branches: only: - develop + - feature/timeline-template # Production builds are exectuted only on tagged commits to the # master branch. diff --git a/src/components/ChallengeEditor/ChallengeName-Field/ChallengeName-Field.module.scss b/src/components/ChallengeEditor/ChallengeName-Field/ChallengeName-Field.module.scss index d6aa6895..51348657 100644 --- a/src/components/ChallengeEditor/ChallengeName-Field/ChallengeName-Field.module.scss +++ b/src/components/ChallengeEditor/ChallengeName-Field/ChallengeName-Field.module.scss @@ -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; } } + diff --git a/src/components/ChallengeEditor/ChallengeView/index.js b/src/components/ChallengeEditor/ChallengeView/index.js index 1d25750b..0e65044f 100644 --- a/src/components/ChallengeEditor/ChallengeView/index.js +++ b/src/components/ChallengeEditor/ChallengeView/index.js @@ -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, @@ -202,6 +203,13 @@ const ChallengeView = ({ {isBetaMode() && ( )} + {}} + readOnly + /> > )} { diff --git a/src/components/ChallengeEditor/TimelineTemplate-Field/TimelineTemplate-Field.module.scss b/src/components/ChallengeEditor/TimelineTemplate-Field/TimelineTemplate-Field.module.scss new file mode 100644 index 00000000..f6707a0c --- /dev/null +++ b/src/components/ChallengeEditor/TimelineTemplate-Field/TimelineTemplate-Field.module.scss @@ -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; + } + } +} diff --git a/src/components/ChallengeEditor/TimelineTemplate-Field/index.js b/src/components/ChallengeEditor/TimelineTemplate-Field/index.js new file mode 100644 index 00000000..242f4e07 --- /dev/null +++ b/src/components/ChallengeEditor/TimelineTemplate-Field/index.js @@ -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 ( + <> + + + Timeline Template {!this.props.readOnly && *} : + + + ({ 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} + /> + + + { error && + + + {error} + + } + > + ) + } +} + +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 diff --git a/src/components/ChallengeEditor/index.js b/src/components/ChallengeEditor/index.js index 9d3668a6..95d5421f 100644 --- a/src/components/ChallengeEditor/index.js +++ b/src/components/ChallengeEditor/index.js @@ -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 @@ -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', @@ -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() @@ -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 () { @@ -1363,6 +1366,13 @@ class ChallengeEditor extends Component { + { errorContainer } @@ -1440,6 +1450,13 @@ class ChallengeEditor extends Component { {isBetaMode() && ( )} + )} {!isTask && ( diff --git a/src/services/challenges.js b/src/services/challenges.js index 321b6ee1..3f77e8de 100644 --- a/src/services/challenges.js +++ b/src/services/challenges.js @@ -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', []) }