diff --git a/config/constants/development.js b/config/constants/development.js
index f4d30341..19d232bf 100644
--- a/config/constants/development.js
+++ b/config/constants/development.js
@@ -1,16 +1,19 @@
const DOMAIN = 'topcoder-dev.com'
const DEV_API_HOSTNAME = `https://api.${DOMAIN}`
+const API_V5 = `${DEV_API_HOSTNAME}/v5`
+
module.exports = {
API_V2: `${DEV_API_HOSTNAME}/v2`,
API_V3: `${DEV_API_HOSTNAME}/v3`,
API_V4: `${DEV_API_HOSTNAME}/v4`,
- API_V5: `${DEV_API_HOSTNAME}/v5`,
+ API_V5,
ACCOUNTS_APP_CONNECTOR_URL: `https://accounts-auth0.${DOMAIN}`,
ACCOUNTS_APP_LOGIN_URL: `https://accounts-auth0.${DOMAIN}`,
COMMUNITY_APP_URL: `https://www.${DOMAIN}`,
MEMBER_API_URL: `${DEV_API_HOSTNAME}/v5/members`,
CHALLENGE_API_URL: `${DEV_API_HOSTNAME}/v5/challenges`,
+ CHALLENGE_API_VERSION: '1.1.0',
CHALLENGE_TIMELINE_TEMPLATES_URL: `${DEV_API_HOSTNAME}/v5/timeline-templates`,
CHALLENGE_TYPES_URL: `${DEV_API_HOSTNAME}/v5/challenge-types`,
CHALLENGE_TRACKS_URL: `${DEV_API_HOSTNAME}/v5/challenge-tracks`,
@@ -22,8 +25,6 @@ module.exports = {
RESOURCES_API_URL: `${DEV_API_HOSTNAME}/v5/resources`,
RESOURCE_ROLES_API_URL: `${DEV_API_HOSTNAME}/v5/resource-roles`,
SUBMISSIONS_API_URL: `${DEV_API_HOSTNAME}/v5/submissions`,
- PLATFORMS_V4_API_URL: `${DEV_API_HOSTNAME}/v4/platforms`,
- TECHNOLOGIES_V4_API_URL: `${DEV_API_HOSTNAME}/v4/technologies`,
SUBMISSION_REVIEW_APP_URL: `https://submission-review.${DOMAIN}/challenges`,
STUDIO_URL: `https://studio.${DOMAIN}`,
CONNECT_APP_URL: `https://connect.${DOMAIN}`,
@@ -52,5 +53,8 @@ module.exports = {
MULTI_ROUND_CHALLENGE_TEMPLATE_ID: 'd4201ca4-8437-4d63-9957-3f7708184b07',
UNIVERSAL_NAV_URL: '//uni-nav.topcoder-dev.com/v1/tc-universal-nav.js',
HEADER_AUTH_URLS_HREF: `https://accounts-auth0.${DOMAIN}?utm_source=community-app-main`,
- HEADER_AUTH_URLS_LOCATION: `https://accounts-auth0.${DOMAIN}?retUrl=%S&utm_source=community-app-main`
+ HEADER_AUTH_URLS_LOCATION: `https://accounts-auth0.${DOMAIN}?retUrl=%S&utm_source=community-app-main`,
+ SKILLS_V5_API_URL: `${API_V5}/standardized-skills/skills/autocomplete`,
+ UPDATE_SKILLS_V5_API_URL: `${API_V5}/standardized-skills/work-skills`,
+ WORK_TYPE_ID: '4d2bdbc8-eb3b-4156-8d20-98a46589cc5d'
}
diff --git a/config/constants/production.js b/config/constants/production.js
index 29c73f3e..cff76956 100644
--- a/config/constants/production.js
+++ b/config/constants/production.js
@@ -1,16 +1,18 @@
const DOMAIN = 'topcoder.com'
const PROD_API_HOSTNAME = `https://api.${DOMAIN}`
+const API_V5 = `${PROD_API_HOSTNAME}/v5`
module.exports = {
API_V2: `${PROD_API_HOSTNAME}/v2`,
API_V3: `${PROD_API_HOSTNAME}/v3`,
API_V4: `${PROD_API_HOSTNAME}/v4`,
- API_V5: `${PROD_API_HOSTNAME}/v5`,
+ API_V5,
ACCOUNTS_APP_CONNECTOR_URL: process.env.ACCOUNTS_APP_CONNECTOR_URL || `https://accounts-auth0.${DOMAIN}`,
ACCOUNTS_APP_LOGIN_URL: `https://accounts-auth0.${DOMAIN}`,
COMMUNITY_APP_URL: `https://www.${DOMAIN}`,
MEMBER_API_URL: `${PROD_API_HOSTNAME}/v5/members`,
CHALLENGE_API_URL: `${PROD_API_HOSTNAME}/v5/challenges`,
+ CHALLENGE_API_VERSION: '1.1.0',
CHALLENGE_TIMELINE_TEMPLATES_URL: `${PROD_API_HOSTNAME}/v5/timeline-templates`,
CHALLENGE_TYPES_URL: `${PROD_API_HOSTNAME}/v5/challenge-types`,
CHALLENGE_TRACKS_URL: `${PROD_API_HOSTNAME}/v5/challenge-tracks`,
@@ -22,8 +24,6 @@ module.exports = {
RESOURCES_API_URL: `${PROD_API_HOSTNAME}/v5/resources`,
RESOURCE_ROLES_API_URL: `${PROD_API_HOSTNAME}/v5/resource-roles`,
SUBMISSIONS_API_URL: `${PROD_API_HOSTNAME}/v5/submissions`,
- PLATFORMS_V4_API_URL: `${PROD_API_HOSTNAME}/v4/platforms`,
- TECHNOLOGIES_V4_API_URL: `${PROD_API_HOSTNAME}/v4/technologies`,
SUBMISSION_REVIEW_APP_URL: `https://submission-review.${DOMAIN}/challenges`,
STUDIO_URL: `https://studio.${DOMAIN}`,
CONNECT_APP_URL: `https://connect.${DOMAIN}`,
@@ -50,5 +50,8 @@ module.exports = {
MULTI_ROUND_CHALLENGE_TEMPLATE_ID: 'd4201ca4-8437-4d63-9957-3f7708184b07',
UNIVERSAL_NAV_URL: '//uni-nav.topcoder.com/v1/tc-universal-nav.js',
HEADER_AUTH_URLS_HREF: `https://accounts-auth0.${DOMAIN}?utm_source=community-app-main`,
- HEADER_AUTH_URLS_LOCATION: `https://accounts-auth0.${DOMAIN}?retUrl=%S&utm_source=community-app-main`
+ HEADER_AUTH_URLS_LOCATION: `https://accounts-auth0.${DOMAIN}?retUrl=%S&utm_source=community-app-main`,
+ SKILLS_V5_API_URL: `${API_V5}/standardized-skills/skills/autocomplete`,
+ UPDATE_SKILLS_V5_API_URL: `${API_V5}/standardized-skills/work-skills`,
+ WORK_TYPE_ID: '4d2bdbc8-eb3b-4156-8d20-98a46589cc5d'
}
diff --git a/src/actions/challenges.js b/src/actions/challenges.js
index 73855f2e..ab019815 100644
--- a/src/actions/challenges.js
+++ b/src/actions/challenges.js
@@ -1,7 +1,6 @@
import _ from 'lodash'
import {
fetchChallengeTypes,
- fetchChallengeTags,
fetchGroups,
fetchTimelineTemplates,
fetchChallengePhases,
@@ -20,7 +19,8 @@ import {
deleteChallenge as deleteChallengeAPI,
createChallenge as createChallengeAPI,
createResource as createResourceAPI,
- deleteResource as deleteResourceAPI
+ deleteResource as deleteResourceAPI,
+ updateChallengeSkillsApi
} from '../services/challenges'
import { searchProfilesByUserIds } from '../services/user'
import {
@@ -52,7 +52,9 @@ import {
CHALLENGE_STATUS,
LOAD_CHALLENGE_RESOURCES_SUCCESS,
LOAD_CHALLENGE_RESOURCES_PENDING,
- LOAD_CHALLENGE_RESOURCES_FAILURE
+ LOAD_CHALLENGE_RESOURCES_FAILURE,
+ WORK_TYPE_ID,
+ UPDATE_CHALLENGES_SKILLS_SUCCESS
} from '../config/constants'
import { loadProject } from './projects'
import { removeChallengeFromPhaseProduct, saveChallengeAsPhaseProduct } from '../services/projects'
@@ -515,17 +517,6 @@ export function loadChallengeTimelines () {
}
}
-export function loadChallengeTags () {
- return async (dispatch) => {
- const challengeTags = await fetchChallengeTags()
- dispatch({
- type: LOAD_CHALLENGE_METADATA_SUCCESS,
- metadataKey: 'challengeTags',
- metadataValue: challengeTags
- })
- }
-}
-
export function loadGroups () {
return async (dispatch, getState) => {
const groups = await fetchGroups({
@@ -739,3 +730,29 @@ export function replaceResourceInRole (challengeId, roleId, newMember, oldMember
}
}
}
+
+/**
+ * Update Challenge skill
+ * @param {UUID} challengeId id of the challenge for which resource is to be replaced
+ * @param {Array} skills array of skill
+ */
+export function updateChallengeSkills (challengeId, skills) {
+ return async (dispatch) => {
+ try {
+ if (!skills) {
+ return
+ }
+ await updateChallengeSkillsApi({
+ workId: challengeId,
+ workTypeId: WORK_TYPE_ID,
+ skillIds: skills.map(skill => skill.id)
+ })
+ dispatch({
+ type: UPDATE_CHALLENGES_SKILLS_SUCCESS,
+ payload: skills
+ })
+ } catch (error) {
+ return _.get(error, 'response.data.message', 'Can not save skill')
+ }
+ }
+}
diff --git a/src/components/ChallengeEditor/ChallengeView/index.js b/src/components/ChallengeEditor/ChallengeView/index.js
index c7bfbb95..667966ae 100644
--- a/src/components/ChallengeEditor/ChallengeView/index.js
+++ b/src/components/ChallengeEditor/ChallengeView/index.js
@@ -244,7 +244,6 @@ const ChallengeView = ({
Public specification *
diff --git a/src/components/ChallengeEditor/SkillsField/index.js b/src/components/ChallengeEditor/SkillsField/index.js
new file mode 100644
index 00000000..8025519a
--- /dev/null
+++ b/src/components/ChallengeEditor/SkillsField/index.js
@@ -0,0 +1,82 @@
+import React, { useMemo } from 'react'
+import PropTypes from 'prop-types'
+import Select from '../../Select'
+import { searchSkills } from '../../../services/skills'
+import cn from 'classnames'
+import styles from './styles.module.scss'
+import { AUTOCOMPLETE_DEBOUNCE_TIME_MS } from '../../../config/constants'
+import _ from 'lodash'
+
+const fetchSkills = _.debounce((inputValue, callback) => {
+ searchSkills(inputValue).then(
+ (skills) => {
+ const suggestedOptions = skills.map((skillItem) => ({
+ label: skillItem.name,
+ value: skillItem.id
+ }))
+ return callback(suggestedOptions)
+ })
+ .catch(() => {
+ return callback(null)
+ })
+}, AUTOCOMPLETE_DEBOUNCE_TIME_MS)
+
+const SkillsField = ({ readOnly, challenge, onUpdateSkills }) => {
+ const selectedSkills = useMemo(() => (challenge.skills || []).map(skill => ({
+ label: skill.name,
+ value: skill.id
+ })), [challenge.skills])
+ const existingSkills = useMemo(() => selectedSkills.map(item => item.label).join(','), [selectedSkills])
+
+ return (
+ <>
+
+
+
+
+
+
+ {readOnly ? (
+ {existingSkills}
+ ) : (
+
+
+
+ { !readOnly && challenge.submitTriggered && (!selectedSkills || !selectedSkills.length) &&
+
+
+ Select at least one skill
+
+
}
+ >
+ )
+}
+
+SkillsField.defaultProps = {
+ readOnly: false,
+ onUpdateSkills: () => { }
+}
+
+SkillsField.propTypes = {
+ readOnly: PropTypes.bool,
+ challenge: PropTypes.shape().isRequired,
+ onUpdateSkills: PropTypes.func
+}
+
+export default SkillsField
diff --git a/src/components/ChallengeEditor/TagsField/Tags-Field.module.scss b/src/components/ChallengeEditor/SkillsField/styles.module.scss
similarity index 100%
rename from src/components/ChallengeEditor/TagsField/Tags-Field.module.scss
rename to src/components/ChallengeEditor/SkillsField/styles.module.scss
diff --git a/src/components/ChallengeEditor/SpecialChallengeField/index.js b/src/components/ChallengeEditor/SpecialChallengeField/index.js
new file mode 100644
index 00000000..246ed4c3
--- /dev/null
+++ b/src/components/ChallengeEditor/SpecialChallengeField/index.js
@@ -0,0 +1,71 @@
+import React, { useMemo } from 'react'
+import PropTypes from 'prop-types'
+import Select from '../../Select'
+import cn from 'classnames'
+import styles from './styles.module.scss'
+import { SPECIAL_CHALLENGE_TAGS } from '../../../config/constants'
+import _ from 'lodash'
+
+const options = [
+ {
+ label: 'No',
+ value: ''
+ },
+ ...SPECIAL_CHALLENGE_TAGS.map(tag => ({
+ label: tag,
+ value: tag
+ }))
+]
+
+const SpecialChallengeField = ({ challenge, onUpdateMultiSelect, readOnly }) => {
+ const selectedValue = useMemo(() => {
+ const selectedTag = _.filter(challenge.tags, (tag) => SPECIAL_CHALLENGE_TAGS.indexOf(tag) >= 0)[0]
+ return _.find(options, {
+ value: selectedTag || ''
+ })
+ }, [challenge.tags])
+ return (
+ <>
+
+
+
+
+
+
+ {readOnly ? (
+ {selectedValue.label}
+ ) : (
+
+
+ >
+ )
+}
+
+SpecialChallengeField.defaultProps = {
+ readOnly: false
+}
+
+SpecialChallengeField.propTypes = {
+ challenge: PropTypes.shape().isRequired,
+ onUpdateMultiSelect: PropTypes.func.isRequired,
+ readOnly: PropTypes.bool
+}
+
+export default SpecialChallengeField
diff --git a/src/components/ChallengeEditor/SpecialChallengeField/styles.module.scss b/src/components/ChallengeEditor/SpecialChallengeField/styles.module.scss
new file mode 100644
index 00000000..877b75fc
--- /dev/null
+++ b/src/components/ChallengeEditor/SpecialChallengeField/styles.module.scss
@@ -0,0 +1,52 @@
+@import "../../../styles/includes";
+
+.row {
+ box-sizing: border-box;
+ display: flex;
+ flex-direction: row;
+ margin: 30px 30px 0 30px;
+ align-content: space-between;
+ justify-content: flex-start;
+
+ .field {
+ @include upto-sm {
+ display: block;
+ padding-bottom: 10px;
+ }
+
+ label {
+ @include roboto-bold();
+
+ font-size: 16px;
+ line-height: 19px;
+ font-weight: 500;
+ color: $tc-gray-80;
+ }
+
+ &.col1 {
+ max-width: 185px;
+ min-width: 185px;
+ margin-right: 14px;
+ white-space: nowrap;
+ display: flex;
+ align-items: center;
+
+ span {
+ color: $tc-red;
+ }
+ }
+
+ &.col2.error {
+ color: $tc-red;
+ margin-top: -25px;
+ }
+ &.col2 {
+ align-self: flex-end;
+ margin-bottom: auto;
+ margin-top: auto;
+ display: flex;
+ flex-direction: row;
+ width: 600px;
+ }
+ }
+}
diff --git a/src/components/ChallengeEditor/TagsField/index.js b/src/components/ChallengeEditor/TagsField/index.js
index 7dcdfb88..b1ffaf9c 100644
--- a/src/components/ChallengeEditor/TagsField/index.js
+++ b/src/components/ChallengeEditor/TagsField/index.js
@@ -1,51 +1,63 @@
-import React from 'react'
+import React, { useMemo } from 'react'
import PropTypes from 'prop-types'
import Select from '../../Select'
import cn from 'classnames'
-import styles from './Tags-Field.module.scss'
+import styles from './styles.module.scss'
+import { SPECIAL_CHALLENGE_TAGS } from '../../../config/constants'
+import _ from 'lodash'
+
+const TagsField = ({ challenge, onUpdateMultiSelect, readOnly }) => {
+ const selectedTags = useMemo(() => {
+ return (challenge.tags || []).map(
+ tag => ({ label: tag, value: tag })
+ )
+ }, [challenge.tags])
+
+ const selectedValues = useMemo(() => {
+ return _.filter(selectedTags, (tag) => SPECIAL_CHALLENGE_TAGS.indexOf(tag.value) < 0)
+ }, [selectedTags])
+
+ const existingTags = useMemo(() => {
+ return selectedValues.length ? selectedValues.map(item => item.value).join(',') : ''
+ }, [selectedValues])
+
+ const selectedSpecialChallengeValues = useMemo(() => {
+ return _.filter(selectedTags, (tag) => SPECIAL_CHALLENGE_TAGS.indexOf(tag.value) >= 0)
+ }, [challenge.tags])
-const TagsField = ({ challengeTags, challenge, onUpdateMultiSelect, readOnly }) => {
- const mapOps = item => ({ label: item.name, value: item.id })
- const existingTags = (challenge.tags && challenge.tags.length) ? challenge.tags.join(',') : ''
return (
- <>
-
-
-
-
-
-
- {readOnly ? (
-
{existingTags}
- ) : (
-
- { !readOnly && challenge.submitTriggered && (!challenge.tags || !challenge.tags.length) &&
-
-
- Select at least one tag
-
-
}
- >
+
)
}
TagsField.defaultProps = {
- challengeTags: [],
readOnly: false
}
TagsField.propTypes = {
challenge: PropTypes.shape().isRequired,
- challengeTags: PropTypes.arrayOf(PropTypes.object).isRequired,
onUpdateMultiSelect: PropTypes.func.isRequired,
readOnly: PropTypes.bool
}
diff --git a/src/components/ChallengeEditor/TagsField/styles.module.scss b/src/components/ChallengeEditor/TagsField/styles.module.scss
new file mode 100644
index 00000000..877b75fc
--- /dev/null
+++ b/src/components/ChallengeEditor/TagsField/styles.module.scss
@@ -0,0 +1,52 @@
+@import "../../../styles/includes";
+
+.row {
+ box-sizing: border-box;
+ display: flex;
+ flex-direction: row;
+ margin: 30px 30px 0 30px;
+ align-content: space-between;
+ justify-content: flex-start;
+
+ .field {
+ @include upto-sm {
+ display: block;
+ padding-bottom: 10px;
+ }
+
+ label {
+ @include roboto-bold();
+
+ font-size: 16px;
+ line-height: 19px;
+ font-weight: 500;
+ color: $tc-gray-80;
+ }
+
+ &.col1 {
+ max-width: 185px;
+ min-width: 185px;
+ margin-right: 14px;
+ white-space: nowrap;
+ display: flex;
+ align-items: center;
+
+ span {
+ color: $tc-red;
+ }
+ }
+
+ &.col2.error {
+ color: $tc-red;
+ margin-top: -25px;
+ }
+ &.col2 {
+ align-self: flex-end;
+ margin-bottom: auto;
+ margin-top: auto;
+ display: flex;
+ flex-direction: row;
+ width: 600px;
+ }
+ }
+}
diff --git a/src/components/ChallengeEditor/TextEditor-Field/index.js b/src/components/ChallengeEditor/TextEditor-Field/index.js
index bd3f8b40..6a90a139 100644
--- a/src/components/ChallengeEditor/TextEditor-Field/index.js
+++ b/src/components/ChallengeEditor/TextEditor-Field/index.js
@@ -1,5 +1,7 @@
import React, { Component } from 'react'
+import SpecialChallengeField from '../SpecialChallengeField'
import TagsField from '../TagsField'
+import SkillsField from '../SkillsField'
import FinalDeliverablesField from '../FinalDeliverables-Field'
import StockArtsField from '../StockArts-Field'
import SubmssionVisibility from '../SubmissionVisibility-Field'
@@ -25,21 +27,18 @@ class TextEditorField extends Component {
render () {
const {
- challengeTags,
challenge,
onUpdateCheckbox,
addFileType,
removeFileType,
onUpdateDescription,
+ onUpdateSkills,
onUpdateMultiSelect,
shouldShowPrivateDescription,
onUpdateMetadata,
readOnly
} = this.props
const { addedNewPrivateDescription } = this.state
- const challengeTagsFiltered = challengeTags.map(function (tag) {
- return { id: tag.name, name: tag.name }
- })
const showShowPrivateDescriptionField = addedNewPrivateDescription || (challenge.privateDescription !== null && challenge.privateDescription !== undefined)
return (
@@ -79,12 +78,21 @@ class TextEditorField extends Component {
/>
)}
+
+
{challenge.trackId === CHALLENGE_TRACKS.DESIGN && (
{},
onUpdateCheckbox: () => {},
addFileType: () => {},
onUpdateDescription: () => {},
+ onUpdateSkills: () => {},
onUpdateMultiSelect: () => {},
readOnly: false
}
TextEditorField.propTypes = {
- challengeTags: PropTypes.arrayOf(PropTypes.object).isRequired,
challenge: PropTypes.shape().isRequired,
onUpdateCheckbox: PropTypes.func,
addFileType: PropTypes.func,
removeFileType: PropTypes.func,
onUpdateMetadata: PropTypes.func,
onUpdateDescription: PropTypes.func,
+ onUpdateSkills: PropTypes.func,
onUpdateMultiSelect: PropTypes.func,
shouldShowPrivateDescription: PropTypes.bool,
readOnly: PropTypes.bool
diff --git a/src/components/ChallengeEditor/index.js b/src/components/ChallengeEditor/index.js
index 0bf42dfa..70eeb754 100644
--- a/src/components/ChallengeEditor/index.js
+++ b/src/components/ChallengeEditor/index.js
@@ -133,6 +133,7 @@ class ChallengeEditor extends Component {
this.onUpdatePhase = this.onUpdatePhase.bind(this)
this.resetChallengeData = this.resetChallengeData.bind(this)
this.onUpdateDescription = this.onUpdateDescription.bind(this)
+ this.onUpdateSkills = this.onUpdateSkills.bind(this)
this.onActiveChallenge = this.onActiveChallenge.bind(this)
this.resetModal = this.resetModal.bind(this)
this.resetDesignChallengeWarningModal = this.resetDesignChallengeWarningModal.bind(this)
@@ -152,6 +153,7 @@ class ChallengeEditor extends Component {
this.getAvailableTimelineTemplates = this.getAvailableTimelineTemplates.bind(this)
this.autoUpdateChallengeThrottled = _.throttle(this.validateAndAutoUpdateChallenge.bind(this), 3000) // 3s
this.updateResource = this.updateResource.bind(this)
+ this.updateSkills = this.updateSkills.bind(this)
this.onDeleteChallenge = this.onDeleteChallenge.bind(this)
this.deleteModalLaunch = this.deleteModalLaunch.bind(this)
this.toggleForumOnCreate = this.toggleForumOnCreate.bind(this)
@@ -329,6 +331,14 @@ class ChallengeEditor extends Component {
})
}
+ onUpdateSkills (skills) {
+ const { challenge: oldChallenge } = this.state
+ const newChallenge = { ...oldChallenge, skills }
+ this.setState({ challenge: newChallenge }, () => {
+ this.autoUpdateChallengeThrottled('skills')
+ })
+ }
+
/**
* Update Input value of challenge
* @param e The input event
@@ -784,7 +794,7 @@ class ChallengeEditor extends Component {
'typeId',
'name',
'description',
- 'tags',
+ 'skills',
'prizeSets'
]
let isRequiredMissing = false
@@ -934,7 +944,8 @@ class ChallengeEditor extends Component {
'winners',
'milestoneId',
'discussions',
- 'task'
+ 'task',
+ 'skills'
], this.state.challenge)
const isTask = _.find(metadata.challengeTypes, { id: challenge.typeId, isTask: true })
challenge.legacy = _.assign(this.state.challenge.legacy, {
@@ -1137,6 +1148,8 @@ class ChallengeEditor extends Component {
break
}
}
+ } else if (changedField === 'skills') {
+ await this.updateSkills(challengeId, this.state.challenge.skills)
} else {
let patchObject = (changedField === 'reviewType')
? { legacy: { reviewType: this.state.challenge[changedField] } } // NOTE it assumes challenge API PATCH respects the changes in legacy JSON
@@ -1200,7 +1213,7 @@ class ChallengeEditor extends Component {
}
async updateAllChallengeInfo (status, cb = () => { }) {
- const { updateChallengeDetails, assignedMemberDetails: oldAssignedMember, projectDetail } = this.props
+ const { updateChallengeDetails, assignedMemberDetails: oldAssignedMember, projectDetail, challengeDetails } = this.props
if (this.state.isSaving) return
this.setState({ isSaving: true }, async () => {
const challenge = this.collectChallengeData(status)
@@ -1217,6 +1230,9 @@ class ChallengeEditor extends Component {
if (assignedMemberHandle !== oldMemberHandle) {
await this.updateResource(challengeId, 'Submitter', assignedMemberHandle, oldMemberHandle)
}
+ if (!_.isEqual(challengeDetails.skills, newChallenge.skills)) {
+ await this.updateSkills(challengeId, newChallenge.skills)
+ }
const { copilot: previousCopilot, reviewer: previousReviewer } = this.state.draftChallenge.data
if (copilot !== previousCopilot) await this.updateResource(challengeId, 'Copilot', copilot, previousCopilot)
if (type === 'First2Finish' || type === 'Task') {
@@ -1291,6 +1307,10 @@ class ChallengeEditor extends Component {
await this.props.replaceResourceInRole(challengeId, roleId, value, prevValue)
}
+ async updateSkills (challengeId, skills) {
+ return this.props.updateChallengeSkills(challengeId, skills)
+ }
+
updateAttachmentlist (challenge, attachments) {
const newChallenge = _.cloneDeep(challenge)
if (attachments.length > 0) {
@@ -1556,16 +1576,15 @@ class ChallengeEditor extends Component {
{!isLoading &&
}
{!isLoading && (!isActive) && (!isCompleted) &&
- {/*
-
-
*/}
-
- {!this.state.hasValidationErrors ? (
-
- ) : (
-
- )}
-
+ {(!preventCopilotFromActivatingTask) && (
+
+ {!this.state.hasValidationErrors ? (
+
+ ) : (
+
+ )}
+
+ )}
{
(
isDraft &&
@@ -1589,7 +1608,7 @@ class ChallengeEditor extends Component {
}
}
- {!isLoading && isActive &&
+ {(!isLoading) && isActive && (!preventCopilotFromActivatingTask) &&
@@ -1826,13 +1845,13 @@ class ChallengeEditor extends Component {
Access specification templates here
@@ -1916,6 +1935,7 @@ ChallengeEditor.propTypes = {
updateChallengeDetails: PropTypes.func.isRequired,
createChallenge: PropTypes.func,
replaceResourceInRole: PropTypes.func,
+ updateChallengeSkills: PropTypes.func,
partiallyUpdateChallengeDetails: PropTypes.func.isRequired,
deleteChallenge: PropTypes.func.isRequired,
loggedInUser: PropTypes.shape().isRequired,
diff --git a/src/components/Select/index.js b/src/components/Select/index.js
index a121be62..97a8ac36 100644
--- a/src/components/Select/index.js
+++ b/src/components/Select/index.js
@@ -1,11 +1,31 @@
import React from 'react'
import _ from 'lodash'
import ReactSelect from 'react-select'
+import CreatableSelect from 'react-select/creatable'
+import AsyncSelect from 'react-select/async'
import PT from 'prop-types'
import styles from './styles'
export default function Select (props) {
- const { selectRef } = props
+ const { selectRef, isCreatable, isAsync } = props
+
+ if (isAsync) {
+ return (
)
+ }
+ if (isCreatable) {
+ return (
)
+ }
+
return (
}
- */
-export async function fetchChallengeTags () {
- const platforms = await axiosInstance.get(PLATFORMS_V4_API_URL)
- const technologies = await axiosInstance.get(TECHNOLOGIES_V4_API_URL)
- return [
- ..._.map(_.get(platforms, 'data.result.content', []), tag => _.pick(tag, 'name')),
- ..._.map(_.get(technologies, 'data.result.content', []), tag => _.pick(tag, 'name'))
- ]
-}
-
/**
* Api request for fetching Groups
*
@@ -112,7 +98,11 @@ export async function fetchChallengePhases () {
* @returns {Promise<*>}
*/
export async function fetchChallenge (challengeId) {
- const response = await axiosInstance.get(`${CHALLENGE_API_URL}/${challengeId}`)
+ const response = await axiosInstance.get(`${CHALLENGE_API_URL}/${challengeId}`, {
+ headers: {
+ 'app-version': CHALLENGE_API_VERSION
+ }
+ })
return normalizeChallengeDataFromAPI(_.get(response, 'data'))
}
@@ -122,7 +112,11 @@ export async function fetchChallenge (challengeId) {
* @returns {Promise<*>} challenge data
*/
export function createChallenge (challenge) {
- return axiosInstance.post(CHALLENGE_API_URL, updateChallengePhaseBeforeSendRequest(challenge)).then(response => {
+ return axiosInstance.post(CHALLENGE_API_URL, updateChallengePhaseBeforeSendRequest(challenge), {
+ headers: {
+ 'app-version': CHALLENGE_API_VERSION
+ }
+ }).then(response => {
return normalizeChallengeDataFromAPI(_.get(response, 'data'))
})
}
@@ -134,7 +128,11 @@ export function createChallenge (challenge) {
* @returns {Promise<*>} challenge data
*/
export function updateChallenge (challengeId, challenge) {
- return axiosInstance.put(`${CHALLENGE_API_URL}/${challengeId}`, updateChallengePhaseBeforeSendRequest(challenge)).then(response => {
+ return axiosInstance.put(`${CHALLENGE_API_URL}/${challengeId}`, updateChallengePhaseBeforeSendRequest(challenge), {
+ headers: {
+ 'app-version': CHALLENGE_API_VERSION
+ }
+ }).then(response => {
return normalizeChallengeDataFromAPI(_.get(response, 'data'))
})
}
@@ -148,7 +146,11 @@ export function updateChallenge (challengeId, challenge) {
* @returns {Promise<*>} attachments data
*/
export function createAttachments (challengeId, attachments) {
- return axiosInstance.post(`${CHALLENGE_API_URL}/${challengeId}/attachments`, attachments)
+ return axiosInstance.post(`${CHALLENGE_API_URL}/${challengeId}/attachments`, attachments, {
+ headers: {
+ 'app-version': CHALLENGE_API_VERSION
+ }
+ })
}
/**
@@ -160,7 +162,11 @@ export function createAttachments (challengeId, attachments) {
* @returns {Promise}
*/
export function removeAttachment (challengeId, attachmentId) {
- return axiosInstance.delete(`${CHALLENGE_API_URL}/${challengeId}/attachments/${attachmentId}`)
+ return axiosInstance.delete(`${CHALLENGE_API_URL}/${challengeId}/attachments/${attachmentId}`, {
+ headers: {
+ 'app-version': CHALLENGE_API_VERSION
+ }
+ })
}
/**
@@ -173,7 +179,11 @@ export function fetchChallenges (filters, params) {
...filters,
...params
}
- return axiosInstance.get(`${CHALLENGE_API_URL}?${qs.stringify(query, { encode: false })}`).then(response => {
+ return axiosInstance.get(`${CHALLENGE_API_URL}?${qs.stringify(query, { encode: false })}`, {
+ headers: {
+ 'app-version': CHALLENGE_API_VERSION
+ }
+ }).then(response => {
// normalize challenge data in the list of challenges for consistency with data of a single challenge details page
response.data = response.data.map(normalizeChallengeDataFromAPI)
return response
@@ -186,7 +196,11 @@ export function fetchChallenges (filters, params) {
* @param params
*/
export function patchChallenge (challengeId, params) {
- return axiosInstance.patch(`${CHALLENGE_API_URL}/${challengeId}`, updateChallengePhaseBeforeSendRequest(params)).then(rs => {
+ return axiosInstance.patch(`${CHALLENGE_API_URL}/${challengeId}`, updateChallengePhaseBeforeSendRequest(params), {
+ headers: {
+ 'app-version': CHALLENGE_API_VERSION
+ }
+ }).then(rs => {
return normalizeChallengeDataFromAPI(_.get(rs, 'data'))
})
}
@@ -196,7 +210,11 @@ export function patchChallenge (challengeId, params) {
* @param challengeId
*/
export function deleteChallenge (challengeId) {
- return axiosInstance.delete(`${CHALLENGE_API_URL}/${challengeId}`)
+ return axiosInstance.delete(`${CHALLENGE_API_URL}/${challengeId}`, {
+ headers: {
+ 'app-version': CHALLENGE_API_VERSION
+ }
+ })
}
/**
@@ -267,3 +285,13 @@ export async function deleteResource (resource) {
const resp = await axiosInstance.delete(RESOURCES_API_URL, { data: resource })
return _.get(resp, 'data', {})
}
+
+/**
+ * Api request for updating challenge skill
+ * @param {Object} skills data
+ * @returns {Promise<*>}
+ */
+export async function updateChallengeSkillsApi (skills) {
+ const resp = await axiosInstance.post(UPDATE_SKILLS_V5_API_URL, skills)
+ return _.get(resp, 'data', {})
+}
diff --git a/src/services/skills.js b/src/services/skills.js
new file mode 100644
index 00000000..261fd62c
--- /dev/null
+++ b/src/services/skills.js
@@ -0,0 +1,20 @@
+import _ from 'lodash'
+import {
+ SKILLS_V5_API_URL
+} from '../config/constants'
+import { axiosInstance } from './axiosWithAuth'
+import * as queryString from 'query-string'
+
+/**
+ * Api request for fetching skills
+ *
+ * @param {String} term search key
+ *
+ * @returns {Promise<*>}
+ */
+export async function searchSkills (term) {
+ const skills = await axiosInstance.get(`${SKILLS_V5_API_URL}?${queryString.stringify({
+ term
+ })}`)
+ return _.get(skills, 'data', [])
+}