diff --git a/src/components/PositiveNumberInput/PositiveNumberInput.jsx b/src/components/PositiveNumberInput/PositiveNumberInput.jsx
new file mode 100644
index 000000000..7f01cf7ff
--- /dev/null
+++ b/src/components/PositiveNumberInput/PositiveNumberInput.jsx
@@ -0,0 +1,69 @@
+import React from 'react'
+import PT from 'prop-types'
+import { noop, omit } from 'lodash'
+
+class PositiveNumberInput extends React.PureComponent {
+ constructor(props) {
+ super(props)
+
+ this.isInputValid = true
+
+ this.onKeyDown = this.onKeyDown.bind(this)
+ this.onPaste = this.onPaste.bind(this)
+ this.onKeyUp = this.onKeyUp.bind(this)
+ }
+
+ onKeyDown(evt) {
+ const isPrintableKey = evt.key.length === 1 && !(evt.ctrlKey || evt.metaKey)
+ const digitPattern = /\d/
+
+ // Don't allow typing non digit characters
+ if (isPrintableKey && !digitPattern.test(evt.key)) {
+ evt.preventDefault()
+ }
+ this.props.onKeyDown(evt)
+ }
+
+ onPaste(evt) {
+ const text = evt.clipboardData.getData('text')
+ const digitsPattern = /^\d+$/
+
+ // Don't allow pasting non digit text
+ if (!digitsPattern.test(text)) {
+ evt.preventDefault()
+ }
+ this.props.onPaste(evt)
+ }
+
+ onKeyUp(evt) {
+ const isValid = evt.target.validity.valid
+ if (isValid !== this.isInputValid) {
+ this.isInputValid = isValid
+ this.props.onValidityChange(isValid)
+ }
+ this.props.onKeyUp(evt)
+ }
+
+ render() {
+ const props = omit(this.props, ['onValidityChange'])
+ return
+ }
+}
+
+PositiveNumberInput.defaultProps = {
+ onKeyDown: noop,
+ onPaste: noop,
+ onKeyUp: noop,
+ onValidityChange: noop
+
+}
+
+PositiveNumberInput.propTypes = {
+ onKeyDown: PT.func,
+ onPaste: PT.func,
+ onKeyUp: PT.func,
+ onValidityChange: PT.func
+}
+
+
+export default PositiveNumberInput
diff --git a/src/projects/detail/components/Accordion/Accordion.jsx b/src/projects/detail/components/Accordion/Accordion.jsx
index 3dad63d4b..f2cf2a905 100644
--- a/src/projects/detail/components/Accordion/Accordion.jsx
+++ b/src/projects/detail/components/Accordion/Accordion.jsx
@@ -171,7 +171,8 @@ class Accordion extends React.Component {
case TYPE.SELECT_DROPDOWN: return mapValue(value)
case TYPE.TALENT_PICKER: {
const getRoleName = (role) => _.find(options, { role }).roleTitle
- return _.filter(value, (v) => v.people !== '0').map((v) => `${getRoleName(v.role)}: ${v.people}`).join(', ')
+ const totalPeoplePerRole = _.mapValues(_.groupBy(value, v => v.role), valuesUnderGroup => _.sumBy(valuesUnderGroup, v => Number(v.people)))
+ return _.toPairs(totalPeoplePerRole).filter(([, people]) => people > 0).map(([role, people]) => `${getRoleName(role)} ${people}`).join(', ')
}
default: return value
}
diff --git a/src/projects/detail/components/SkillsQuestion/SkillsQuestionBase.jsx b/src/projects/detail/components/SkillsQuestion/SkillsQuestionBase.jsx
index 0bb34e060..8896234d4 100644
--- a/src/projects/detail/components/SkillsQuestion/SkillsQuestionBase.jsx
+++ b/src/projects/detail/components/SkillsQuestion/SkillsQuestionBase.jsx
@@ -165,6 +165,7 @@ class SkillsQuestion extends React.PureComponent {
skillsCategories,
currentProjectData,
categoriesField,
+ selectWrapperClass,
} = this.props
const { availableOptions, customOptionValue } = this.state
@@ -189,13 +190,14 @@ class SkillsQuestion extends React.PureComponent {
return (
-
checkboxGroupValues}
- setValue={(val) => { this.handleChange(_.union(val, selectGroupValues)) }}
- />
-
+ {checkboxGroupOptions.length > 0 ? (
+
checkboxGroupValues}
+ setValue={(val) => { this.handleChange(_.union(val, selectGroupValues)) }}
+ />) : null}
+
-
-
-
- Role
- Number of People Required
- Engagement Duration
- Skill Required
-
-
-
-
- {options.length > 0 ? values.map((v, roleIndex) => {
- const roleSetting = _.find(options, { role: v.role })
- return (
-
- )
- }) : null}
-
-
+
+
Role
+
People
+
Duration (months)
+
+
+
+ {options.length > 0 ? values.map((v, roleIndex) => {
+ const roleSetting = _.find(options, { role: v.role })
+ return (
+
+ )
+ }) : null}
+
{hasError ? {errorMessage}
: null}
diff --git a/src/projects/detail/components/TalentPickerQuestion/TalentPickerQuestion.scss b/src/projects/detail/components/TalentPickerQuestion/TalentPickerQuestion.scss
index a2da9f944..9f0bbe9ac 100644
--- a/src/projects/detail/components/TalentPickerQuestion/TalentPickerQuestion.scss
+++ b/src/projects/detail/components/TalentPickerQuestion/TalentPickerQuestion.scss
@@ -1,30 +1,61 @@
@import '~tc-ui/src/styles/tc-includes';
+@import '../../../../styles/includes';
.container {
font-size: 15px;
- overflow-x: auto;
+ text-align: left;
+ margin-top: 5px;
}
-.table {
- text-align: left;
+.header-row {
+ @include roboto;
+
+ white-space: nowrap;
+ padding: 0 4px;
+ color: $tc-silver-110;
+ font-size: 13px;
+ line-height: 15px;
+ background-color: $tc-gray-neutral-light;
+ border: 1px solid $tc-gray-neutral-dark;
+ display: flex;
+
+ .body {
+ @include roboto;
- td,
- th {
- white-space: nowrap;
- padding: 10px;
+ color: $tc-gray-100;
}
+}
+
+
+.col-title {
+ padding: 15px 7.5px;
+ flex-grow: 0;
+ flex-shrink: 0;
- thead {
- @include roboto-bold;
+ &.col-duration,
+ &.col-people {
+ width: 50%;
+ flex-grow: 1;
+ flex-shrink: 1;
+ min-width: 0;
}
- tbody {
- tr {
- border: 1px solid $tc-gray-80;
- }
+ &.col-role {
+ width: 168px;
+ }
+
+ &.col-actions {
+ width: 76px;
}
}
.hide {
display: none;
}
+
+
+@media screen and (max-width: $screen-rg - 1px) {
+ .header-row {
+ display: none;
+ }
+}
diff --git a/src/projects/detail/components/TalentPickerRow/TalentPickerRow.jsx b/src/projects/detail/components/TalentPickerRow/TalentPickerRow.jsx
index 4cfb8a89e..1bb77eb36 100644
--- a/src/projects/detail/components/TalentPickerRow/TalentPickerRow.jsx
+++ b/src/projects/detail/components/TalentPickerRow/TalentPickerRow.jsx
@@ -1,12 +1,15 @@
import React from 'react'
import PT from 'prop-types'
+import MediaQuery from 'react-responsive'
-import SelectDropdown from '../../../../components/SelectDropdown/SelectDropdownBase'
-import IconX from '../../../../assets/icons/icon-x-mark.svg'
-import IconAdd from '../../../../assets/icons/icon-ui-bold-add.svg'
+import IconX from '../../../../assets/icons/ui-16px-1_bold-remove.svg'
+import IconAdd from '../../../../assets/icons/ui-16px-1_bold-add.svg'
import SkillsQuestion from '../SkillsQuestion/SkillsQuestionBase'
+import PositiveNumberInput from '../../../../components/PositiveNumberInput/PositiveNumberInput'
+import ProductTypeIcon from '../../../../components/ProductTypeIcon'
-import './TalentPickerRow.scss'
+import styles from './TalentPickerRow.scss'
+import { SCREEN_BREAKPOINT_MD, SCREEN_BREAKPOINT_RG } from '../../../../config/constants'
const always = () => true
const never = () => false
@@ -16,9 +19,6 @@ class TalentPickerRow extends React.PureComponent {
constructor(props) {
super(props)
- this.peopleOptions = this.getPeopleOptions()
- this.durationOptions = this.getDurationOptions()
-
this.handlePeopleChange = this.handlePeopleChange.bind(this)
this.handleDurationChange = this.handleDurationChange.bind(this)
this.handleSkillChange = this.handleSkillChange.bind(this)
@@ -27,12 +27,12 @@ class TalentPickerRow extends React.PureComponent {
this.onDeleteRow = this.onDeleteRow.bind(this)
}
- handlePeopleChange(value) {
- this.props.onChange(this.props.rowIndex, 'people', value)
+ handlePeopleChange(evt) {
+ this.props.onChange(this.props.rowIndex, 'people', evt.target.value)
}
- handleDurationChange(value) {
- this.props.onChange(this.props.rowIndex, 'duration', value)
+ handleDurationChange(evt) {
+ this.props.onChange(this.props.rowIndex, 'duration', evt.target.value)
}
handleSkillChange(value) {
@@ -49,65 +49,142 @@ class TalentPickerRow extends React.PureComponent {
deleteRowHandler(rowIndex)
}
- getPeopleOptions() {
- return ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '10+'].map((v) => ({
- value: v,
- title: v,
- }))
- }
-
- getDurationOptions() {
- return ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'].map((v) => ({
- value: v,
- title: v,
- hide: v === '0',
- }))
- }
-
render() {
const { value, canBeDeleted, roleSetting, rowIndex } = this.props
- return (
-
- {roleSetting.roleTitle}
-
-
-
-
-
-
-
- {/*
- Please do not refactor getValue prop's value to a binded function with constant reference.
- SkillsQuestion is a pure component. If all the props are constant across renders, SkillsQuestion cannot detect the change in value.skills.
- So, it'll break the functionality of the component.
- "getValue" prop is left as inline arrow function to trigger re rendering of the SkillsQuestion component whenever the parent rerenders.
- */}
- value.skills}
- onChange={_.noop}
- />
-
-
-
-
-
+ /* Different columns are defined here and used in componsing mobile/desktop views below */
+ const roleColumn = (
+
+
+
+
{roleSetting.roleTitle}
+
+
+ )
+
+ const actionsColumn = (
+
+
+
+
+
+ {canBeDeleted(value.role, rowIndex) && (
+
+
- {canBeDeleted(value.role, rowIndex) && (
-
-
+ )}
+
+
+ )
+
+ const peopleColumn = (
+
+ )
+
+ const durationColumn = (
+
+
+ Duration (months)
+
+
+
+ )
+
+ const skillSelectionColumn = (
+
+
+ Skills
+
+ {/*
+ Please do not refactor getValue prop's value to a binded function with constant reference.
+ SkillsQuestion is a pure component. If all the props are constant across renders, SkillsQuestion cannot detect the change in value.skills.
+ So, it'll break the functionality of the component.
+ "getValue" prop is left as inline arrow function to trigger re rendering of the SkillsQuestion component whenever the parent rerenders.
+ */}
+ value.skills}
+ onChange={_.noop}
+ selectWrapperClass={styles.noMargin}
+ />
+
+ )
+
+ return (
+
+ {(matches) => {
+ return matches ? (
+ // Desktop Layout (992px+)
+
+
+ {roleColumn}
+ {peopleColumn}
+ {durationColumn}
+ {actionsColumn}
- )}
-
-
-
+ {skillSelectionColumn}
+
+ ) : (
+
+ {(matches) => {
+ return matches ? (
+ // Tablet Layout (768px to 991px)
+
+
+ {roleColumn}
+ {actionsColumn}
+
+
+
+ {peopleColumn}
+ {durationColumn}
+ {skillSelectionColumn}
+
+
+ ) : (
+ // Mobile Layout (till 767px)
+
+
+ {roleColumn}
+ {actionsColumn}
+
+
+
+ {peopleColumn}
+ {durationColumn}
+
+
+
{skillSelectionColumn}
+
+ )
+ }}
+
+ )
+ }}
+
)
}
}
@@ -120,13 +197,13 @@ TalentPickerRow.propTypes = {
onDeleteRow: PT.func.isRequired,
roleSetting: PT.shape({
roleTitle: PT.string.isRequired,
- skillsCategories: PT.arrayOf(PT.string)
+ skillsCategories: PT.arrayOf(PT.string),
}).isRequired,
value: PT.shape({
role: PT.string.isRequired,
people: PT.string.isRequired,
duration: PT.string.isRequired,
- skills: PT.array
+ skills: PT.array,
}),
}
diff --git a/src/projects/detail/components/TalentPickerRow/TalentPickerRow.scss b/src/projects/detail/components/TalentPickerRow/TalentPickerRow.scss
index 35c08da95..8f8ab7ca9 100644
--- a/src/projects/detail/components/TalentPickerRow/TalentPickerRow.scss
+++ b/src/projects/detail/components/TalentPickerRow/TalentPickerRow.scss
@@ -1,12 +1,156 @@
-.skill-selection {
- min-width: 200px;
-}
+@import "~tc-ui/src/styles/tc-includes";
+@import "../../../../styles/includes";
-.btn {
+.action-btn {
cursor: pointer;
margin: 0 2px;
+ background-color: $tc-gray-neutral-dark;
+ height: 23px;
+ width: 23px;
+ text-align: center;
+ line-height: 25px;
+ border-radius: 12px;
+
+ svg {
+ fill: $tc-gray-50;
+ height: 10px;
+ }
+}
+
+.action-btn-remove {
+ margin-left: 8px;
}
.d-flex {
display: flex;
-}
\ No newline at end of file
+}
+
+.row {
+ border: 1px solid $tc-gray-neutral-dark;
+ border-top-width: 0;
+ white-space: nowrap;
+ padding: 4px;
+ line-height: 20px;
+ font-size: 13px;
+ vertical-align: top;
+
+ &:first-child {
+ border-top-width: 1px;
+ }
+}
+
+.inner-row {
+ width: 100%;
+ display: flex;
+}
+
+.col {
+ padding: 4px 7.5px;
+ flex-grow: 0;
+ flex-shrink: 0;
+
+ &.col-duration,
+ &.col-people {
+ width: 50%;
+ }
+
+ &.col-skill-selection {
+ flex-grow: 1;
+ flex-shrink: 1;
+ margin-bottom: 4px;
+
+ // Prevents the column from expanding beyond parent due to flex-grow:1
+ min-width: 0;
+ }
+
+ &.col-role {
+ flex-grow: 1;
+
+ // Prevents the column from expanding beyond parent due to flex-grow:1
+ min-width: 0;
+ }
+
+ &.col-actions {
+ width: 76px;
+ }
+
+ // Resets standard margins applied by tc-file-field__inputs elements
+ .noMargin {
+ margin-top: 0;
+ margin-bottom: 0;
+ margin-left: 0;
+ margin-right: 0;
+ }
+
+ .label {
+ display: block;
+ }
+}
+
+.col-role-container {
+ display: flex;
+ align-items: center;
+}
+
+.col-actions-container {
+ justify-content: flex-end;
+ margin-top: 5px;
+}
+
+.col-role {
+ svg {
+ width: 22.5px;
+ margin-right: 11px;
+ flex-grow: 0;
+ flex-shrink: 0;
+ }
+}
+
+.role-text {
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+// Tablet Layout (768px+)
+@media screen and (min-width: $screen-md) {
+ .col {
+ &.col-duration {
+ width: 138px;
+ }
+
+ &.col-people {
+ width: 78px;
+ }
+ }
+}
+
+// Desktop Layout (992px+)
+@media screen and (min-width: $screen-rg) {
+ .row {
+ padding: 8px 4px;
+ }
+
+ .col {
+ &.col-role {
+ width: 168px;
+ flex-grow: 0;
+ }
+
+ &.col-duration,
+ &.col-people {
+ width: 50%;
+ flex-grow: 1;
+ flex-shrink: 1;
+ min-width: 0;
+
+ .label {
+ display: none;
+ }
+ }
+
+ &.col-skill-selection {
+ margin-left: 168px;
+ margin-right: 76px;
+ }
+ }
+}