From a5b741af2a3f0650dfc0f8ee3e00609c0d73baad Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Fri, 1 Sep 2023 07:07:24 +1000 Subject: [PATCH 01/12] WM is not handling error response from submissions-api download endpoint https://topcoder.atlassian.net/browse/PROD-4355 --- .../ChallengeEditor/Submissions/index.js | 74 ++++++++++++++----- src/util/topcoder-react-lib.js | 18 +++++ 2 files changed, 72 insertions(+), 20 deletions(-) diff --git a/src/components/ChallengeEditor/Submissions/index.js b/src/components/ChallengeEditor/Submissions/index.js index 7aba7d92..13428040 100644 --- a/src/components/ChallengeEditor/Submissions/index.js +++ b/src/components/ChallengeEditor/Submissions/index.js @@ -9,6 +9,7 @@ import moment from 'moment' import _ from 'lodash' import { STUDIO_URL, SUBMISSION_REVIEW_APP_URL, getTCMemberURL } from '../../../config/constants' import { PrimaryButton } from '../../Buttons' +import AlertModal from '../../Modal/AlertModal' import cn from 'classnames' import ReactSVG from 'react-svg' import { @@ -20,17 +21,23 @@ import { checkAdmin } from '../../../util/tc' import { - getTopcoderReactLib + getTopcoderReactLib, + isValidDownloadFile } from '../../../util/topcoder-react-lib' import { compressFiles } from '../../../util/files' import styles from './Submissions.module.scss' +import modalStyles from '../../../styles/modal.module.scss' const assets = require.context('../../../assets/images', false, /svg/) const ArrowDown = './arrow-down.svg' const Lock = './lock.svg' const Download = './IconSquareDownload.svg' +const theme = { + container: modalStyles.modalContainer +} + class SubmissionsComponent extends React.Component { constructor (props) { super(props) @@ -42,7 +49,8 @@ class SubmissionsComponent extends React.Component { isShowInformation: false, memberOfModal: '', sortedSubmissions: [], - downloadingAll: false + downloadingAll: false, + alertMessage: '' } this.getSubmissionsSortParam = this.getSubmissionsSortParam.bind(this) this.updateSortedSubmissions = this.updateSortedSubmissions.bind(this) @@ -222,7 +230,7 @@ class SubmissionsComponent extends React.Component { const { field, sort } = this.getSubmissionsSortParam() const revertSort = sort === 'desc' ? 'asc' : 'desc' - const { sortedSubmissions, downloadingAll } = this.state + const { sortedSubmissions, downloadingAll, alertMessage } = this.state const renderSubmission = s => (
@@ -544,19 +552,27 @@ class SubmissionsComponent extends React.Component { const submissionsService = getService(token) submissionsService.downloadSubmission(s.id) .then((blob) => { - // eslint-disable-next-line no-undef - const url = window.URL.createObjectURL(new Blob([blob])) - const link = document.createElement('a') - link.href = url - let fileName = s.legacySubmissionId - if (!fileName) { - fileName = s.id - } - fileName = fileName + '.zip' - link.setAttribute('download', `${fileName}`) - document.body.appendChild(link) - link.click() - link.parentNode.removeChild(link) + isValidDownloadFile(blob).then((isValidFile) => { + if (isValidFile.success) { + // eslint-disable-next-line no-undef + const url = window.URL.createObjectURL(new Blob([blob])) + const link = document.createElement('a') + link.href = url + let fileName = s.legacySubmissionId + if (!fileName) { + fileName = s.id + } + fileName = fileName + '.zip' + link.setAttribute('download', `${fileName}`) + document.body.appendChild(link) + link.click() + link.parentNode.removeChild(link) + } else { + this.setState({ + alertMessage: isValidFile.message || 'Can not download this submission.' + }) + } + }) }) }} > @@ -611,10 +627,14 @@ class SubmissionsComponent extends React.Component { fileName = fileName + '.zip' submissionsService.downloadSubmission(submission.id) .then((blob) => { - const file = new window.File([blob], `${fileName}`) - allFiles.push(file) - downloadedFile += 1 - checkToCompressFiles() + isValidDownloadFile(blob).then((isValidFile) => { + if (isValidFile.success) { + const file = new window.File([blob], `${fileName}`) + allFiles.push(file) + } + downloadedFile += 1 + checkToCompressFiles() + }) }).catch(() => { downloadedFile += 1 checkToCompressFiles() @@ -625,6 +645,20 @@ class SubmissionsComponent extends React.Component {
) : null} + + {alertMessage ? ( + { + this.setState({ + alertMessage: '' + }) + }} + /> + ) : null} ) } diff --git a/src/util/topcoder-react-lib.js b/src/util/topcoder-react-lib.js index 5eb90f84..e139379b 100644 --- a/src/util/topcoder-react-lib.js +++ b/src/util/topcoder-react-lib.js @@ -14,3 +14,21 @@ export const getTopcoderReactLib = () => { const reactLib = require('topcoder-react-lib') return reactLib } + +export const isValidDownloadFile = async (blobFile) => { + if (!blobFile) { + return { + success: false + } + } + if (blobFile.type.indexOf('json') >= 0) { + const backendResonse = JSON.parse(await blobFile.text()) + return { + success: false, + message: backendResonse.message || '' + } + } + return { + success: true + } +} From 7a776fce978aa6c04a389a38dfef17745f360230 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Thu, 14 Sep 2023 09:18:57 +1000 Subject: [PATCH 02/12] Prevent copilots from paying themselves https://github.com/topcoder-platform/work-manager/issues/1568 --- .../ChallengeViewTabs/index.js | 89 +++++++++++++++---- src/components/ChallengeEditor/index.js | 49 +++++++--- src/config/constants.js | 4 + src/util/tc.js | 10 +++ 4 files changed, 119 insertions(+), 33 deletions(-) diff --git a/src/components/ChallengeEditor/ChallengeViewTabs/index.js b/src/components/ChallengeEditor/ChallengeViewTabs/index.js index c8cb0007..84ee5676 100644 --- a/src/components/ChallengeEditor/ChallengeViewTabs/index.js +++ b/src/components/ChallengeEditor/ChallengeViewTabs/index.js @@ -13,7 +13,12 @@ import LegacyLinks from '../../LegacyLinks' import ForumLink from '../../ForumLink' import ResourcesTab from '../Resources' import Submissions from '../Submissions' -import { checkAdmin, checkEditResourceRoles, checkReadOnlyRoles } from '../../../util/tc' +import { + checkAdmin, + checkEditResourceRoles, + checkReadOnlyRoles, + checkCopilot +} from '../../../util/tc' import { CHALLENGE_STATUS, MESSAGE } from '../../../config/constants' import Tooltip from '../../Tooltip' import CancelDropDown from '../Cancel-Dropdown' @@ -117,6 +122,21 @@ const ChallengeViewTabs = ({ const isDraft = challenge.status.toUpperCase() === CHALLENGE_STATUS.DRAFT const isSelfServiceCopilot = challenge.legacy.selfServiceCopilot === loggedInUser.handle const isAdmin = checkAdmin(token) + + // Make sure that the Launch and Mark as completed buttons are hidden + // for tasks that are assigned to the current logged in user, if that user has the copilot role. + const preventCopilotFromActivatingTask = useMemo(() => { + return isTask && + checkCopilot(token) && + assignedMemberDetails && + loggedInUser && + `${loggedInUser.userId}` === `${assignedMemberDetails.userId}` + }, [ + token, + assignedMemberDetails, + loggedInUser + ]) + const isReadOnly = checkReadOnlyRoles(token) const canApprove = (isSelfServiceCopilot || enableEdit) && isDraft && isSelfService const hasBillingAccount = _.get(projectDetail, 'billingAccountId') !== null @@ -125,10 +145,35 @@ const ChallengeViewTabs = ({ // OR if this isn't a non-self-service draft, permit launching if: // a) the current user is either the self-service copilot or is an admin AND // b) the challenge is approved - const canLaunch = enableEdit && hasBillingAccount && !isReadOnly && - ((!isSelfService && isDraft) || - ((isSelfServiceCopilot || isAdmin) && - challenge.status.toUpperCase() === CHALLENGE_STATUS.APPROVED)) + const canLaunch = useMemo(() => { + return enableEdit && + hasBillingAccount && + (!isReadOnly) && + (!preventCopilotFromActivatingTask) && + ( + ( + !isSelfService && + isDraft + ) || + ( + ( + isSelfServiceCopilot || + isAdmin + ) && + challenge.status.toUpperCase() === CHALLENGE_STATUS.APPROVED + ) + ) + }, [ + enableEdit, + hasBillingAccount, + isReadOnly, + isSelfService, + isDraft, + isSelfServiceCopilot, + isAdmin, + challenge.status, + preventCopilotFromActivatingTask + ]) return (
@@ -184,20 +229,26 @@ const ChallengeViewTabs = ({ />
)} - {isTask && challenge.status === 'Active' && ( -
- {assignedMemberDetails ? ( - - - - ) : ( - - {/* Don't disable button for real inside tooltip, otherwise mouseEnter/Leave events work not good */} - - - )} -
- )} + { + ( + isTask && + challenge.status === 'Active' && + !preventCopilotFromActivatingTask + ) && ( +
+ {assignedMemberDetails ? ( + + + + ) : ( + + {/* Don't disable button for real inside tooltip, otherwise mouseEnter/Leave events work not good */} + + + )} +
+ ) + } {enableEdit && !canEditResource && ( )} diff --git a/src/components/ChallengeEditor/index.js b/src/components/ChallengeEditor/index.js index f181e87b..0bf42dfa 100644 --- a/src/components/ChallengeEditor/index.js +++ b/src/components/ChallengeEditor/index.js @@ -30,7 +30,12 @@ import { MULTI_ROUND_CHALLENGE_TEMPLATE_ID, DS_TRACK_ID, CHALLENGE_STATUS } from '../../config/constants' -import { getDomainTypes, getResourceRoleByName, is2RoundsChallenge } from '../../util/tc' +import { + getDomainTypes, + getResourceRoleByName, + is2RoundsChallenge, + checkCopilot +} from '../../util/tc' import { getPhaseEndDate } from '../../util/date' import { PrimaryButton, OutlineButton } from '../Buttons' import TrackField from './Track-Field' @@ -1527,6 +1532,14 @@ class ChallengeEditor extends Component { const statusMessage = challenge.status && challenge.status.split(' ')[0].toUpperCase() const errorContainer =
{error}
+ // Make sure that the Launch and Mark as completed buttons are hidden + // for tasks that are assigned to the current logged in user, if that user has the copilot role. + const preventCopilotFromActivatingTask = isTask && + checkCopilot(token) && + assignedMemberDetails && + loggedInUser && + `${loggedInUser.userId}` === `${assignedMemberDetails.userId}` + const actionButtons = {!isLoading && this.state.hasValidationErrors &&
Please fix the errors before saving
} { @@ -1553,18 +1566,23 @@ class ChallengeEditor extends Component { )} - {isDraft && ( -
- {(challenge.legacyId || isTask) && !this.state.hasValidationErrors ? ( - - ) : ( - - {/* Don't disable button for real inside tooltip, otherwise mouseEnter/Leave events work not good */} - - - )} -
- )} + { + ( + isDraft && + !preventCopilotFromActivatingTask + ) && ( +
+ {(challenge.legacyId || isTask) && !this.state.hasValidationErrors ? ( + + ) : ( + + {/* Don't disable button for real inside tooltip, otherwise mouseEnter/Leave events work not good */} + + + )} +
+ ) + } {statusMessage !== CHALLENGE_STATUS.CANCELLED &&
@@ -1575,7 +1593,10 @@ class ChallengeEditor extends Component {
- {isTask && ( + {( + isTask && + !preventCopilotFromActivatingTask + ) && (
diff --git a/src/config/constants.js b/src/config/constants.js index 0bb1db0f..a959f65b 100644 --- a/src/config/constants.js +++ b/src/config/constants.js @@ -263,6 +263,10 @@ export const ADMIN_ROLES = [ 'connect admin' ] +export const COPILOT_ROLES = [ + 'copilot' +] + export const downloadAttachmentURL = (challengeId, attachmentId, token) => `${CHALLENGE_API_URL}/${challengeId}/attachments/${attachmentId}/download?token=${token}` diff --git a/src/util/tc.js b/src/util/tc.js index 2f85ed5d..00b8c81e 100644 --- a/src/util/tc.js +++ b/src/util/tc.js @@ -6,6 +6,7 @@ import { CHALLENGE_TRACKS, ALLOWED_USER_ROLES, ADMIN_ROLES, + COPILOT_ROLES, SUBMITTER_ROLE_UUID, READ_ONLY_ROLES, ALLOWED_DOWNLOAD_SUBMISSIONS_ROLES, @@ -198,6 +199,15 @@ export const checkAdmin = token => { return roles.some(val => ADMIN_ROLES.indexOf(val.toLowerCase()) > -1) } +/** + * Checks if token has any of the copilot roles + * @param token + */ +export const checkCopilot = token => { + const roles = _.get(decodeToken(token), 'roles') + return roles.some(val => COPILOT_ROLES.indexOf(val.toLowerCase()) > -1) +} + /** * Get resource role by name * From aa76dfeaa478a681d42d7b0948cbfb1925886a7d Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Fri, 15 Sep 2023 06:27:03 +1000 Subject: [PATCH 03/12] Refactor tags and skills in anticipation of new skills services https://github.com/topcoder-platform/work-manager/issues/1566 https://github.com/topcoder-platform/work-manager/issues/1569 --- config/constants/development.js | 9 +-- config/constants/production.js | 8 +-- src/actions/challenges.js | 12 ---- .../ChallengeEditor/ChallengeView/index.js | 1 - .../ChallengeEditor/SkillsField/index.js | 60 ++++++++++++++++ .../styles.module.scss} | 0 .../SpecialChallengeField/index.js | 71 +++++++++++++++++++ .../SpecialChallengeField/styles.module.scss | 52 ++++++++++++++ .../ChallengeEditor/TagsField/index.js | 51 +++++++++---- .../TagsField/styles.module.scss | 52 ++++++++++++++ .../ChallengeEditor/TextEditor-Field/index.js | 17 +++-- src/components/ChallengeEditor/index.js | 1 - src/components/Select/index.js | 30 +++++++- src/config/constants.js | 8 ++- src/containers/ChallengeEditor/index.js | 5 -- src/services/challenges.js | 15 ---- src/services/skills.js | 20 ++++++ 17 files changed, 344 insertions(+), 68 deletions(-) create mode 100644 src/components/ChallengeEditor/SkillsField/index.js rename src/components/ChallengeEditor/{TagsField/Tags-Field.module.scss => SkillsField/styles.module.scss} (100%) create mode 100644 src/components/ChallengeEditor/SpecialChallengeField/index.js create mode 100644 src/components/ChallengeEditor/SpecialChallengeField/styles.module.scss create mode 100644 src/components/ChallengeEditor/TagsField/styles.module.scss create mode 100644 src/services/skills.js diff --git a/config/constants/development.js b/config/constants/development.js index f4d30341..26c6b21d 100644 --- a/config/constants/development.js +++ b/config/constants/development.js @@ -1,11 +1,13 @@ 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}`, @@ -22,8 +24,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 +52,6 @@ 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}/emsi-skills/skills/auto-complete` } diff --git a/config/constants/production.js b/config/constants/production.js index 29c73f3e..68e61b01 100644 --- a/config/constants/production.js +++ b/config/constants/production.js @@ -1,11 +1,12 @@ 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}`, @@ -22,8 +23,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 +49,6 @@ 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}/emsi-skills/skills/auto-complete` } diff --git a/src/actions/challenges.js b/src/actions/challenges.js index 73855f2e..e09885a0 100644 --- a/src/actions/challenges.js +++ b/src/actions/challenges.js @@ -1,7 +1,6 @@ import _ from 'lodash' import { fetchChallengeTypes, - fetchChallengeTags, fetchGroups, fetchTimelineTemplates, fetchChallengePhases, @@ -515,17 +514,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({ 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..a7f2fac0 --- /dev/null +++ b/src/components/ChallengeEditor/SkillsField/index.js @@ -0,0 +1,60 @@ +import React, { useState, 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.emsiId + })) + return callback(suggestedOptions) + }) + .catch(() => callback(null)) +}, AUTOCOMPLETE_DEBOUNCE_TIME_MS) + +const SkillsField = ({ readOnly }) => { + const [selectedSkills, setSelectedSkills] = useState([]) + const existingSkills = useMemo(() => selectedSkills.map(item => item.label).join(','), [selectedSkills]) + + return ( +
+
+ +
+
+ + {readOnly ? ( + {existingSkills} + ) : ( + + {readOnly ? ( + {selectedValue.label} + ) : ( + {readOnly ? ( {existingTags} - ) : ( onUpdateMultiSelect([ + ...(value || []), + ...selectedSpecialChallengeValues + ], 'tags')} + isCreatable + /> + )}
@@ -39,13 +62,11 @@ const TagsField = ({ challengeTags, challenge, onUpdateMultiSelect, readOnly }) } 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..ee4b6e21 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,7 +27,6 @@ class TextEditorField extends Component { render () { const { - challengeTags, challenge, onUpdateCheckbox, addFileType, @@ -37,9 +38,6 @@ class TextEditorField extends Component { 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 +77,19 @@ class TextEditorField extends Component { />
)} + + {challenge.trackId === CHALLENGE_TRACKS.DESIGN && ( {}, onUpdateCheckbox: () => {}, @@ -128,7 +132,6 @@ TextEditorField.defaultProps = { } TextEditorField.propTypes = { - challengeTags: PropTypes.arrayOf(PropTypes.object).isRequired, challenge: PropTypes.shape().isRequired, onUpdateCheckbox: PropTypes.func, addFileType: PropTypes.func, diff --git a/src/components/ChallengeEditor/index.js b/src/components/ChallengeEditor/index.js index 0bf42dfa..bb144f7c 100644 --- a/src/components/ChallengeEditor/index.js +++ b/src/components/ChallengeEditor/index.js @@ -1826,7 +1826,6 @@ class ChallengeEditor extends Component { Access specification templates here
) + } + 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 * 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', []) +} From 073fa1fb984c547d486bdfa8f13e6ab71c4f7ddc Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Wed, 20 Sep 2023 07:39:01 +1000 Subject: [PATCH 04/12] Update to use new skill auto-complete format --- src/components/ChallengeEditor/SkillsField/index.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/ChallengeEditor/SkillsField/index.js b/src/components/ChallengeEditor/SkillsField/index.js index a7f2fac0..c7cf4b9c 100644 --- a/src/components/ChallengeEditor/SkillsField/index.js +++ b/src/components/ChallengeEditor/SkillsField/index.js @@ -12,11 +12,13 @@ const fetchSkills = _.debounce((inputValue, callback) => { (skills) => { const suggestedOptions = skills.map((skillItem) => ({ label: skillItem.name, - value: skillItem.emsiId + value: skillItem.id })) return callback(suggestedOptions) }) - .catch(() => callback(null)) + .catch(() => { + return callback(null) + }) }, AUTOCOMPLETE_DEBOUNCE_TIME_MS) const SkillsField = ({ readOnly }) => { From b27dbfbbe80a6d8ba6abe89055386f3fc8c8cc67 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Wed, 18 Oct 2023 06:29:17 +1100 Subject: [PATCH 05/12] Saving skills to new standardised skills API https://topcoder.atlassian.net/browse/TSJR-25 --- config/constants/development.js | 4 ++- config/constants/production.js | 4 ++- src/actions/challenges.js | 33 +++++++++++++++++-- .../ChallengeEditor/SkillsField/index.js | 23 +++++++++---- .../ChallengeEditor/TextEditor-Field/index.js | 5 +++ src/components/ChallengeEditor/index.js | 26 +++++++++++++-- src/config/constants.js | 6 +++- src/containers/ChallengeEditor/index.js | 6 ++++ src/reducers/challenges.js | 15 ++++++++- src/services/challenges.js | 12 ++++++- 10 files changed, 119 insertions(+), 15 deletions(-) diff --git a/config/constants/development.js b/config/constants/development.js index 26c6b21d..a613f53d 100644 --- a/config/constants/development.js +++ b/config/constants/development.js @@ -53,5 +53,7 @@ module.exports = { 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`, - SKILLS_V5_API_URL: `${API_V5}/emsi-skills/skills/auto-complete` + 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 68e61b01..1b69d935 100644 --- a/config/constants/production.js +++ b/config/constants/production.js @@ -50,5 +50,7 @@ module.exports = { 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`, - SKILLS_V5_API_URL: `${API_V5}/emsi-skills/skills/auto-complete` + 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 e09885a0..ab019815 100644 --- a/src/actions/challenges.js +++ b/src/actions/challenges.js @@ -19,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 { @@ -51,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' @@ -727,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/SkillsField/index.js b/src/components/ChallengeEditor/SkillsField/index.js index c7cf4b9c..a3fe0b91 100644 --- a/src/components/ChallengeEditor/SkillsField/index.js +++ b/src/components/ChallengeEditor/SkillsField/index.js @@ -1,4 +1,4 @@ -import React, { useState, useMemo } from 'react' +import React, { useMemo } from 'react' import PropTypes from 'prop-types' import Select from '../../Select' import { searchSkills } from '../../../services/skills' @@ -21,8 +21,11 @@ const fetchSkills = _.debounce((inputValue, callback) => { }) }, AUTOCOMPLETE_DEBOUNCE_TIME_MS) -const SkillsField = ({ readOnly }) => { - const [selectedSkills, setSelectedSkills] = useState([]) +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 ( @@ -41,7 +44,12 @@ const SkillsField = ({ readOnly }) => { simpleValue isAsync value={selectedSkills} - onChange={(value) => setSelectedSkills(value || [])} + onChange={(values) => { + onUpdateSkills((values || []).map(value => ({ + name: value.label, + id: value.value + }))) + }} cacheOptions loadOptions={fetchSkills} /> @@ -52,11 +60,14 @@ const SkillsField = ({ readOnly }) => { } SkillsField.defaultProps = { - readOnly: false + readOnly: false, + onUpdateSkills: () => { } } SkillsField.propTypes = { - readOnly: PropTypes.bool + readOnly: PropTypes.bool, + challenge: PropTypes.shape().isRequired, + onUpdateSkills: PropTypes.func } export default SkillsField diff --git a/src/components/ChallengeEditor/TextEditor-Field/index.js b/src/components/ChallengeEditor/TextEditor-Field/index.js index ee4b6e21..6a90a139 100644 --- a/src/components/ChallengeEditor/TextEditor-Field/index.js +++ b/src/components/ChallengeEditor/TextEditor-Field/index.js @@ -32,6 +32,7 @@ class TextEditorField extends Component { addFileType, removeFileType, onUpdateDescription, + onUpdateSkills, onUpdateMultiSelect, shouldShowPrivateDescription, onUpdateMetadata, @@ -89,6 +90,8 @@ class TextEditorField extends Component { /> {challenge.trackId === CHALLENGE_TRACKS.DESIGN && ( @@ -127,6 +130,7 @@ TextEditorField.defaultProps = { onUpdateCheckbox: () => {}, addFileType: () => {}, onUpdateDescription: () => {}, + onUpdateSkills: () => {}, onUpdateMultiSelect: () => {}, readOnly: false } @@ -138,6 +142,7 @@ TextEditorField.propTypes = { 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 bb144f7c..c8b898e3 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 @@ -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) { @@ -1832,6 +1852,7 @@ class ChallengeEditor extends Component { removeFileType={this.removeFileType} onUpdateInput={this.onUpdateInput} onUpdateDescription={this.onUpdateDescription} + onUpdateSkills={this.onUpdateSkills} onUpdateMultiSelect={this.onUpdateMultiSelect} onUpdateMetadata={this.onUpdateMetadata} /> @@ -1915,6 +1936,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/config/constants.js b/src/config/constants.js index 837f2d0c..aba789d8 100644 --- a/src/config/constants.js +++ b/src/config/constants.js @@ -28,7 +28,9 @@ export const { API_V3, API_V4, API_V5, - SKILLS_V5_API_URL + SKILLS_V5_API_URL, + UPDATE_SKILLS_V5_API_URL, + WORK_TYPE_ID } = process.env export const CREATE_FORUM_TYPE_IDS = typeof process.env.CREATE_FORUM_TYPE_IDS === 'string' ? process.env.CREATE_FORUM_TYPE_IDS.split(',') : process.env.CREATE_FORUM_TYPE_IDS @@ -89,6 +91,8 @@ export const LOAD_CHALLENGES_SUCCESS = 'LOAD_CHALLENGES_SUCCESS' export const LOAD_CHALLENGES_PENDING = 'LOAD_CHALLENGES_PENDING' export const LOAD_CHALLENGES_FAILURE = 'LOAD_CHALLENGES_FAILURE' +export const UPDATE_CHALLENGES_SKILLS_SUCCESS = 'UPDATE_CHALLENGES_SKILLS_SUCCESS' + export const LOAD_CHALLENGE_DETAILS = 'LOAD_CHALLENGE_DETAILS' export const LOAD_CHALLENGE_DETAILS_SUCCESS = 'LOAD_CHALLENGE_DETAILS_SUCCESS' export const LOAD_CHALLENGE_DETAILS_PENDING = 'LOAD_CHALLENGE_DETAILS_PENDING' diff --git a/src/containers/ChallengeEditor/index.js b/src/containers/ChallengeEditor/index.js index 354d0655..2f55d3b5 100644 --- a/src/containers/ChallengeEditor/index.js +++ b/src/containers/ChallengeEditor/index.js @@ -28,6 +28,7 @@ import { deleteChallenge, createChallenge, replaceResourceInRole, + updateChallengeSkills, createResource, deleteResource } from '../../actions/challenges' @@ -415,6 +416,7 @@ class ChallengeEditor extends Component { partiallyUpdateChallengeDetails, createChallenge, replaceResourceInRole, + updateChallengeSkills, deleteChallenge, loggedInUser, projectPhases, @@ -541,6 +543,7 @@ class ChallengeEditor extends Component { updateChallengeDetails={updateChallengeDetails} createChallenge={createChallenge} replaceResourceInRole={replaceResourceInRole} + updateChallengeSkills={updateChallengeSkills} partiallyUpdateChallengeDetails={partiallyUpdateChallengeDetails} projectPhases={projectPhases} assignYourselfCopilot={this.assignYourselfCopilot} @@ -580,6 +583,7 @@ class ChallengeEditor extends Component { assignedMemberDetails={assignedMemberDetails} updateChallengeDetails={updateChallengeDetails} replaceResourceInRole={replaceResourceInRole} + updateChallengeSkills={updateChallengeSkills} partiallyUpdateChallengeDetails={ partiallyUpdateChallengeDetails } @@ -672,6 +676,7 @@ ChallengeEditor.propTypes = { createResource: PropTypes.func.isRequired, deleteResource: PropTypes.func.isRequired, replaceResourceInRole: PropTypes.func, + updateChallengeSkills: PropTypes.func, loadProject: PropTypes.func, projectPhases: PropTypes.arrayOf(PropTypes.object), isProjectPhasesLoading: PropTypes.bool, @@ -734,6 +739,7 @@ const mapDispatchToProps = { deleteChallenge, createChallenge, replaceResourceInRole, + updateChallengeSkills, loadProject, createResource, deleteResource diff --git a/src/reducers/challenges.js b/src/reducers/challenges.js index 5e455c56..6fb678f5 100644 --- a/src/reducers/challenges.js +++ b/src/reducers/challenges.js @@ -33,7 +33,8 @@ import { DELETE_CHALLENGE_SUCCESS, DELETE_CHALLENGE_FAILURE, DELETE_CHALLENGE_PENDING, - MULTI_ROUND_CHALLENGE_TEMPLATE_ID + MULTI_ROUND_CHALLENGE_TEMPLATE_ID, + UPDATE_CHALLENGES_SKILLS_SUCCESS } from '../config/constants' const initialState = { @@ -340,6 +341,18 @@ export default function (state = initialState, action) { } case SET_FILTER_CHALLENGE_VALUE: return { ...state, filterChallengeName: action.value.name, status: action.value.status } + case UPDATE_CHALLENGES_SKILLS_SUCCESS: { + if (state.challengeDetails) { + return { + ...state, + challengeDetails: { + ...state.challengeDetails, + skills: action.payload + } + } + } + return state + } default: return state } diff --git a/src/services/challenges.js b/src/services/challenges.js index 016a47d2..1c891cc5 100644 --- a/src/services/challenges.js +++ b/src/services/challenges.js @@ -2,7 +2,7 @@ import _ from 'lodash' import qs from 'qs' import { axiosInstance } from './axiosWithAuth' import { updateChallengePhaseBeforeSendRequest, convertChallengePhaseFromSecondsToHours, normalizeChallengeDataFromAPI } from '../util/date' -import { GROUPS_DROPDOWN_PER_PAGE } from '../config/constants' +import { GROUPS_DROPDOWN_PER_PAGE, UPDATE_SKILLS_V5_API_URL } from '../config/constants' const { CHALLENGE_API_URL, CHALLENGE_TYPES_URL, @@ -252,3 +252,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', {}) +} From 88b229efc540138b3b52ae2affad1c9bb0282611 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Wed, 18 Oct 2023 06:57:08 +1100 Subject: [PATCH 06/12] Tweaked wording here --- src/components/ChallengeEditor/SpecialChallengeField/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ChallengeEditor/SpecialChallengeField/index.js b/src/components/ChallengeEditor/SpecialChallengeField/index.js index e0a36863..246ed4c3 100644 --- a/src/components/ChallengeEditor/SpecialChallengeField/index.js +++ b/src/components/ChallengeEditor/SpecialChallengeField/index.js @@ -8,7 +8,7 @@ import _ from 'lodash' const options = [ { - label: 'Blank', + label: 'No', value: '' }, ...SPECIAL_CHALLENGE_TAGS.map(tag => ({ From 869abbb6e5614c6845c7b93d59a15877b40b1d2e Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Tue, 24 Oct 2023 06:45:32 +1100 Subject: [PATCH 07/12] Update to include challenge-api-version header in challenge API requests --- config/constants/development.js | 1 + config/constants/production.js | 1 + src/services/challenges.js | 49 +++++++++++++++++++++++++++------ 3 files changed, 43 insertions(+), 8 deletions(-) diff --git a/config/constants/development.js b/config/constants/development.js index a613f53d..19d232bf 100644 --- a/config/constants/development.js +++ b/config/constants/development.js @@ -13,6 +13,7 @@ module.exports = { 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`, diff --git a/config/constants/production.js b/config/constants/production.js index 1b69d935..cff76956 100644 --- a/config/constants/production.js +++ b/config/constants/production.js @@ -12,6 +12,7 @@ module.exports = { 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`, diff --git a/src/services/challenges.js b/src/services/challenges.js index 1c891cc5..4accb6c7 100644 --- a/src/services/challenges.js +++ b/src/services/challenges.js @@ -5,6 +5,7 @@ import { updateChallengePhaseBeforeSendRequest, convertChallengePhaseFromSeconds import { GROUPS_DROPDOWN_PER_PAGE, UPDATE_SKILLS_V5_API_URL } from '../config/constants' const { CHALLENGE_API_URL, + CHALLENGE_API_VERSION, CHALLENGE_TYPES_URL, CHALLENGE_TRACKS_URL, CHALLENGE_TIMELINE_TEMPLATES_URL, @@ -97,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: { + 'challenge-api-version': CHALLENGE_API_VERSION + } + }) return normalizeChallengeDataFromAPI(_.get(response, 'data')) } @@ -107,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: { + 'challenge-api-version': CHALLENGE_API_VERSION + } + }).then(response => { return normalizeChallengeDataFromAPI(_.get(response, 'data')) }) } @@ -119,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: { + 'challenge-api-version': CHALLENGE_API_VERSION + } + }).then(response => { return normalizeChallengeDataFromAPI(_.get(response, 'data')) }) } @@ -133,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: { + 'challenge-api-version': CHALLENGE_API_VERSION + } + }) } /** @@ -145,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: { + 'challenge-api-version': CHALLENGE_API_VERSION + } + }) } /** @@ -158,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: { + 'challenge-api-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 @@ -171,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: { + 'challenge-api-version': CHALLENGE_API_VERSION + } + }).then(rs => { return normalizeChallengeDataFromAPI(_.get(rs, 'data')) }) } @@ -181,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: { + 'challenge-api-version': CHALLENGE_API_VERSION + } + }) } /** From b10ce0327f05de4b060f3570816fc8fdb6afcf2f Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Tue, 24 Oct 2023 07:51:36 +1100 Subject: [PATCH 08/12] Change header to match latest changes --- src/services/challenges.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/services/challenges.js b/src/services/challenges.js index 4accb6c7..2d5db99f 100644 --- a/src/services/challenges.js +++ b/src/services/challenges.js @@ -100,7 +100,7 @@ export async function fetchChallengePhases () { export async function fetchChallenge (challengeId) { const response = await axiosInstance.get(`${CHALLENGE_API_URL}/${challengeId}`, { headers: { - 'challenge-api-version': CHALLENGE_API_VERSION + 'app-version': CHALLENGE_API_VERSION } }) return normalizeChallengeDataFromAPI(_.get(response, 'data')) @@ -114,7 +114,7 @@ export async function fetchChallenge (challengeId) { export function createChallenge (challenge) { return axiosInstance.post(CHALLENGE_API_URL, updateChallengePhaseBeforeSendRequest(challenge), { headers: { - 'challenge-api-version': CHALLENGE_API_VERSION + 'app-version': CHALLENGE_API_VERSION } }).then(response => { return normalizeChallengeDataFromAPI(_.get(response, 'data')) @@ -130,7 +130,7 @@ export function createChallenge (challenge) { export function updateChallenge (challengeId, challenge) { return axiosInstance.put(`${CHALLENGE_API_URL}/${challengeId}`, updateChallengePhaseBeforeSendRequest(challenge), { headers: { - 'challenge-api-version': CHALLENGE_API_VERSION + 'app-version': CHALLENGE_API_VERSION } }).then(response => { return normalizeChallengeDataFromAPI(_.get(response, 'data')) @@ -148,7 +148,7 @@ export function updateChallenge (challengeId, challenge) { export function createAttachments (challengeId, attachments) { return axiosInstance.post(`${CHALLENGE_API_URL}/${challengeId}/attachments`, attachments, { headers: { - 'challenge-api-version': CHALLENGE_API_VERSION + 'app-version': CHALLENGE_API_VERSION } }) } @@ -164,7 +164,7 @@ export function createAttachments (challengeId, attachments) { export function removeAttachment (challengeId, attachmentId) { return axiosInstance.delete(`${CHALLENGE_API_URL}/${challengeId}/attachments/${attachmentId}`, { headers: { - 'challenge-api-version': CHALLENGE_API_VERSION + 'app-version': CHALLENGE_API_VERSION } }) } @@ -181,7 +181,7 @@ export function fetchChallenges (filters, params) { } return axiosInstance.get(`${CHALLENGE_API_URL}?${qs.stringify(query, { encode: false })}`, { headers: { - 'challenge-api-version': CHALLENGE_API_VERSION + '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 @@ -198,7 +198,7 @@ export function fetchChallenges (filters, params) { export function patchChallenge (challengeId, params) { return axiosInstance.patch(`${CHALLENGE_API_URL}/${challengeId}`, updateChallengePhaseBeforeSendRequest(params), { headers: { - 'challenge-api-version': CHALLENGE_API_VERSION + 'app-version': CHALLENGE_API_VERSION } }).then(rs => { return normalizeChallengeDataFromAPI(_.get(rs, 'data')) @@ -212,7 +212,7 @@ export function patchChallenge (challengeId, params) { export function deleteChallenge (challengeId) { return axiosInstance.delete(`${CHALLENGE_API_URL}/${challengeId}`, { headers: { - 'challenge-api-version': CHALLENGE_API_VERSION + 'app-version': CHALLENGE_API_VERSION } }) } From 8f0e2ce262e2401f46fd1e4fbedfc95b16d072f6 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Wed, 1 Nov 2023 15:17:35 +1100 Subject: [PATCH 09/12] =?UTF-8?q?Don=E2=80=99t=20require=20tags?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ChallengeEditor/SkillsField/index.js | 61 +++++++++++-------- .../ChallengeEditor/TagsField/index.js | 53 +++++++--------- src/components/ChallengeEditor/index.js | 2 +- 3 files changed, 58 insertions(+), 58 deletions(-) diff --git a/src/components/ChallengeEditor/SkillsField/index.js b/src/components/ChallengeEditor/SkillsField/index.js index a3fe0b91..8025519a 100644 --- a/src/components/ChallengeEditor/SkillsField/index.js +++ b/src/components/ChallengeEditor/SkillsField/index.js @@ -29,33 +29,42 @@ const SkillsField = ({ readOnly, challenge, onUpdateSkills }) => { const existingSkills = useMemo(() => selectedSkills.map(item => item.label).join(','), [selectedSkills]) return ( -
-
- + <> +
+
+ +
+
+ + {readOnly ? ( + {existingSkills} + ) : ( + - {readOnly ? ( - {existingSkills} - ) : ( - - {readOnly ? ( - {existingTags} - ) : ( - + {readOnly ? ( + {existingTags} + ) : ( + From b69372101a022d50bf91e7b63630047b18633158 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Fri, 3 Nov 2023 07:04:42 +1100 Subject: [PATCH 12/12] =?UTF-8?q?Don=E2=80=99t=20let=20copilots=20assign?= =?UTF-8?q?=20to=20themselves?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ChallengeEditor/index.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/components/ChallengeEditor/index.js b/src/components/ChallengeEditor/index.js index adc68891..c8d9953e 100644 --- a/src/components/ChallengeEditor/index.js +++ b/src/components/ChallengeEditor/index.js @@ -1576,13 +1576,15 @@ class ChallengeEditor extends Component {
{!isLoading && } {!isLoading && (!isActive) && (!isCompleted) &&
-
- {!this.state.hasValidationErrors ? ( - - ) : ( - - )} -
+ {(!preventCopilotFromActivatingTask) && ( +
+ {!this.state.hasValidationErrors ? ( + + ) : ( + + )} +
+ )} { ( isDraft &&