Skip to content

Commit f9cf766

Browse files
authored
Merge pull request #1577 from topcoder-platform/develop
Standardised skills and minor fixes
2 parents a372512 + 278ed99 commit f9cf766

File tree

18 files changed

+525
-109
lines changed

18 files changed

+525
-109
lines changed

config/constants/development.js

+8-4
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
const DOMAIN = 'topcoder-dev.com'
22
const DEV_API_HOSTNAME = `https://api.${DOMAIN}`
33

4+
const API_V5 = `${DEV_API_HOSTNAME}/v5`
5+
46
module.exports = {
57
API_V2: `${DEV_API_HOSTNAME}/v2`,
68
API_V3: `${DEV_API_HOSTNAME}/v3`,
79
API_V4: `${DEV_API_HOSTNAME}/v4`,
8-
API_V5: `${DEV_API_HOSTNAME}/v5`,
10+
API_V5,
911
ACCOUNTS_APP_CONNECTOR_URL: `https://accounts-auth0.${DOMAIN}`,
1012
ACCOUNTS_APP_LOGIN_URL: `https://accounts-auth0.${DOMAIN}`,
1113
COMMUNITY_APP_URL: `https://www.${DOMAIN}`,
1214
MEMBER_API_URL: `${DEV_API_HOSTNAME}/v5/members`,
1315
CHALLENGE_API_URL: `${DEV_API_HOSTNAME}/v5/challenges`,
16+
CHALLENGE_API_VERSION: '1.1.0',
1417
CHALLENGE_TIMELINE_TEMPLATES_URL: `${DEV_API_HOSTNAME}/v5/timeline-templates`,
1518
CHALLENGE_TYPES_URL: `${DEV_API_HOSTNAME}/v5/challenge-types`,
1619
CHALLENGE_TRACKS_URL: `${DEV_API_HOSTNAME}/v5/challenge-tracks`,
@@ -22,8 +25,6 @@ module.exports = {
2225
RESOURCES_API_URL: `${DEV_API_HOSTNAME}/v5/resources`,
2326
RESOURCE_ROLES_API_URL: `${DEV_API_HOSTNAME}/v5/resource-roles`,
2427
SUBMISSIONS_API_URL: `${DEV_API_HOSTNAME}/v5/submissions`,
25-
PLATFORMS_V4_API_URL: `${DEV_API_HOSTNAME}/v4/platforms`,
26-
TECHNOLOGIES_V4_API_URL: `${DEV_API_HOSTNAME}/v4/technologies`,
2728
SUBMISSION_REVIEW_APP_URL: `https://submission-review.${DOMAIN}/challenges`,
2829
STUDIO_URL: `https://studio.${DOMAIN}`,
2930
CONNECT_APP_URL: `https://connect.${DOMAIN}`,
@@ -52,5 +53,8 @@ module.exports = {
5253
MULTI_ROUND_CHALLENGE_TEMPLATE_ID: 'd4201ca4-8437-4d63-9957-3f7708184b07',
5354
UNIVERSAL_NAV_URL: '//uni-nav.topcoder-dev.com/v1/tc-universal-nav.js',
5455
HEADER_AUTH_URLS_HREF: `https://accounts-auth0.${DOMAIN}?utm_source=community-app-main`,
55-
HEADER_AUTH_URLS_LOCATION: `https://accounts-auth0.${DOMAIN}?retUrl=%S&utm_source=community-app-main`
56+
HEADER_AUTH_URLS_LOCATION: `https://accounts-auth0.${DOMAIN}?retUrl=%S&utm_source=community-app-main`,
57+
SKILLS_V5_API_URL: `${API_V5}/standardized-skills/skills/autocomplete`,
58+
UPDATE_SKILLS_V5_API_URL: `${API_V5}/standardized-skills/work-skills`,
59+
WORK_TYPE_ID: '4d2bdbc8-eb3b-4156-8d20-98a46589cc5d'
5660
}

config/constants/production.js

+7-4
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
const DOMAIN = 'topcoder.com'
22
const PROD_API_HOSTNAME = `https://api.${DOMAIN}`
3+
const API_V5 = `${PROD_API_HOSTNAME}/v5`
34

45
module.exports = {
56
API_V2: `${PROD_API_HOSTNAME}/v2`,
67
API_V3: `${PROD_API_HOSTNAME}/v3`,
78
API_V4: `${PROD_API_HOSTNAME}/v4`,
8-
API_V5: `${PROD_API_HOSTNAME}/v5`,
9+
API_V5,
910
ACCOUNTS_APP_CONNECTOR_URL: process.env.ACCOUNTS_APP_CONNECTOR_URL || `https://accounts-auth0.${DOMAIN}`,
1011
ACCOUNTS_APP_LOGIN_URL: `https://accounts-auth0.${DOMAIN}`,
1112
COMMUNITY_APP_URL: `https://www.${DOMAIN}`,
1213
MEMBER_API_URL: `${PROD_API_HOSTNAME}/v5/members`,
1314
CHALLENGE_API_URL: `${PROD_API_HOSTNAME}/v5/challenges`,
15+
CHALLENGE_API_VERSION: '1.1.0',
1416
CHALLENGE_TIMELINE_TEMPLATES_URL: `${PROD_API_HOSTNAME}/v5/timeline-templates`,
1517
CHALLENGE_TYPES_URL: `${PROD_API_HOSTNAME}/v5/challenge-types`,
1618
CHALLENGE_TRACKS_URL: `${PROD_API_HOSTNAME}/v5/challenge-tracks`,
@@ -22,8 +24,6 @@ module.exports = {
2224
RESOURCES_API_URL: `${PROD_API_HOSTNAME}/v5/resources`,
2325
RESOURCE_ROLES_API_URL: `${PROD_API_HOSTNAME}/v5/resource-roles`,
2426
SUBMISSIONS_API_URL: `${PROD_API_HOSTNAME}/v5/submissions`,
25-
PLATFORMS_V4_API_URL: `${PROD_API_HOSTNAME}/v4/platforms`,
26-
TECHNOLOGIES_V4_API_URL: `${PROD_API_HOSTNAME}/v4/technologies`,
2727
SUBMISSION_REVIEW_APP_URL: `https://submission-review.${DOMAIN}/challenges`,
2828
STUDIO_URL: `https://studio.${DOMAIN}`,
2929
CONNECT_APP_URL: `https://connect.${DOMAIN}`,
@@ -50,5 +50,8 @@ module.exports = {
5050
MULTI_ROUND_CHALLENGE_TEMPLATE_ID: 'd4201ca4-8437-4d63-9957-3f7708184b07',
5151
UNIVERSAL_NAV_URL: '//uni-nav.topcoder.com/v1/tc-universal-nav.js',
5252
HEADER_AUTH_URLS_HREF: `https://accounts-auth0.${DOMAIN}?utm_source=community-app-main`,
53-
HEADER_AUTH_URLS_LOCATION: `https://accounts-auth0.${DOMAIN}?retUrl=%S&utm_source=community-app-main`
53+
HEADER_AUTH_URLS_LOCATION: `https://accounts-auth0.${DOMAIN}?retUrl=%S&utm_source=community-app-main`,
54+
SKILLS_V5_API_URL: `${API_V5}/standardized-skills/skills/autocomplete`,
55+
UPDATE_SKILLS_V5_API_URL: `${API_V5}/standardized-skills/work-skills`,
56+
WORK_TYPE_ID: '4d2bdbc8-eb3b-4156-8d20-98a46589cc5d'
5457
}

src/actions/challenges.js

+31-14
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import _ from 'lodash'
22
import {
33
fetchChallengeTypes,
4-
fetchChallengeTags,
54
fetchGroups,
65
fetchTimelineTemplates,
76
fetchChallengePhases,
@@ -20,7 +19,8 @@ import {
2019
deleteChallenge as deleteChallengeAPI,
2120
createChallenge as createChallengeAPI,
2221
createResource as createResourceAPI,
23-
deleteResource as deleteResourceAPI
22+
deleteResource as deleteResourceAPI,
23+
updateChallengeSkillsApi
2424
} from '../services/challenges'
2525
import { searchProfilesByUserIds } from '../services/user'
2626
import {
@@ -52,7 +52,9 @@ import {
5252
CHALLENGE_STATUS,
5353
LOAD_CHALLENGE_RESOURCES_SUCCESS,
5454
LOAD_CHALLENGE_RESOURCES_PENDING,
55-
LOAD_CHALLENGE_RESOURCES_FAILURE
55+
LOAD_CHALLENGE_RESOURCES_FAILURE,
56+
WORK_TYPE_ID,
57+
UPDATE_CHALLENGES_SKILLS_SUCCESS
5658
} from '../config/constants'
5759
import { loadProject } from './projects'
5860
import { removeChallengeFromPhaseProduct, saveChallengeAsPhaseProduct } from '../services/projects'
@@ -515,17 +517,6 @@ export function loadChallengeTimelines () {
515517
}
516518
}
517519

518-
export function loadChallengeTags () {
519-
return async (dispatch) => {
520-
const challengeTags = await fetchChallengeTags()
521-
dispatch({
522-
type: LOAD_CHALLENGE_METADATA_SUCCESS,
523-
metadataKey: 'challengeTags',
524-
metadataValue: challengeTags
525-
})
526-
}
527-
}
528-
529520
export function loadGroups () {
530521
return async (dispatch, getState) => {
531522
const groups = await fetchGroups({
@@ -739,3 +730,29 @@ export function replaceResourceInRole (challengeId, roleId, newMember, oldMember
739730
}
740731
}
741732
}
733+
734+
/**
735+
* Update Challenge skill
736+
* @param {UUID} challengeId id of the challenge for which resource is to be replaced
737+
* @param {Array} skills array of skill
738+
*/
739+
export function updateChallengeSkills (challengeId, skills) {
740+
return async (dispatch) => {
741+
try {
742+
if (!skills) {
743+
return
744+
}
745+
await updateChallengeSkillsApi({
746+
workId: challengeId,
747+
workTypeId: WORK_TYPE_ID,
748+
skillIds: skills.map(skill => skill.id)
749+
})
750+
dispatch({
751+
type: UPDATE_CHALLENGES_SKILLS_SUCCESS,
752+
payload: skills
753+
})
754+
} catch (error) {
755+
return _.get(error, 'response.data.message', 'Can not save skill')
756+
}
757+
}
758+
}

src/components/ChallengeEditor/ChallengeView/index.js

-1
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,6 @@ const ChallengeView = ({
244244
<div className={styles.group}>
245245
<div className={styles.title}>Public specification <span>*</span></div>
246246
<TextEditorField
247-
challengeTags={metadata.challengeTags}
248247
challenge={challenge}
249248
readOnly
250249
/>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import React, { useMemo } from 'react'
2+
import PropTypes from 'prop-types'
3+
import Select from '../../Select'
4+
import { searchSkills } from '../../../services/skills'
5+
import cn from 'classnames'
6+
import styles from './styles.module.scss'
7+
import { AUTOCOMPLETE_DEBOUNCE_TIME_MS } from '../../../config/constants'
8+
import _ from 'lodash'
9+
10+
const fetchSkills = _.debounce((inputValue, callback) => {
11+
searchSkills(inputValue).then(
12+
(skills) => {
13+
const suggestedOptions = skills.map((skillItem) => ({
14+
label: skillItem.name,
15+
value: skillItem.id
16+
}))
17+
return callback(suggestedOptions)
18+
})
19+
.catch(() => {
20+
return callback(null)
21+
})
22+
}, AUTOCOMPLETE_DEBOUNCE_TIME_MS)
23+
24+
const SkillsField = ({ readOnly, challenge, onUpdateSkills }) => {
25+
const selectedSkills = useMemo(() => (challenge.skills || []).map(skill => ({
26+
label: skill.name,
27+
value: skill.id
28+
})), [challenge.skills])
29+
const existingSkills = useMemo(() => selectedSkills.map(item => item.label).join(','), [selectedSkills])
30+
31+
return (
32+
<>
33+
<div className={styles.row}>
34+
<div className={cn(styles.field, styles.col1)}>
35+
<label htmlFor='keywords'>Skills {!readOnly && (<span>*</span>)} :</label>
36+
</div>
37+
<div className={cn(styles.field, styles.col2)}>
38+
<input type='hidden' />
39+
{readOnly ? (
40+
<span>{existingSkills}</span>
41+
) : (
42+
<Select
43+
id='skill-select'
44+
isMulti
45+
simpleValue
46+
isAsync
47+
value={selectedSkills}
48+
onChange={(values) => {
49+
onUpdateSkills((values || []).map(value => ({
50+
name: value.label,
51+
id: value.value
52+
})))
53+
}}
54+
cacheOptions
55+
loadOptions={fetchSkills}
56+
/>
57+
)}
58+
</div>
59+
</div>
60+
61+
{ !readOnly && challenge.submitTriggered && (!selectedSkills || !selectedSkills.length) && <div className={styles.row}>
62+
<div className={cn(styles.field, styles.col1)} />
63+
<div className={cn(styles.field, styles.col2, styles.error)}>
64+
Select at least one skill
65+
</div>
66+
</div> }
67+
</>
68+
)
69+
}
70+
71+
SkillsField.defaultProps = {
72+
readOnly: false,
73+
onUpdateSkills: () => { }
74+
}
75+
76+
SkillsField.propTypes = {
77+
readOnly: PropTypes.bool,
78+
challenge: PropTypes.shape().isRequired,
79+
onUpdateSkills: PropTypes.func
80+
}
81+
82+
export default SkillsField
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import React, { useMemo } from 'react'
2+
import PropTypes from 'prop-types'
3+
import Select from '../../Select'
4+
import cn from 'classnames'
5+
import styles from './styles.module.scss'
6+
import { SPECIAL_CHALLENGE_TAGS } from '../../../config/constants'
7+
import _ from 'lodash'
8+
9+
const options = [
10+
{
11+
label: 'No',
12+
value: ''
13+
},
14+
...SPECIAL_CHALLENGE_TAGS.map(tag => ({
15+
label: tag,
16+
value: tag
17+
}))
18+
]
19+
20+
const SpecialChallengeField = ({ challenge, onUpdateMultiSelect, readOnly }) => {
21+
const selectedValue = useMemo(() => {
22+
const selectedTag = _.filter(challenge.tags, (tag) => SPECIAL_CHALLENGE_TAGS.indexOf(tag) >= 0)[0]
23+
return _.find(options, {
24+
value: selectedTag || ''
25+
})
26+
}, [challenge.tags])
27+
return (
28+
<>
29+
<div className={styles.row}>
30+
<div className={cn(styles.field, styles.col1)}>
31+
<label htmlFor='keywords'>Special Challenge :</label>
32+
</div>
33+
<div className={cn(styles.field, styles.col2)}>
34+
<input type='hidden' />
35+
{readOnly ? (
36+
<span>{selectedValue.label}</span>
37+
) : (
38+
<Select
39+
options={options}
40+
id='track-select'
41+
simpleValue
42+
value={selectedValue}
43+
onChange={(value) => {
44+
const newTags = _.filter(challenge.tags, (tag) => SPECIAL_CHALLENGE_TAGS.indexOf(tag) < 0)
45+
if (value && value.value) {
46+
newTags.push(value.value)
47+
}
48+
onUpdateMultiSelect(newTags.map((tag) => ({
49+
label: tag,
50+
value: tag
51+
})), 'tags')
52+
}}
53+
/>
54+
)}
55+
</div>
56+
</div>
57+
</>
58+
)
59+
}
60+
61+
SpecialChallengeField.defaultProps = {
62+
readOnly: false
63+
}
64+
65+
SpecialChallengeField.propTypes = {
66+
challenge: PropTypes.shape().isRequired,
67+
onUpdateMultiSelect: PropTypes.func.isRequired,
68+
readOnly: PropTypes.bool
69+
}
70+
71+
export default SpecialChallengeField
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
@import "../../../styles/includes";
2+
3+
.row {
4+
box-sizing: border-box;
5+
display: flex;
6+
flex-direction: row;
7+
margin: 30px 30px 0 30px;
8+
align-content: space-between;
9+
justify-content: flex-start;
10+
11+
.field {
12+
@include upto-sm {
13+
display: block;
14+
padding-bottom: 10px;
15+
}
16+
17+
label {
18+
@include roboto-bold();
19+
20+
font-size: 16px;
21+
line-height: 19px;
22+
font-weight: 500;
23+
color: $tc-gray-80;
24+
}
25+
26+
&.col1 {
27+
max-width: 185px;
28+
min-width: 185px;
29+
margin-right: 14px;
30+
white-space: nowrap;
31+
display: flex;
32+
align-items: center;
33+
34+
span {
35+
color: $tc-red;
36+
}
37+
}
38+
39+
&.col2.error {
40+
color: $tc-red;
41+
margin-top: -25px;
42+
}
43+
&.col2 {
44+
align-self: flex-end;
45+
margin-bottom: auto;
46+
margin-top: auto;
47+
display: flex;
48+
flex-direction: row;
49+
width: 600px;
50+
}
51+
}
52+
}

0 commit comments

Comments
 (0)