diff --git a/src/components/Select/Select.jsx b/src/components/Select/Select.jsx
index 9b114966e..92f300a7e 100644
--- a/src/components/Select/Select.jsx
+++ b/src/components/Select/Select.jsx
@@ -16,6 +16,9 @@ const Select = (props) => {
if (props.showDropdownIndicator) {
containerclass = 'react-select-container'
}
+ if (props.heightAuto) {
+ containerclass += ' height-auto'
+ }
if (props.createOption) {
return (
@@ -24,7 +27,6 @@ const Select = (props) => {
createOptionPosition="first"
className={containerclass}
classNamePrefix="react-select"
- noOptionsMessage={() => ('Type to search')}
/>
)
} else {
@@ -34,10 +36,13 @@ const Select = (props) => {
createOptionPosition="first"
className={containerclass}
classNamePrefix="react-select"
- noOptionsMessage={() => ('Type to search')}
/>
)
}
}
+Select.defaultProps = {
+ noOptionsMessage: () => 'Type to search'
+}
+
export default Select
diff --git a/src/components/Select/Select.scss b/src/components/Select/Select.scss
index 682b85c02..f165bb4d4 100644
--- a/src/components/Select/Select.scss
+++ b/src/components/Select/Select.scss
@@ -5,6 +5,13 @@
$reactselectcontentheight: 20px;
@mixin reactselectstyles {
+ &.height-auto .react-select__control{
+ height: auto;
+ & > div {
+ height: auto;
+ }
+ }
+
& .react-select__control:hover {
border-color: $tc-gray-50;
}
diff --git a/src/projects/detail/components/Accordion/Accordion.jsx b/src/projects/detail/components/Accordion/Accordion.jsx
index 29c1352dd..3c9dcbe67 100644
--- a/src/projects/detail/components/Accordion/Accordion.jsx
+++ b/src/projects/detail/components/Accordion/Accordion.jsx
@@ -22,7 +22,8 @@ const TYPE = {
ADD_ONS: 'add-ons',
TEXTINPUT: 'textinput',
TEXTBOX: 'textbox',
- NUMBERINPUT: 'numberinput'
+ NUMBERINPUT: 'numberinput',
+ SKILLS: 'skills'
}
/**
@@ -33,7 +34,7 @@ const TYPE = {
* @returns {Function} valueMapper
*/
const createValueMapper = (valuesMap) => (value) => (
- valuesMap[value] && (valuesMap[value].summaryLabel || valuesMap[value].label)
+ valuesMap[value] && (valuesMap[value].summaryLabel || valuesMap[value].label || valuesMap[value].title)
)
class Accordion extends React.Component {
@@ -113,6 +114,7 @@ class Accordion extends React.Component {
case TYPE.CHECKBOX_GROUP: return value.map(mapValue).join(', ')
case TYPE.RADIO_GROUP: return mapValue(value)
case TYPE.ADD_ONS: return `${value.length} selected`
+ case TYPE.SKILLS: return value.map(mapValue).join(', ')
default: return value
}
}
diff --git a/src/projects/detail/components/SkillsQuestion/SkillsCheckboxGroup.jsx b/src/projects/detail/components/SkillsQuestion/SkillsCheckboxGroup.jsx
new file mode 100644
index 000000000..b56b2ebe7
--- /dev/null
+++ b/src/projects/detail/components/SkillsQuestion/SkillsCheckboxGroup.jsx
@@ -0,0 +1,76 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import cn from 'classnames'
+import './SkillsCheckboxGroup.scss'
+
+class SkillsCheckboxGroup extends Component {
+
+ constructor(props) {
+ super(props)
+ this.changeValue = this.changeValue.bind(this)
+ }
+
+ changeValue() {
+ const value = []
+ this.props.options.forEach((option, key) => {
+ if (this['element-' + key].checked) {
+ value.push(option.value)
+ }
+ })
+ this.props.setValue(value)
+ this.props.onChange(this.props.name, value)
+ }
+
+ render() {
+ const { label, name = 'tc-checkbox-group', options, layout, wrapperClass, getValue, disabled } = this.props
+ const curValue = getValue() || []
+
+ const renderOption = (cb, key) => {
+ const checked = curValue.includes(cb.value)
+ const checkboxDisabled = cb.disabled || disabled
+ const rClass = cn('tc-checkbox-group-item', { disabled, selected: checked })
+ const id = name+'-opt-'+key
+ const setRef = (c) => this['element-' + key] = c
+ return (
+
+
+
+
+
+ {
+ cb.description && checked &&
{cb.description}
+ }
+
+ )
+ }
+ const chkGrpClass = cn('tc-checkbox-group', wrapperClass, {
+ horizontal: layout === 'horizontal',
+ vertical: layout === 'vertical'
+ })
+ return (
+
+
+
{options.map(renderOption)}
+
+ )
+ }
+}
+
+SkillsCheckboxGroup.PropTypes = {
+ options: PropTypes.arrayOf(PropTypes.object).isRequired
+}
+
+SkillsCheckboxGroup.defaultProps = {
+ onChange: () => {}
+}
+
+export default SkillsCheckboxGroup
diff --git a/src/projects/detail/components/SkillsQuestion/SkillsCheckboxGroup.scss b/src/projects/detail/components/SkillsQuestion/SkillsCheckboxGroup.scss
new file mode 100644
index 000000000..9e72d1e8f
--- /dev/null
+++ b/src/projects/detail/components/SkillsQuestion/SkillsCheckboxGroup.scss
@@ -0,0 +1,118 @@
+@import '~tc-ui/src/styles/tc-includes';
+
+.tc-checkbox-group {
+ &.horizontal {
+ .group-options {
+ flex-direction: row;
+ justify-content: space-between;
+ }
+ }
+ &.vertical {
+ .group-options {
+ flex-direction: column;
+ }
+ }
+ .group-label {
+ @include roboto;
+ font-size: 13px;
+ display: block;
+ margin: 10px auto;
+ color: $tc-gray-70;
+ }
+ .group-options {
+ display: flex;
+ flex-direction: column;
+ }
+}
+
+.tc-checkbox-group-item {
+ margin: 0 0 10px 0;
+ background-color: $tc-gray-neutral-light;
+ padding: 10px;
+ border-radius: 4px;
+ display: block;
+ width: auto;
+
+ &:first-child {
+ margin-top: 10px;
+ }
+ &.disabled {
+ > label {
+ color: $tc-gray-30;
+ cursor: default;
+ }
+ }
+ &.selected {
+ background-color: $tc-dark-blue-10;
+ }
+ .checkmark {
+ flex: 0 0 20px;
+ height: 20px;
+ width: 20px;
+ margin: 0;
+ padding: 0;
+ vertical-align: bottom;
+ position: relative;
+ display: inline-block;
+ > input {
+ display: none;
+ &:checked ~ label {
+ background: $tc-dark-blue-100;
+ border-color: $tc-dark-blue-100;
+ &:after {
+ opacity: 1;
+ border-color: $tc-gray-0;
+ }
+ }
+ }
+ > label {
+ font-weight: 400;
+ color: $tc-gray-100;
+ font-size: 12px;
+ cursor: pointer;
+ position: absolute;
+ display: inline-block;
+ width: 20px;
+ height: 20px;
+ top: 0;
+ left: 0;
+ border-radius: 2px;
+ box-shadow: none;
+ border: 1px solid $tc-gray-50;
+ background: $tc-gray-neutral-light;
+ transition: all .150s ease-in-out;
+
+ &:after {
+ opacity: 0;
+ content: '';
+ position: absolute;
+ width: 13px;
+ height: 7px;
+ background: transparent;
+ top: 4px;
+ left: 3px;
+ border: 3px solid $tc-dark-blue-100;
+ border-top: none;
+ border-right: none;
+ transform: rotate(-45deg);
+ transition: all .150s ease-in-out;
+ }
+ }
+ }
+ .item-description {
+ color: $tc-gray-70;
+ font-size: 13px;
+ margin-top: 10px;
+ line-height: 20px;
+ }
+ > label {
+ @include roboto;
+ color: $tc-gray-100;
+ margin-right: 0;
+ display: inline-block;
+ vertical-align: middle;
+ margin-left: 10px;
+ user-select: none;
+ cursor: pointer;
+ }
+}
diff --git a/src/projects/detail/components/SkillsQuestion/SkillsQuestion.jsx b/src/projects/detail/components/SkillsQuestion/SkillsQuestion.jsx
new file mode 100644
index 000000000..524209487
--- /dev/null
+++ b/src/projects/detail/components/SkillsQuestion/SkillsQuestion.jsx
@@ -0,0 +1,110 @@
+import React from 'react'
+import _ from 'lodash'
+import PropTypes from 'prop-types'
+import { HOC as hoc } from 'formsy-react'
+import SkillsCheckboxGroup from './SkillsCheckboxGroup'
+import Select from '../../../../components/Select/Select'
+import './SkillsQuestion.scss'
+
+class SkillsQuestion extends React.Component {
+ constructor(props) {
+ super(props)
+ this.handleChange = this.handleChange.bind(this)
+ }
+
+ handleChange(val = []) {
+ const { setValue, onChange, name } = this.props
+ onChange(name, val)
+ setValue(val)
+ }
+
+ componentDidUpdate(prevProps) {
+ const { skillsCategoriesField, currentProjectData, options, getValue, onChange, setValue, name } = this.props
+ const prevSelectedCategories = _.get(prevProps.currentProjectData, skillsCategoriesField, [])
+ const selectedCategories = _.get(currentProjectData, skillsCategoriesField, [])
+
+ if (selectedCategories.length !== prevSelectedCategories.length) {
+ const currentValues = getValue() || []
+ const prevAvailableOptions = options.filter(option => _.intersection(option.categories, prevSelectedCategories).length > 0)
+ const nextAvailableOptions = options.filter(option => _.intersection(option.categories, selectedCategories).length > 0)
+ const prevValues = currentValues.filter(skill => prevAvailableOptions.some(option => option.value === skill))
+ const nextValues = currentValues.filter(skill => nextAvailableOptions.some(option => option.value === skill))
+
+ if (prevValues.length < nextValues.length) {
+ onChange(name, prevValues)
+ setValue(prevValues)
+ } else if (prevValues.length > nextValues.length) {
+ onChange(name, nextValues)
+ setValue(nextValues)
+ }
+ }
+ }
+
+ render() {
+ const {
+ isFormDisabled,
+ isPristine,
+ isValid,
+ getErrorMessage,
+ validationError,
+ disabled,
+ currentProjectData,
+ skillsCategoriesField,
+ options,
+ getValue
+ } = this.props
+
+ const selectedCategories = _.get(currentProjectData, skillsCategoriesField, [])
+ const availableOptions = options.filter(option => _.intersection(option.categories, selectedCategories).length > 0)
+ let currentValues = getValue() || []
+ currentValues = currentValues.filter(skill => availableOptions.some(option => option.value === skill))
+
+ const questionDisabled = isFormDisabled() || disabled || selectedCategories.length === 0
+ const hasError = !isPristine() && !isValid()
+ const errorMessage = getErrorMessage() || validationError
+
+ const checkboxGroupOptions = availableOptions.filter(option => option.isFrequent)
+ const checkboxGroupValues = currentValues.filter(val => _.some(checkboxGroupOptions, option => option.value === val ))
+ const selectGroupOptions = availableOptions.filter(option => !option.isFrequent)
+ const selectGroupValues = currentValues.filter(val => _.some(selectGroupOptions, option => option.value === val ))
+
+ return (
+
+
checkboxGroupValues}
+ setValue={(val) => { this.handleChange(_.union(val, selectGroupValues)) }}
+ />
+
+
+ { hasError && ({errorMessage}
) }
+
+ )
+ }
+}
+
+SkillsQuestion.propTypes = {
+ options: PropTypes.arrayOf(PropTypes.object).isRequired
+}
+
+SkillsQuestion.defaultProps = {
+ onChange: () => {}
+}
+
+export default hoc(SkillsQuestion)
diff --git a/src/projects/detail/components/SkillsQuestion/SkillsQuestion.scss b/src/projects/detail/components/SkillsQuestion/SkillsQuestion.scss
new file mode 100644
index 000000000..ee29bbb9f
--- /dev/null
+++ b/src/projects/detail/components/SkillsQuestion/SkillsQuestion.scss
@@ -0,0 +1,22 @@
+@import '~tc-ui/src/styles/tc-includes';
+
+.select-wrapper {
+ margin: 5px 0 15px 0;
+}
+.error-message{
+ display:block;
+ margin: 5px auto;
+ @include roboto;
+
+ color: $tc-red-70;
+ font-size: 13px;
+ line-height:20px;
+ font-style:italic;
+ border: 1px solid $tc-red-30;
+ background: $tc-red-10;
+ padding:10px;
+ border-radius: 2px;
+ strong{
+ @include roboto-bold;
+ }
+}
diff --git a/src/projects/detail/components/SpecQuestions.jsx b/src/projects/detail/components/SpecQuestions.jsx
index 0c0916964..de9c6fa9b 100644
--- a/src/projects/detail/components/SpecQuestions.jsx
+++ b/src/projects/detail/components/SpecQuestions.jsx
@@ -9,6 +9,7 @@ import cn from 'classnames'
import SpecQuestionList from './SpecQuestionList/SpecQuestionList'
import SpecQuestionIcons from './SpecQuestionList/SpecQuestionIcons'
+import SkillsQuestion from './SkillsQuestion/SkillsQuestion'
import SpecFeatureQuestion from './SpecFeatureQuestion'
import ColorSelector from './../../../components/ColorSelector/ColorSelector'
import SelectDropdown from './../../../components/SelectDropdown/SelectDropdown'
@@ -298,6 +299,15 @@ const SpecQuestions = ({
hideTitle: true
})
break
+ case 'skills':
+ ChildElem = SkillsQuestion
+ _.assign(elemProps, {
+ currentProjectData,
+ options: q.skills,
+ skillsCategoriesField: q.skillsCategoriesField,
+ fieldName: q.fieldName
+ })
+ break
default:
ChildElem = () => (
@@ -350,12 +360,12 @@ const SpecQuestions = ({
// hide question in edit mode if configured
(isCreation || !question.hiddenOnEdit)
).map((q, index) => (
- _.includes(['checkbox-group', 'radio-group', 'add-ons', 'textinput', 'textbox', 'numberinput'], q.type) && q.visibilityForRendering === STEP_VISIBILITY.READ_OPTIMIZED ? (
+ _.includes(['checkbox-group', 'radio-group', 'add-ons', 'textinput', 'textbox', 'numberinput', 'skills'], q.type) && q.visibilityForRendering === STEP_VISIBILITY.READ_OPTIMIZED ? (
{renderQ(q, index)}