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 ? ( + {selectedValue.label} + ) : ( + - {readOnly ? ( - {existingTags} - ) : ( + {readOnly ? ( + {existingTags} + ) : ( +