diff --git a/.circleci/config.yml b/.circleci/config.yml index 897301638..a32f2f31f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -136,7 +136,7 @@ workflows: - build-dev filters: branches: - only: ['feature/unified-permissions', 'feature/accept-reject-terms-in-profile'] + only: ['feature/taas-jobs-2'] - deployProd: context : org-global diff --git a/config/constants/dev.js b/config/constants/dev.js index 930c775a1..4c84dad0a 100644 --- a/config/constants/dev.js +++ b/config/constants/dev.js @@ -54,5 +54,7 @@ module.exports = { DASHBOARD_FAQ_CONTENT_ID : process.env.DASHBOARD_FAQ_CONTENT_ID, CONTENTFUL_DELIVERY_KEY : process.env.CONTENTFUL_DELIVERY_KEY, - CONTENTFUL_SPACE_ID : process.env.CONTENTFUL_SPACE_ID + CONTENTFUL_SPACE_ID : process.env.CONTENTFUL_SPACE_ID, + + TAAS_APP_URL: 'https://platform.topcoder-dev.com/taas' } diff --git a/config/constants/master.js b/config/constants/master.js index 6234a4e14..12d635aa3 100644 --- a/config/constants/master.js +++ b/config/constants/master.js @@ -54,5 +54,7 @@ module.exports = { DASHBOARD_FAQ_CONTENT_ID : process.env.DASHBOARD_FAQ_CONTENT_ID, CONTENTFUL_DELIVERY_KEY : process.env.CONTENTFUL_DELIVERY_KEY, - CONTENTFUL_SPACE_ID : process.env.CONTENTFUL_SPACE_ID + CONTENTFUL_SPACE_ID : process.env.CONTENTFUL_SPACE_ID, + + TAAS_APP_URL: 'https://platform.topcoder.com/taas' } diff --git a/config/constants/qa.js b/config/constants/qa.js index 3be11e79b..5cf3c2ea9 100644 --- a/config/constants/qa.js +++ b/config/constants/qa.js @@ -49,5 +49,7 @@ module.exports = { TC_SYSTEM_USERID: process.env.QA_TC_SYSTEM_USERID, MAINTENANCE_MODE: process.env.QA_MAINTENANCE_MODE, - TC_CDN_URL: process.env.TC_CDN_URL + TC_CDN_URL: process.env.TC_CDN_URL, + + TAAS_APP_URL: 'https://platform.topcoder-dev.com/taas' } diff --git a/src/api/skills.js b/src/api/skills.js new file mode 100644 index 000000000..cdb90e2ef --- /dev/null +++ b/src/api/skills.js @@ -0,0 +1,49 @@ +import { TC_API_URL } from '../config/constants' +import { axiosInstance as axios } from './requestInterceptor' + +const skillPageSize = 100 +let cachedSkillsAsPromise + +/** + * Loads and caches all the skills the first time. Returns the skills list from the cache from the second time. + */ +export function getSkills() { + cachedSkillsAsPromise = cachedSkillsAsPromise || getAllSkills().catch(ex => { + console.error('Error loading skills', ex) + cachedSkillsAsPromise = null + return [] + }) + + return cachedSkillsAsPromise +} + +/** + * Recursively loads all the pages from skills api. + */ +function getAllSkills() { + let skills = [] + + return new Promise((resolve, reject) => { + const loop = (page) => getSkillsPage(page) + .then((skillResponse) => { + skills = skills.concat(skillResponse.data) + if (skillResponse.data.length === skillPageSize) { + page++ + loop(page) + } else { + resolve(skills) + } + }) + .catch(ex => reject(ex)) + + loop(1) + }) +} + +/** + * Loads the skills in the given page. + * @param {number} page The page number to load + */ +function getSkillsPage(page) { + return axios.get(`${TC_API_URL}/v5/taas-teams/skills?perPage=${skillPageSize}&orderBy=name&page=${page}`) +} diff --git a/src/components/Feed/FeedComments.jsx b/src/components/Feed/FeedComments.jsx index 6cbf2a63d..28a71adec 100644 --- a/src/components/Feed/FeedComments.jsx +++ b/src/components/Feed/FeedComments.jsx @@ -176,6 +176,21 @@ class FeedComments extends React.Component { } } + convertMdToHtml(markdown) { + try { + return (
) + } catch (e) { + return ( +
+

{markdown}

+

+ This message could not be rendered properly, please contact Topcoder Support. +

+
+ ) + } + } + render() { const { currentUser, onLoadMoreComments, isLoadingComments, hasMoreComments, onAddNewComment, @@ -325,7 +340,7 @@ class FeedComments extends React.Component { canDelete={comments && (idx !== comments.length - 1)} // cannot delete the first post which is now shown as a last one commentAnchorPrefix={commentAnchorPrefix} > -
+ { this.convertMdToHtml(itemContent) } ) }) diff --git a/src/components/Feed/FeedComments.scss b/src/components/Feed/FeedComments.scss index e97eef98b..54d4e34ee 100644 --- a/src/components/Feed/FeedComments.scss +++ b/src/components/Feed/FeedComments.scss @@ -32,6 +32,14 @@ padding-bottom: 2 * $base-unit; } +.comment-render-error { + border: 1px dashed $tc-orange-70; + color: $tc-orange-70; + text-align: center; + padding: 2px; + margin-top: 5px; +} + .load-more { background-color: $tc-white; padding: 2 * $base-unit 0 $base-unit; diff --git a/src/config/constants.js b/src/config/constants.js index ed6ae46c9..490ac76f1 100644 --- a/src/config/constants.js +++ b/src/config/constants.js @@ -729,7 +729,7 @@ export const SEGMENT_KEY = process.env.CONNECT_SEGMENT_KEY */ export const DOMAIN = process.env.domain || 'topcoder.com' export const CONNECT_DOMAIN = `connect.${DOMAIN}` -export const CONNECT_MAIN_PAGE_URL = `http://connect.${DOMAIN}` +export const CONNECT_MAIN_PAGE_URL = `https://connect.${DOMAIN}` export const ACCOUNTS_APP_CONNECTOR_URL = process.env.ACCOUNTS_APP_CONNECTOR_URL export const ACCOUNTS_APP_LOGIN_URL = process.env.ACCOUNTS_APP_LOGIN_URL || `https://accounts-auth0.${DOMAIN}` export const ACCOUNTS_APP_REGISTER_URL = process.env.ACCOUNTS_APP_REGISTER_URL || `https://accounts-auth0.${DOMAIN}` @@ -1109,7 +1109,7 @@ export const PROJECT_TYPE_TALENT_AS_A_SERVICE = 'talent-as-a-service' /** * URL to the Topcoder TaaS App */ -export const TAAS_APP_URL = process.env.TAAS_APP_URL || 'https://mfe.topcoder-dev.com/taas' +export const TAAS_APP_URL = process.env.TAAS_APP_URL || 'https://platform.topcoder-dev.com/taas' /** * Milestone Types diff --git a/src/helpers/markdownToState.js b/src/helpers/markdownToState.js index 33606032f..ddf8180c9 100644 --- a/src/helpers/markdownToState.js +++ b/src/helpers/markdownToState.js @@ -1,6 +1,5 @@ import {convertFromRaw} from 'draft-js' import sanitizeHtml from 'sanitize-html' -import Alert from 'react-s-alert' const Remarkable = require('remarkable') // Block level items, key is Remarkable's key for them, value returned is @@ -363,13 +362,11 @@ function markdownToState(markdown, options = {}) { // If any error occurs set value to plain text const plainTextBlock = getNewBlock(BlockTypes['paragraph_open']()) plainTextBlock.text = markdown - + result = convertFromRaw({ entityMap: [], blocks: [plainTextBlock], }) - - Alert.warning('Some message could not be rendered properly, please contact Topcoder Support') } return result diff --git a/src/projects/create/components/ProjectSubmitted.jsx b/src/projects/create/components/ProjectSubmitted.jsx index 123f2d185..ccf6afcb8 100644 --- a/src/projects/create/components/ProjectSubmitted.jsx +++ b/src/projects/create/components/ProjectSubmitted.jsx @@ -1,37 +1,104 @@ import React from 'react' import PT from 'prop-types' +import qs from 'query-string' require('./ProjectSubmitted.scss') +import { + CONNECT_MAIN_PAGE_URL, PROJECT_TYPE_TALENT_AS_A_SERVICE, TAAS_APP_URL +} from '../../../config/constants' + class ProjectSubmitted extends React.Component { - constructor(props) { - super(props) - this.state = { - url: `projects/${props.params.status || props.projectId}` + /** + * Build project URL based on the `type` query param in URL. + * + * @param {boolean} isTaas + * @param {String} projectId project id + */ + getProjectUrl(isTaas, projectId = '') { + const url = isTaas + // if the project type is TaaS, then use link to TaaS App + ? `${TAAS_APP_URL}/myteams/${projectId}` + // otherwise use link inside Connect App + : `${CONNECT_MAIN_PAGE_URL}/projects/${projectId}` + + return url + } + + getPageConfiguration() { + const { type } = qs.parse(window.location.search) + const isTaas = type === PROJECT_TYPE_TALENT_AS_A_SERVICE + + const projectId = this.props.params.status || this.props.projectId + + const project = { + headerSubTitle: 'Your project has been created', + textBody: ( +
+ Topcoder will be contacting you soon to discuss your project proposal. +
+
+ +In the meantime, get a jump on the process by inviting your coworkers to your project and securely share any detailed requirements documents you have inside your project. + +
+ ), + leftButton: { + header: 'All Projects', + subText: 'View all of your projects', + url: this.getProjectUrl(isTaas) + }, + rightButton: { + header: 'Go to Project', + subText: 'Invite your team members and share requirements', + url: this.getProjectUrl(isTaas, projectId) + }, } + + const taas = { + headerSubTitle: 'Your talent request has been created', + textBody: ( +
+ Topcoder will be contacting you soon to discuss your talent needs. +
+ ), + leftButton: { + header: 'All Projects', + subText: 'View all of your projects', + url: this.getProjectUrl(false) // also showing link to Connect App Project List + }, + rightButton: { + header: 'View Talent Request', + subText: 'Modify your request and track fulfillment', + url: this.getProjectUrl(isTaas, projectId) + }, + } + + return isTaas? taas: project } render() { + + const { + headerSubTitle, + textBody, + leftButton, + rightButton + } = this.getPageConfiguration() + return (
Congratulations!
-
Your project has been created
-
- Topcoder will be contacting you soon to discuss your project proposal. -
-
- -In the meantime, get a jump on the process by inviting your coworkers to your project and securely share any detailed requirements documents you have inside your project. - -
+
{headerSubTitle}
+ {textBody}
- - Go to Project - Invite your team members and share requirements + + {leftButton.header} + {leftButton.subText} - - All Projects - View all of your projects + + {rightButton.header} + {rightButton.subText}
@@ -41,7 +108,6 @@ In the meantime, get a jump on the process by inviting your coworkers to your pr } ProjectSubmitted.defaultProps = { - vm: {}, params: {}, } diff --git a/src/projects/create/components/ProjectWizard.jsx b/src/projects/create/components/ProjectWizard.jsx index 0ca419fdc..b05edf01a 100644 --- a/src/projects/create/components/ProjectWizard.jsx +++ b/src/projects/create/components/ProjectWizard.jsx @@ -605,9 +605,6 @@ class ProjectWizard extends Component { />
diff --git a/src/projects/create/containers/CreateContainer.jsx b/src/projects/create/containers/CreateContainer.jsx index 0ee63d074..5e687c758 100644 --- a/src/projects/create/containers/CreateContainer.jsx +++ b/src/projects/create/containers/CreateContainer.jsx @@ -124,11 +124,12 @@ class CreateContainer extends React.Component { projectId: nextProjectId, isProjectDirty: false }, () => { + const type = _.get(this.state, 'updatedProject.type') // go to submitted state console.log('go to submitted state') window.localStorage.removeItem(LS_INCOMPLETE_PROJECT) window.localStorage.removeItem(LS_INCOMPLETE_WIZARD) - this.props.history.push('/new-project/submitted/' + nextProjectId) + this.props.history.push('/new-project/submitted/' + nextProjectId + (type ? `?type=${type}` : '')) }) } else if (this.state.creatingProject !== nextProps.processing) { diff --git a/src/projects/detail/components/Accordion/Accordion.jsx b/src/projects/detail/components/Accordion/Accordion.jsx index 3b64f6543..3fb3d9c2b 100644 --- a/src/projects/detail/components/Accordion/Accordion.jsx +++ b/src/projects/detail/components/Accordion/Accordion.jsx @@ -33,6 +33,7 @@ const TYPE = { SELECT_DROPDOWN: 'select-dropdown', TALENT_PICKER: 'talent-picker', TALENT_PICKER_V2: 'talent-picker-v2', + JOBS_PICKER: 'jobs-picker', } /** @@ -180,6 +181,9 @@ class Accordion extends React.Component { 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(', ') } + case TYPE.JOBS_PICKER: { + return _.map(value, 'title').join(', ') + } default: return value } } diff --git a/src/projects/detail/components/JobPickerRow/JobPickerRow.jsx b/src/projects/detail/components/JobPickerRow/JobPickerRow.jsx new file mode 100644 index 000000000..db9357a17 --- /dev/null +++ b/src/projects/detail/components/JobPickerRow/JobPickerRow.jsx @@ -0,0 +1,289 @@ +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 SelectDropdown from 'appirio-tech-react-components/components/SelectDropdown/SelectDropdown' + +import styles from './JobPickerRow.scss' + +const always = () => true +const never = () => false +const emptyError = () => '' + +const MAX_NUMBER = Math.pow(2, 31) - 1 + +class JobPickerRow 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.handleJobTitleChange = this.handleJobTitleChange.bind(this) + this.handleRoleChange = this.handleRoleChange.bind(this) + this.handleWorkloadChange = this.handleWorkloadChange.bind(this) + this.handleDescriptionChange = this.handleDescriptionChange.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'} + ] + + this.roleOptions = [ + { value: null, title: 'Select Role'}, + { value: 'designer', title: 'Designer'}, + { value: 'software-developer', title: 'Software Developer'}, + { value: 'data-scientist', title: 'Data Scientist'}, + { value: 'data-engineer', title: 'Data Engineer'}, + { value: 'qa', title: 'QA Tester'}, + { value: 'qa-engineer', title: 'QA Engineer'} + ] + } + handleJobTitleChange(evt) { + this.props.onChange(this.props.rowIndex, 'title', evt.target.value) + } + 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) + } + + handleRoleChange(evt) { + this.props.onChange(this.props.rowIndex, 'role', evt) + } + + handleDescriptionChange(evt) { + this.props.onChange(this.props.rowIndex, 'description', evt.target.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, rowIndex } = this.props + const isRowIncomplete = value.title.trim().length > 0 || value.people > 0 || value.duration > 0 || (value.skills && value.skills.length) + ||(value.role && value.role.value !== null) ||(value.workLoad && value.workLoad.value !== null) || (value.description.trim().length > 0) + + /* Different columns are defined here and used in componsing mobile/desktop views below */ + const titleColumn = ( +
+
+ + +
+
+ ) + + const actionsColumn = ( +
+
+
+ +
+ { rowIndex > 0 && ( +
+ +
+ )} +
+
+ ) + + 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 roleColumn = ( +
+ + +
+ ) + const descriptionColumn = ( +
+ +
+