diff --git a/.circleci/config.yml b/.circleci/config.yml index bc9e9d23..0d96a6c9 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -150,7 +150,7 @@ workflows: context : org-global filters: &filters-dev branches: - only: ['develop', 'hot-fix-jira-vuln-2333'] + only: ['develop'] # Production builds are exectuted only on tagged commits to the # master branch. diff --git a/package-lock.json b/package-lock.json index d5c828dc..65c76e8a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1142,6 +1142,17 @@ "glob-to-regexp": "^0.3.0" } }, + "@nateradebaugh/react-datetime": { + "version": "4.4.11", + "resolved": "https://registry.npmjs.org/@nateradebaugh/react-datetime/-/react-datetime-4.4.11.tgz", + "integrity": "sha512-PO/j7v8cb0Aw/MwokOL07jyYAnwkvLp9Y9n+cR4Rqr4bM051uj9VHufuXQduMj1BKAfIMNIIrhb4NhcGCnjwow==", + "requires": { + "@reach/popover": "0.16.2", + "classcat": "^5.0.1", + "date-fns": "^2.28.0", + "use-onclickoutside": "^0.4.0" + } + }, "@nodelib/fs.stat": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz", @@ -1152,6 +1163,82 @@ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.5.4.tgz", "integrity": "sha512-ZpKr+WTb8zsajqgDkvCEWgp6d5eJT6Q63Ng2neTbzBO76Lbe91vX/iVIW9dikq+Fs3yEo+ls4cxeXABD2LtcbQ==" }, + "@reach/observe-rect": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@reach/observe-rect/-/observe-rect-1.2.0.tgz", + "integrity": "sha512-Ba7HmkFgfQxZqqaeIWWkNK0rEhpxVQHIoVyW1YDSkGsGIXzcaW4deC8B0pZrNSSyLTdIk7y+5olKt5+g0GmFIQ==" + }, + "@reach/popover": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@reach/popover/-/popover-0.16.2.tgz", + "integrity": "sha512-IwkRrHM7Vt33BEkSXneovymJv7oIToOfTDwRKpuYEB/BWYMAuNfbsRL7KVe6MjkgchDeQzAk24cYY1ztQj5HQQ==", + "requires": { + "@reach/portal": "0.16.2", + "@reach/rect": "0.16.0", + "@reach/utils": "0.16.0", + "tabbable": "^4.0.0", + "tslib": "^2.3.0" + }, + "dependencies": { + "tslib": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" + } + } + }, + "@reach/portal": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@reach/portal/-/portal-0.16.2.tgz", + "integrity": "sha512-9ur/yxNkuVYTIjAcfi46LdKUvH0uYZPfEp4usWcpt6PIp+WDF57F/5deMe/uGi/B/nfDweQu8VVwuMVrCb97JQ==", + "requires": { + "@reach/utils": "0.16.0", + "tiny-warning": "^1.0.3", + "tslib": "^2.3.0" + }, + "dependencies": { + "tslib": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" + } + } + }, + "@reach/rect": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@reach/rect/-/rect-0.16.0.tgz", + "integrity": "sha512-/qO9jQDzpOCdrSxVPR6l674mRHNTqfEjkaxZHluwJ/2qGUtYsA0GSZiF/+wX/yOWeBif1ycxJDa6HusAMJZC5Q==", + "requires": { + "@reach/observe-rect": "1.2.0", + "@reach/utils": "0.16.0", + "prop-types": "^15.7.2", + "tiny-warning": "^1.0.3", + "tslib": "^2.3.0" + }, + "dependencies": { + "tslib": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" + } + } + }, + "@reach/utils": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@reach/utils/-/utils-0.16.0.tgz", + "integrity": "sha512-PCggBet3qaQmwFNcmQ/GqHSefadAFyNCUekq9RrWoaU9hh/S4iaFgf2MBMdM47eQj5i/Bk0Mm07cP/XPFlkN+Q==", + "requires": { + "tiny-warning": "^1.0.3", + "tslib": "^2.3.0" + }, + "dependencies": { + "tslib": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" + } + } + }, "@sentry/hub": { "version": "5.28.0", "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-5.28.0.tgz", @@ -1822,6 +1909,11 @@ "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" }, + "are-passive-events-supported": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/are-passive-events-supported/-/are-passive-events-supported-1.1.1.tgz", + "integrity": "sha512-5wnvlvB/dTbfrCvJ027Y4L4gW/6Mwoy1uFSavney0YO++GU+0e/flnjiBBwH+1kh7xNCgCOGvmJC3s32joYbww==" + }, "are-we-there-yet": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", @@ -3455,6 +3547,11 @@ } } }, + "classcat": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.3.tgz", + "integrity": "sha512-6dK2ke4VEJZOFx2ZfdDAl5OhEL8lvkl6EHF92IfRePfHxQTqir5NlcNVUv+2idjDqCX2NDc8m8YSAI5NI975ZQ==" + }, "classnames": { "version": "2.2.6", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz", @@ -4348,6 +4445,11 @@ "whatwg-url": "^8.0.0" } }, + "date-fns": { + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.28.0.tgz", + "integrity": "sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw==" + }, "debug": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", @@ -9829,6 +9931,14 @@ "resolved": "https://registry.npmjs.org/moment-duration-format/-/moment-duration-format-2.3.2.tgz", "integrity": "sha512-cBMXjSW+fjOb4tyaVHuaVE/A5TqkukDWiOfxxAjY+PEqmmBQlLwn+8OzwPiG3brouXKY5Un4pBjAeB6UToXHaQ==" }, + "moment-timezone": { + "version": "0.5.34", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.34.tgz", + "integrity": "sha512-3zAEHh2hKUs3EXLESx/wsgw6IQdusOT8Bxm3D9UrHPQR7zlMmzwybC8zHEM1tQ4LJwP7fcxrWr8tuBg05fFCbg==", + "requires": { + "moment": ">= 2.9.0" + } + }, "move-concurrently": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", @@ -17430,6 +17540,11 @@ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" }, + "tabbable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-4.0.0.tgz", + "integrity": "sha512-H1XoH1URcBOa/rZZWxLxHCtOdVUEev+9vo5YdYhC9tCY4wnybX+VQrCYuy9ubkg69fCBxCONJOSLGfw0DWMffQ==" + }, "table": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/table/-/table-4.0.3.tgz", @@ -18079,6 +18194,28 @@ "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==" }, + "use-isomorphic-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.1.tgz", + "integrity": "sha512-L7Evj8FGcwo/wpbv/qvSfrkHFtOpCzvM5yl2KVyDJoylVuSvzphiiasmjgQPttIGBAy2WKiBNR98q8w7PiNgKQ==" + }, + "use-latest": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/use-latest/-/use-latest-1.2.0.tgz", + "integrity": "sha512-d2TEuG6nSLKQLAfW3By8mKr8HurOlTkul0sOpxbClIv4SQ4iOd7BYr7VIzdbktUCnv7dua/60xzd8igMU6jmyw==", + "requires": { + "use-isomorphic-layout-effect": "^1.0.0" + } + }, + "use-onclickoutside": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/use-onclickoutside/-/use-onclickoutside-0.4.0.tgz", + "integrity": "sha512-lg1U+V8SaCfemgBs5dg+cfEOzjuwVS9ATH0VMLSBHI6R11tbfmiKci1lg6pjwXr1sj95XWd2+5EbffJEAPdkJQ==", + "requires": { + "are-passive-events-supported": "^1.1.0", + "use-latest": "^1.0.0" + } + }, "util": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", diff --git a/package.json b/package.json index 21b87380..02e50216 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "@fortawesome/fontawesome-svg-core": "^1.2.14", "@fortawesome/free-solid-svg-icons": "^5.7.1", "@fortawesome/react-fontawesome": "^0.1.4", + "@nateradebaugh/react-datetime": "^4.4.11", "@popperjs/core": "^2.5.4", "@svgr/webpack": "2.4.1", "axios": "^0.19.0", @@ -53,6 +54,7 @@ "mini-css-extract-plugin": "0.4.3", "moment": "^2.24.0", "moment-duration-format": "^2.2.2", + "moment-timezone": "^0.5.34", "node-sass": "^4.14.0", "optimize-css-assets-webpack-plugin": "5.0.1", "pnp-webpack-plugin": "1.1.0", diff --git a/src/components/ChallengeEditor/ChallengeSchedule-Field/index.js b/src/components/ChallengeEditor/ChallengeSchedule-Field/index.js index 328477d2..3abd88b9 100644 --- a/src/components/ChallengeEditor/ChallengeSchedule-Field/index.js +++ b/src/components/ChallengeEditor/ChallengeSchedule-Field/index.js @@ -6,7 +6,7 @@ import $ from 'jquery' import styles from './ChallengeSchedule-Field.module.scss' import cn from 'classnames' import jstz from 'jstimezonedetect' -import PhaseInput from '../../PhaseInput' +import StartDateInput from '../../StartDateInput' import Chart from 'react-google-charts' import Select from '../../Select' import { parseSVG } from '../../../util/svg' @@ -183,7 +183,7 @@ class ChallengeScheduleField extends Component { return ( _.map(challenge.phases, (p, index) => (
-
}
- const showTimeline = false // disables the timeline for time being https://github.com/topcoder-platform/challenge-engine-ui/issues/706 const isTask = _.get(challenge, 'task.isTask', false) + const phases = _.get(challenge, 'phases', []) return (
@@ -188,16 +189,14 @@ const ChallengeView = ({ )} { -
+ phases.map((phase, index) => ( -
+ )) } {showTimeline && ( { + this.onTick() + }, 500) + this.intervalId = setInterval(() => { + this.onTick() + }, 60000) + } + + componentDidUnMount () { + clearInterval(this.intervalId) } componentDidUpdate () { @@ -810,6 +821,65 @@ class ChallengeEditor extends Component { this.setState({ challenge: newChallenge }) } + onTick () { + if (this.state && this.state) { + const { phases } = this.state.challenge + let newChallenge = _.cloneDeep(this.state.challenge) + for (let index = 0; index < phases.length; ++index) { + newChallenge.phases[index].isDurationActive = + moment(newChallenge.phases[index]['scheduledEndDate']).isAfter() + newChallenge.phases[index].isStartTimeActive = index > 0 ? false + : moment(newChallenge.phases[0]['scheduledStartDate']).isAfter() + newChallenge.phases[index].isOpen = + newChallenge.phases[index].isDurationActive + } + this.setState({ challenge: newChallenge }) + } + } + + onUpdatePhaseDate (phase, index) { + const { phases } = this.state.challenge + let newChallenge = _.cloneDeep(this.state.challenge) + if (phase.isBlur && newChallenge.phases[index]['name'] === 'Submission') { + newChallenge.phases[index]['duration'] = _.max([ + newChallenge.phases[index - 1]['duration'], + phase.duration + ]) + newChallenge.phases[index]['scheduledEndDate'] = + moment(newChallenge.phases[index]['scheduledStartDate']) + .add(newChallenge.phases[index]['duration'], 'hours') + .format('MM/DD/YYYY HH:mm') + } else { + newChallenge.phases[index]['duration'] = phase.duration + newChallenge.phases[index]['scheduledStartDate'] = phase.startDate + newChallenge.phases[index]['scheduledEndDate'] = phase.endDate + } + + for (let phaseIndex = index + 1; phaseIndex < phases.length; ++phaseIndex) { + if (newChallenge.phases[phaseIndex]['name'] === 'Submission') { + newChallenge.phases[phaseIndex]['scheduledStartDate'] = + newChallenge.phases[phaseIndex - 1]['scheduledStartDate'] + newChallenge.phases[phaseIndex]['duration'] = _.max([ + newChallenge.phases[phaseIndex - 1]['duration'], + newChallenge.phases[phaseIndex]['duration'] + ]) + } else { + newChallenge.phases[phaseIndex]['scheduledStartDate'] = + newChallenge.phases[phaseIndex - 1]['scheduledEndDate'] + } + newChallenge.phases[phaseIndex]['scheduledEndDate'] = + moment(newChallenge.phases[phaseIndex]['scheduledStartDate']) + .add(newChallenge.phases[phaseIndex]['duration'], 'hours') + .format('MM/DD/YYYY HH:mm') + } + + this.setState({ challenge: newChallenge }) + + setTimeout(() => { + this.onTick() + }, 500) + } + collectChallengeData (status) { const { attachments, metadata } = this.props const challenge = pick([ @@ -851,7 +921,9 @@ class ChallengeEditor extends Component { } challenge.phases = challenge.phases.map((p) => pick([ 'duration', - 'phaseId' + 'phaseId', + 'scheduledStartDate', + 'scheduledEndDate' ], p)) if (challenge.terms && challenge.terms.length === 0) delete challenge.terms delete challenge.attachments @@ -1280,7 +1352,7 @@ class ChallengeEditor extends Component { let closeTaskModal = null let draftModal = null - let { type } = challenge + let { type, phases = [] } = challenge if (!type) { const { typeId } = challenge if (typeId && metadata.challengeTypes) { @@ -1561,20 +1633,22 @@ class ChallengeEditor extends Component { )} {!isTask && ( -
- this.onUpdateOthers({ - field: 'startDate', - value: newValue.format() - })} - readOnly={false} - /> -
+ <> + { + phases.map((phase, index) => ( + { + this.onUpdatePhaseDate(item, index) + }} + /> + ) + ) + } + )} { this.state.isDeleteLaunch && !this.state.isConfirm && ( diff --git a/src/components/DurationInput/DurationInput.module.scss b/src/components/DurationInput/DurationInput.module.scss new file mode 100644 index 00000000..c6a1298c --- /dev/null +++ b/src/components/DurationInput/DurationInput.module.scss @@ -0,0 +1,18 @@ +@import "../../styles/includes"; + +.durationInput { + &:disabled { + cursor: not-allowed !important; + background-color: $inactive !important; + } + + &::-webkit-outer-spin-button, + &::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; + } + + &[type=number] { + -moz-appearance: textfield; + } +} \ No newline at end of file diff --git a/src/components/DurationInput/index.js b/src/components/DurationInput/index.js new file mode 100644 index 00000000..84a38228 --- /dev/null +++ b/src/components/DurationInput/index.js @@ -0,0 +1,43 @@ +import React, { useEffect, useRef } from 'react' +import PropTypes from 'prop-types' +import styles from './DurationInput.module.scss' + +const DurationInput = ({ duration, onDurationChange, index, isActive }) => { + const inputRef = useRef(null) + + useEffect(() => { + document.getElementById(`duration-${index}`).disabled = !isActive + }, [isActive, index]) + + return ( +
+ { + e.preventDefault() + onDurationChange(e.target.value) + }} + onBlur={e => { + e.preventDefault() + onDurationChange(e.target.value, true) + }} + autoFocus={inputRef.current === document.activeElement} + /> +
+ ) +} + +DurationInput.propTypes = { + duration: PropTypes.number, + onDurationChange: PropTypes.func.isRequired, + index: PropTypes.number.isRequired, + isActive: PropTypes.bool.isRequired +} + +export default DurationInput diff --git a/src/components/PhaseInput/PhaseInput.module.scss b/src/components/PhaseInput/PhaseInput.module.scss index 8ec34a00..12ea4720 100644 --- a/src/components/PhaseInput/PhaseInput.module.scss +++ b/src/components/PhaseInput/PhaseInput.module.scss @@ -1,18 +1,33 @@ @import "../../styles/includes"; -@i + +:global { + div[data-reach-popover] { + z-index: 10; + } +} .container { display: flex; + margin-bottom: 10px; } .row { box-sizing: border-box; display: flex; flex-direction: row; - margin: 30px 30px 0 30px; + margin: 20px 30px 0 30px; align-content: space-between; justify-content: flex-start; + .title { + display: flex; + justify-content: center; + flex-direction: column; + margin-right: 10px; + font-size: 14px; + font-weight: 300; + } + .field { @include upto-sm { display: block; @@ -42,6 +57,7 @@ &.phaseName { flex-direction: column; align-items: flex-start; + font-weight: bold; .previewDates { font-size: 13px; @@ -68,7 +84,7 @@ } .dayPicker { - width: 180px; + width: 200px; margin-right: 30px; :global { @@ -85,6 +101,15 @@ } } + .inputField { + margin-right: 30px; + width: 80px; + + input { + padding: 0 0 0 10px; + } + } + .timePicker { width: 90px; @@ -121,4 +146,9 @@ color: black; } - +.dateTimeInput { + &:disabled { + cursor: not-allowed !important; + background-color: $inactive !important; + } +} diff --git a/src/components/PhaseInput/index.js b/src/components/PhaseInput/index.js index 2a87c6a6..aeaa8619 100644 --- a/src/components/PhaseInput/index.js +++ b/src/components/PhaseInput/index.js @@ -1,112 +1,112 @@ -import _ from 'lodash' import moment from 'moment' -import React, { Component } from 'react' +import React from 'react' import PropTypes from 'prop-types' import styles from './PhaseInput.module.scss' import cn from 'classnames' -import DayPickerInput from 'react-day-picker/DayPickerInput' -import TimePicker from 'rc-time-picker' -import { - formatDate, - parseDate -} from 'react-day-picker/moment' import 'react-day-picker/lib/style.css' import 'rc-time-picker/assets/index.css' -import Select from '../Select' +import DateTime from '@nateradebaugh/react-datetime' +import isAfter from 'date-fns/isAfter' +import subDays from 'date-fns/subDays' +import '@nateradebaugh/react-datetime/scss/styles.scss' +import DurationInput from '../DurationInput' -const timeFormat = 'HH:mm' -const dateFormat = 'MM/DD/YYYY' +const dateFormat = 'MM/DD/YYYY HH:mm' +const inputDateFormat = 'MM/dd/yyyy' +const inputTimeFormat = 'HH:mm' +const MAX_LENGTH = 5 -class PhaseInput extends Component { - render () { - const { phase, onUpdateSelect, onUpdatePhase, withDates, withDuration, endDate, readOnly } = this.props - if (_.isEmpty(phase)) return null - const date = moment(phase.date).format(dateFormat) - const time = moment(phase.date) +const PhaseInput = ({ onUpdatePhase, phase, readOnly, phaseIndex }) => { + const { scheduledStartDate: startDate, scheduledEndDate: endDate, duration, isStartTimeActive, isDurationActive } = phase - return ( -
-
-
- - { - withDuration && endDate && ( -
- Ends: - {moment(endDate).format(`${dateFormat} ${timeFormat}`)} -
- ) - } -
-
+ const getEndDate = (startDate, duration) => moment(startDate).add(duration, 'hours').format(dateFormat) + + const onStartDateChange = (e) => { + let startDate = moment(e).format(dateFormat) + let endDate = getEndDate(startDate, duration) + onUpdatePhase({ + startDate, + endDate, + duration + }) + } + + const onDurationChange = (e, isBlur = false) => { + if (e.length > MAX_LENGTH) return null + + let duration = parseInt(e || 0) + let endDate = getEndDate(startDate, duration) + onUpdatePhase({ + startDate, + endDate, + duration, + isBlur + }) + } + + return ( +
+
+
+ +
+
+ Start Date: +
{ - withDates && ( -
- {readOnly ? ( - {date} - ) : ( onUpdatePhase(moment(`${moment(selectedDay).format(dateFormat)} ${time.format(timeFormat)}`, `${dateFormat} ${timeFormat}`))} format={dateFormat} />)} -
+ readOnly || !isStartTimeActive ? ( + {moment(startDate).format(dateFormat)} ) - } - { - withDates && ( -
- {readOnly ? ( - {time.format(timeFormat)} - ) : ( onUpdatePhase(value)} + : ( + { + const yesterday = subDays(new Date(), 1) + return isAfter(current, yesterday) + }} + dateFormat={inputDateFormat} + timeFormat={inputTimeFormat} />)} -
- ) - } - { - withDuration && ( -
- {readOnly ? ( - {phase.duration} - ) : ( onUpdatePhase(e.target.value)} min={1} placeholder='Duration (hours)' />)} -
- ) - } - { - !_.isEmpty(phase.scorecards) && ( -
- onUpdatePhase(e.target.value)} min={1} placeholder='Duration (hours)' />)} +
+ ) + } + { + !_.isEmpty(phase.scorecards) && ( +
+