@@ -452,7 +459,7 @@ class SpecQuestions extends React.Component {
!(question.type === 'estimation' && template.hideEstimation)
).map((q, index) => {
return (
- _.includes(['checkbox', 'checkbox-group', 'radio-group', 'add-ons', 'textinput', 'textbox', 'numberinput', 'skills', 'slide-radiogroup', 'slider-standard', 'select-dropdown', 'talent-picker'], q.type) && q.visibilityForRendering === STEP_VISIBILITY.READ_OPTIMIZED ? (
+ _.includes(['checkbox', 'checkbox-group', 'radio-group', 'add-ons', 'textinput', 'textbox', 'numberinput', 'skills', 'slide-radiogroup', 'slider-standard', 'select-dropdown', 'talent-picker', 'talent-picker-v2'], q.type) && q.visibilityForRendering === STEP_VISIBILITY.READ_OPTIMIZED ? (
{
+ if (!required) {
+ return true
+ }
+ return _.some(value, (v) => {
+ return v.people !== '0' && v.duration !== '0' && v.skills.length > 0 && v.workLoad.value !== null && v.jobDescription.length
+ }) // validation body
+ },
+ noPartialFillsExist: (formValues, value) => {
+ return _.every(value, v => {
+
+ const isOneValueFilled = v.people > 0 || v.duration > 0 || (v.skills && v.skills.length) || (v.jobDescription && v.jobDescription.length) || (v.workLoad && v.workLoad.value !== null)
+ const isAllValuesFilled = v.people > 0 && v.duration > 0 && v.skills && v.skills.length && v.jobDescription.length && v.workLoad.value !== null
+ // If one value is filled, all values should be filled to make this row valid. Partial fill is not valid
+ const isRowValid = !isOneValueFilled || isAllValuesFilled
+ return isRowValid
+ })
+ }
+ }
+ setValidations(validations)
+ }
+
+ updateOptions(props) {
+ const options = props.options.map(o => ({...o, skillsCategories: o.skillsCategory ? [ o.skillsCategory ] : null}))
+ this.setState({ options })
+ }
+
+ getDefaultValue() {
+ const { options } = this.props
+ return options.map((o) => ({
+ role: o.role,
+ people: '0',
+ duration: '0',
+ skills: [],
+ additionalSkills: [],
+ workLoad: { value: null, title: 'Select Workload'},
+ jobDescription: ''
+ }))
+ }
+
+ onChange(value) {
+ const {setValue, name} = this.props
+
+ setValue(value)
+ this.props.onChange(name, value)
+ }
+
+ handleValueChange(index, field, value) {
+ const { getValue } = this.props
+ let values = getValue() || this.getDefaultValue()
+ values = [...values.slice(0, index), { ...values[index], [field]: value }, ...values.slice(index + 1)]
+
+ this.onChange(values)
+ }
+
+ insertRole(index, role) {
+ const { getValue } = this.props
+ let values = getValue() || this.getDefaultValue()
+
+ values = [
+ ...values.slice(0, index),
+ {
+ role,
+ people: '0',
+ duration: '0',
+ skills: [],
+ additionalSkills: [],
+ workLoad: { value: null, title: 'Select Workload'},
+ jobDescription: '',
+ },
+ ...values.slice(index)
+ ]
+ this.onChange(values)
+ }
+
+ removeRole(index) {
+ const { getValue } = this.props
+ let values = getValue() || this.getDefaultValue()
+ values = [...values.slice(0, index), ...values.slice(index + 1)]
+ this.onChange(values)
+ }
+
+ canDeleteRole(role, index) {
+ const { getValue } = this.props
+ const values = getValue() || this.getDefaultValue()
+ return _.findIndex(values, { role }) !== index
+ }
+
+ render() {
+ const { wrapperClass, getValue } = this.props
+ const { options } = this.state
+
+ const errorMessage =
+ this.props.getErrorMessage() || this.props.validationError
+ const hasError = !this.props.isPristine() && !this.props.isValid()
+
+ const values = getValue() || this.getDefaultValue()
+
+ return (
+
+
+ {options.length > 0 ? values.map((v, roleIndex) => {
+ const roleSetting = _.find(options, { role: v.role })
+ return (
+
+ )
+ }) : null}
+
+ {hasError ?
{errorMessage}
: null}
+
+ )
+ }
+}
+
+TalentPickerQuestionV2.PropTypes = {
+ options: PropTypes.arrayOf(
+ PropTypes.shape({
+ role: PropTypes.string.isRequired,
+ skillsCategory: PropTypes.string.isRequired,
+ roleTitle: PropTypes.string.isRequired,
+ disabled: PropTypes.bool,
+ })
+ ).isRequired,
+ onChange: PropTypes.func,
+}
+
+TalentPickerQuestionV2.defaultProps = {
+ onChange: _.noop
+}
+
+export default hoc(TalentPickerQuestionV2)
diff --git a/src/projects/detail/components/TalentPickerRow/TalentPickerRowV2.jsx b/src/projects/detail/components/TalentPickerRow/TalentPickerRowV2.jsx
new file mode 100644
index 000000000..acf9eb8c5
--- /dev/null
+++ b/src/projects/detail/components/TalentPickerRow/TalentPickerRowV2.jsx
@@ -0,0 +1,280 @@
+import React from 'react'
+import PT from 'prop-types'
+import cn from 'classnames'
+
+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 SelectDropdown from 'appirio-tech-react-components/components/SelectDropdown/SelectDropdown'
+
+import styles from './TalentPickerRowV2.scss'
+
+const always = () => true
+const never = () => false
+const emptyError = () => ''
+
+class TalentPickerRowV2 extends React.PureComponent {
+ constructor(props) {
+ super(props)
+
+ this.handlePeopleChange = this.handlePeopleChange.bind(this)
+ this.handleDurationChange = this.handleDurationChange.bind(this)
+ this.handleSkillChange = this.handleSkillChange.bind(this)
+ this.handleWorkloadChange = this.handleWorkloadChange.bind(this)
+ this.handleJobDescriptionChange = this.handleJobDescriptionChange.bind(this)
+ this.handleAdditionalSkillChange = this.handleAdditionalSkillChange.bind(this)
+
+ this.resetPeople = this.resetPeople.bind(this)
+ this.resetDuration = this.resetDuration.bind(this)
+
+ this.onAddRow = this.onAddRow.bind(this)
+ this.onDeleteRow = this.onDeleteRow.bind(this)
+
+ this.workloadOptions = [
+ { value: null, title: 'Select Workload'},
+ { value: 'fulltime', title: 'Full-Time'},
+ { value: 'fractional', title: 'Fractional'}
+ ]
+ }
+
+ handlePeopleChange(evt) {
+ this.props.onChange(this.props.rowIndex, 'people', evt.target.value)
+ }
+
+ handleDurationChange(evt) {
+ this.props.onChange(this.props.rowIndex, 'duration', evt.target.value)
+ }
+
+ handleSkillChange(value) {
+ this.props.onChange(this.props.rowIndex, 'skills', value)
+ }
+
+ handleWorkloadChange(evt) {
+ this.props.onChange(this.props.rowIndex, 'workLoad', evt)
+ }
+
+ handleJobDescriptionChange(evt) {
+ this.props.onChange(this.props.rowIndex, 'jobDescription', evt.target.value)
+ }
+
+ handleAdditionalSkillChange(value) {
+ this.props.onChange(this.props.rowIndex, 'additionalSkills', value)
+ }
+ resetDuration() {
+ const { rowIndex, onChange, value } = this.props
+ if (!value.duration) {
+ onChange(rowIndex, 'duration', '0')
+ }
+ }
+
+ resetPeople() {
+ const { rowIndex, onChange, value } = this.props
+ if (!value.people) {
+ onChange(rowIndex, 'people', '0')
+ }
+ }
+
+ onAddRow() {
+ const { rowIndex, value, onAddRow: addRowHandler } = this.props
+ addRowHandler(rowIndex + 1, value.role)
+ }
+
+ onDeleteRow() {
+ const { rowIndex, onDeleteRow: deleteRowHandler } = this.props
+ deleteRowHandler(rowIndex)
+ }
+
+ render() {
+ const { value, canBeDeleted, roleSetting, rowIndex } = this.props
+ const isRowIncomplete = value.people > 0 || value.duration > 0 || (value.skills && value.skills.length)
+ || (value.workLoad && value.workLoad.value !== null) || (value.jobDescription && value.jobDescription.trim() !== '')
+
+ /* Different columns are defined here and used in componsing mobile/desktop views below */
+ const roleColumn = (
+
+
+
+
{roleSetting.roleTitle}
+
+
+ )
+
+ const actionsColumn = (
+
+
+
+
+
+ {canBeDeleted(value.role, rowIndex) && (
+
+
+
+ )}
+
+
+ )
+
+ const peopleColumn = (
+
+
+
+
+ )
+
+ const durationColumn = (
+
+
+
+
+ )
+
+ const skillSelectionColumn = (
+
+
+ {/*
+ 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={cn(styles.noMargin, {[styles.skillHasError]: isRowIncomplete && !(value.skills && value.skills.length)})}
+ />
+
+ )
+
+ const workLoadColumn = (
+
+
+
+
+ )
+
+ const jobDescriptionColumn = (
+
+
+
+
+
+
+ )
+
+ const additionalSkillSelectionColumn = (
+
+
+ {/*
+ 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.additionalSkills}
+ onChange={_.noop}
+ selectWrapperClass={cn(styles.noMargin)}
+ />
+
+ )
+
+ return (
+
+
+ {roleColumn}
+ {actionsColumn}
+
+
+
+ {peopleColumn}
+ {workLoadColumn}
+
+
+ {durationColumn}
+
+
+ {jobDescriptionColumn}
+
+
+
{skillSelectionColumn}
+
{additionalSkillSelectionColumn}
+
+ )
+ }
+}
+
+TalentPickerRowV2.propTypes = {
+ rowIndex: PT.number.isRequired,
+ canBeDeleted: PT.func.isRequired,
+ onChange: PT.func.isRequired,
+ onAddRow: PT.func.isRequired,
+ onDeleteRow: PT.func.isRequired,
+ roleSetting: PT.shape({
+ roleTitle: PT.string.isRequired,
+ skillsCategories: PT.arrayOf(PT.string),
+ }).isRequired,
+ value: PT.shape({
+ role: PT.string.isRequired,
+ people: PT.string.isRequired,
+ duration: PT.string.isRequired,
+ skills: PT.array,
+ }),
+}
+
+TalentPickerRowV2.defaultProps = {}
+
+export default TalentPickerRowV2
diff --git a/src/projects/detail/components/TalentPickerRow/TalentPickerRowV2.scss b/src/projects/detail/components/TalentPickerRow/TalentPickerRowV2.scss
new file mode 100644
index 000000000..6b915ce8d
--- /dev/null
+++ b/src/projects/detail/components/TalentPickerRow/TalentPickerRowV2.scss
@@ -0,0 +1,138 @@
+@import "~tc-ui/src/styles/tc-includes";
+@import "../../../../styles/includes";
+
+.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;
+}
+
+.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;
+ }
+
+ .job-description textarea,
+ .job-textarea[type="text"] {
+ &.empty {
+ color: $tc-gray-30;
+ }
+ min-height : 90px;
+
+ @include placeholder {
+ @include roboto;
+ color: $tc-gray-30;
+ text-transform: none;
+ font-style: italic;
+ font-size: $base-unit*3;
+ line-height: $base-unit*4;
+ }
+ }
+}
+
+.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;
+}
+
+
+.skillHasError {
+ :global {
+ .react-select__control {
+ border-color: $tc-red-70;
+ }
+ }
+}
diff --git a/src/projects/detail/containers/ProjectInfoContainer.js b/src/projects/detail/containers/ProjectInfoContainer.js
index f4d252ecc..8cdb794ed 100644
--- a/src/projects/detail/containers/ProjectInfoContainer.js
+++ b/src/projects/detail/containers/ProjectInfoContainer.js
@@ -17,6 +17,7 @@ import {
PROJECT_ROLE_MANAGER,
DIRECT_PROJECT_URL,
SALESFORCE_PROJECT_LEAD_LINK,
+ WORK_MANAGER_APP,
PROJECT_STATUS_CANCELLED,
PROJECT_STATUS_ACTIVE,
PROJECT_STATUS_COMPLETED,
@@ -25,6 +26,7 @@ import {
PROJECT_ROLE_PROJECT_MANAGER,
PROJECT_ROLE_PROGRAM_MANAGER,
PROJECT_ROLE_SOLUTION_ARCHITECT,
+ PROJECT_CATEGORY_TAAS,
} from '../../../config/constants'
import PERMISSIONS from '../../../config/permissions'
import { hasPermission } from '../../../helpers/permissions'
@@ -425,6 +427,14 @@ class ProjectInfoContainer extends React.Component {
const { showDeleteConfirm } = this.state
const { project, currentMemberRole, isSuperUser, phases, hideInfo, hideMembers,
productsTimelines, isProjectProcessing, notifications, projectTemplates } = this.props
+
+ const projectTemplateId = project.templateId
+ const projectTemplateKey = _.get(project, 'details.products[0]')
+ const projectTemplate = projectTemplateId
+ ? _.find(projectTemplates, pt => pt.id === projectTemplateId)
+ : getProjectTemplateByKey(projectTemplates, projectTemplateKey)
+
+ const isTaaS = PROJECT_CATEGORY_TAAS === projectTemplate.category
let directLinks = null
// check if direct links need to be added
const isMemberOrCopilot = _.indexOf([
@@ -436,6 +446,8 @@ class ProjectInfoContainer extends React.Component {
], currentMemberRole) > -1
if (isMemberOrCopilot || isSuperUser) {
directLinks = []
+ if(!isTaaS)
+ directLinks.push({name: 'Launch Work Manager', href: `${WORK_MANAGER_APP}/${project.id}/challenges`})
if (project.directProjectId) {
directLinks.push({name: 'Project in Topcoder Direct', href: `${DIRECT_PROJECT_URL}${project.directProjectId}`})
} else {
@@ -451,12 +463,6 @@ class ProjectInfoContainer extends React.Component {
const notReadPhaseNotifications = filterTopicAndPostChangedNotifications(projectNotReadNotifications, /^phase#\d+$/)
const notReadAssetsNotifications = filterFileAndLinkChangedNotifications(projectNotReadNotifications)
- const projectTemplateId = project.templateId
- const projectTemplateKey = _.get(project, 'details.products[0]')
- const projectTemplate = projectTemplateId
- ? _.find(projectTemplates, pt => pt.id === projectTemplateId)
- : getProjectTemplateByKey(projectTemplates, projectTemplateKey)
-
const renderFAQs = containsFAQ(projectTemplate)
const navLinks = getProjectNavLinks(project, project.id, renderFAQs).map((navLink) => {
if (navLink.label === 'Messages') {
@@ -492,6 +498,7 @@ class ProjectInfoContainer extends React.Component {
const isProjectActive = project.status === PROJECT_STATUS_ACTIVE
const isV3Project = project.version === 'v3'
const projectCanBeActive = !isV3Project || (!isProjectActive && hasReviewedOrActivePhases) || isProjectActive
+ const BillingInfo = project.billingAccountId ? `BillingAccountId - ${project.billingAccountId}` : 'Billing Account Not Attached'
return (
@@ -547,6 +554,16 @@ class ProjectInfoContainer extends React.Component {
+ {project.billingAccountId ?
+
+ {BillingInfo}
+
+ :
+
+ {BillingInfo}
+
+
+ }
}
{!hideInfo &&
diff --git a/src/projects/detail/containers/ProjectInfoContainer.scss b/src/projects/detail/containers/ProjectInfoContainer.scss
index a12ee6fe4..0e72b7755 100644
--- a/src/projects/detail/containers/ProjectInfoContainer.scss
+++ b/src/projects/detail/containers/ProjectInfoContainer.scss
@@ -48,6 +48,18 @@
letter-spacing: 0.8px;
}
+.administration-section .billing-account-info {
+ @include roboto-medium;
+ font-size: $tc-label-md;
+ color: $tc-gray-40;
+ line-height: $base-unit*6;
+ padding-left: 4 * $base_unit;
+
+ @media screen and (max-width: $screen-md - 1px) {
+ padding-bottom: 4 * $base_unit;
+ }
+}
+
.all-project-link-wrapper {
display: flex;
padding: 20px;
@@ -60,6 +72,8 @@
margin-top: 20px;
padding: 5px 0;
+
+
:global {
.panel {
// avoid double padding for editable project status