diff --git a/README.md b/README.md index 6a9716bc..47540cab 100644 --- a/README.md +++ b/README.md @@ -39,8 +39,6 @@ npm install 1. copy the environment file in docs/dev.env to /.env -1. add `127.0.0.1 local.topcoder-dev.com` to your /etc/hosts file - 1. If you are using local instances of the API's, change the DEV_API_HOSTNAME in configs/constants/development.js to match your local api endpoint. - For example change it to 'http://localhost:3000/', @@ -50,7 +48,7 @@ npm install npm run dev ``` -You can access the app from [http://local.topcoder-dev.com:3001/](http://local.topcoder-dev.com:3001/) +You can access the app from [http://localhost:3000/](http://localhost:3000/) The page will reload if you make edits. diff --git a/config/constants/development.js b/config/constants/development.js index 7043e5b0..2a79c4c3 100644 --- a/config/constants/development.js +++ b/config/constants/development.js @@ -7,7 +7,6 @@ module.exports = { COMMUNITY_APP_URL: `https://www.${DOMAIN}`, MEMBER_API_URL: `${DEV_API_HOSTNAME}/v4/members`, MEMBER_API_V3_URL: `${DEV_API_HOSTNAME}/v3/members`, - DEV_APP_URL: `http://local.${DOMAIN}`, CHALLENGE_API_URL: `${DEV_API_HOSTNAME}/v5/challenges`, CHALLENGE_TIMELINE_TEMPLATES_URL: `${DEV_API_HOSTNAME}/v5/timeline-templates`, CHALLENGE_TYPES_URL: `${DEV_API_HOSTNAME}/v5/challenge-types`, diff --git a/config/constants/production.js b/config/constants/production.js index e690824d..b5bd162e 100644 --- a/config/constants/production.js +++ b/config/constants/production.js @@ -7,7 +7,6 @@ module.exports = { COMMUNITY_APP_URL: `https://www.${DOMAIN}`, MEMBER_API_URL: `${PROD_API_HOSTNAME}/v4/members`, MEMBER_API_V3_URL: `${PROD_API_HOSTNAME}/v3/members`, - DEV_APP_URL: `https://submission-review.${DOMAIN}`, CHALLENGE_API_URL: `${PROD_API_HOSTNAME}/v5/challenges`, CHALLENGE_TIMELINE_TEMPLATES_URL: `${PROD_API_HOSTNAME}/v5/timeline-templates`, CHALLENGE_TYPES_URL: `${PROD_API_HOSTNAME}/v5/challenge-types`, diff --git a/package-lock.json b/package-lock.json index b0c37594..57be0c21 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1018,6 +1018,87 @@ "resolved": "https://registry.npmjs.org/@csstools/convert-colors/-/convert-colors-1.4.0.tgz", "integrity": "sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw==" }, + "@emotion/cache": { + "version": "10.0.29", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-10.0.29.tgz", + "integrity": "sha512-fU2VtSVlHiF27empSbxi1O2JFdNWZO+2NFHfwO0pxgTep6Xa3uGb+3pVKfLww2l/IBGLNEZl5Xf/++A4wAYDYQ==", + "requires": { + "@emotion/sheet": "0.9.4", + "@emotion/stylis": "0.8.5", + "@emotion/utils": "0.11.3", + "@emotion/weak-memoize": "0.2.5" + } + }, + "@emotion/core": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/@emotion/core/-/core-10.1.1.tgz", + "integrity": "sha512-ZMLG6qpXR8x031NXD8HJqugy/AZSkAuMxxqB46pmAR7ze47MhNJ56cdoX243QPZdGctrdfo+s08yZTiwaUcRKA==", + "requires": { + "@babel/runtime": "^7.5.5", + "@emotion/cache": "^10.0.27", + "@emotion/css": "^10.0.27", + "@emotion/serialize": "^0.11.15", + "@emotion/sheet": "0.9.4", + "@emotion/utils": "0.11.3" + } + }, + "@emotion/css": { + "version": "10.0.27", + "resolved": "https://registry.npmjs.org/@emotion/css/-/css-10.0.27.tgz", + "integrity": "sha512-6wZjsvYeBhyZQYNrGoR5yPMYbMBNEnanDrqmsqS1mzDm1cOTu12shvl2j4QHNS36UaTE0USIJawCH9C8oW34Zw==", + "requires": { + "@emotion/serialize": "^0.11.15", + "@emotion/utils": "0.11.3", + "babel-plugin-emotion": "^10.0.27" + } + }, + "@emotion/hash": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==" + }, + "@emotion/memoize": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", + "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==" + }, + "@emotion/serialize": { + "version": "0.11.16", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-0.11.16.tgz", + "integrity": "sha512-G3J4o8by0VRrO+PFeSc3js2myYNOXVJ3Ya+RGVxnshRYgsvErfAOglKAiy1Eo1vhzxqtUvjCyS5gtewzkmvSSg==", + "requires": { + "@emotion/hash": "0.8.0", + "@emotion/memoize": "0.7.4", + "@emotion/unitless": "0.7.5", + "@emotion/utils": "0.11.3", + "csstype": "^2.5.7" + } + }, + "@emotion/sheet": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-0.9.4.tgz", + "integrity": "sha512-zM9PFmgVSqBw4zL101Q0HrBVTGmpAxFZH/pYx/cjJT5advXguvcgjHFTCaIO3enL/xr89vK2bh0Mfyj9aa0ANA==" + }, + "@emotion/stylis": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz", + "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==" + }, + "@emotion/unitless": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" + }, + "@emotion/utils": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-0.11.3.tgz", + "integrity": "sha512-0o4l6pZC+hI88+bzuaX/6BgOvQVhbt2PfmxauVaYOGgbsAw14wdKyvMCZXnsnsHys94iadcF+RG/wZyx6+ZZBw==" + }, + "@emotion/weak-memoize": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz", + "integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==" + }, "@fortawesome/fontawesome-common-types": { "version": "0.2.28", "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.28.tgz", @@ -2157,6 +2238,23 @@ "object.assign": "^4.1.0" } }, + "babel-plugin-emotion": { + "version": "10.0.33", + "resolved": "https://registry.npmjs.org/babel-plugin-emotion/-/babel-plugin-emotion-10.0.33.tgz", + "integrity": "sha512-bxZbTTGz0AJQDHm8k6Rf3RQJ8tX2scsfsRyKVgAbiUPUNIRtlK+7JxP+TAd1kRLABFxe0CFm2VdK4ePkoA9FxQ==", + "requires": { + "@babel/helper-module-imports": "^7.0.0", + "@emotion/hash": "0.8.0", + "@emotion/memoize": "0.7.4", + "@emotion/serialize": "^0.11.16", + "babel-plugin-macros": "^2.0.0", + "babel-plugin-syntax-jsx": "^6.18.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^1.0.5", + "find-root": "^1.1.0", + "source-map": "^0.5.7" + } + }, "babel-plugin-istanbul": { "version": "4.1.6", "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.6.tgz", @@ -2187,6 +2285,11 @@ "resolved": "https://registry.npmjs.org/babel-plugin-named-asset-import/-/babel-plugin-named-asset-import-0.3.6.tgz", "integrity": "sha512-1aGDUfL1qOOIoqk9QKGIo2lANk+C7ko/fqH0uIyC71x3PEGz0uVP8ISgfEsFuG+FKmjHTvFK/nNM8dowpmUxLA==" }, + "babel-plugin-syntax-jsx": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", + "integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY=" + }, "babel-plugin-syntax-object-rest-spread": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz", @@ -4165,6 +4268,11 @@ } } }, + "csstype": { + "version": "2.6.14", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.14.tgz", + "integrity": "sha512-2mSc+VEpGPblzAxyeR+vZhJKgYg0Og0nnRi7pmRXFYYxSfnOnW8A5wwQb4n4cE2nIOzqKOAzLCaEX6aBmNEv8A==" + }, "currently-unhandled": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", @@ -4542,6 +4650,22 @@ "utila": "~0.4" } }, + "dom-helpers": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.0.tgz", + "integrity": "sha512-Ru5o9+V8CpunKnz5LGgWXkmrH/20cGKwcHwS4m73zIvs54CN9epEmT/HLqFJW3kXpakAFkEdzgy1hzlJe3E4OQ==", + "requires": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + }, + "dependencies": { + "csstype": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.6.tgz", + "integrity": "sha512-+ZAmfyWMT7TiIlzdqJgjMb7S4f1beorDbWbsocyK4RaiqA5RTX3K14bnBWmmA9QEM0gRdsjyyrEmcyga8Zsxmw==" + } + } + }, "dom-serializer": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", @@ -5923,8 +6047,7 @@ "find-root": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", - "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", - "dev": true + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" }, "find-up": { "version": "2.1.0", @@ -9342,6 +9465,11 @@ } } }, + "memoize-one": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.1.1.tgz", + "integrity": "sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA==" + }, "memory-fs": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", @@ -14723,13 +14851,18 @@ } }, "react-select": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/react-select/-/react-select-1.3.0.tgz", - "integrity": "sha512-g/QAU1HZrzSfxkwMAo/wzi6/ezdWye302RGZevsATec07hI/iSxcpB1hejFIp7V63DJ8mwuign6KmB3VjdlinQ==", - "requires": { - "classnames": "^2.2.4", - "prop-types": "^15.5.8", - "react-input-autosize": "^2.1.2" + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/react-select/-/react-select-3.1.1.tgz", + "integrity": "sha512-HjC6jT2BhUxbIbxMZWqVcDibrEpdUJCfGicN0MMV+BQyKtCaPTgFekKWiOizSCy4jdsLMGjLqcFGJMhVGWB0Dg==", + "requires": { + "@babel/runtime": "^7.4.4", + "@emotion/cache": "^10.0.9", + "@emotion/core": "^10.0.9", + "@emotion/css": "^10.0.9", + "memoize-one": "^5.0.0", + "prop-types": "^15.6.0", + "react-input-autosize": "^2.2.2", + "react-transition-group": "^4.3.0" } }, "react-side-effect": { @@ -14768,6 +14901,17 @@ "prop-types": "^15.5.0" } }, + "react-transition-group": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.1.tgz", + "integrity": "sha512-Djqr7OQ2aPUiYurhPalTrVy9ddmFCCzwhqQmtN+J3+3DzLO209Fdr70QrN8Z3DsglWql6iY1lDWAfpFiBtuKGw==", + "requires": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + } + }, "read-pkg": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", @@ -17127,8 +17271,8 @@ } }, "tc-auth-lib": { - "version": "github:topcoder-platform/tc-auth-lib#fbd62f7c65f0e7eecccf2c131b07e84104505754", - "from": "github:topcoder-platform/tc-auth-lib#1.0.1", + "version": "github:topcoder-platform/tc-auth-lib#68fdc22464810c51b703a33e529cdbd6d09437de", + "from": "github:topcoder-platform/tc-auth-lib#1.0.4", "requires": { "lodash": "^4.17.19" }, diff --git a/package.json b/package.json index 1f0c041e..a4b659bd 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "react-redux": "^6.0.0", "react-redux-toastr": "^7.5.1", "react-router-dom": "^4.3.1", - "react-select": "^1.2.0", + "react-select": "^3.1.1", "react-stickynode": "^2.1.1", "react-svg": "^4.1.1", "react-tabs": "^3.0.0", diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 00000000..74a52100 Binary files /dev/null and b/public/favicon.ico differ diff --git a/scripts/start.js b/scripts/start.js index b7aaf362..71d668f7 100644 --- a/scripts/start.js +++ b/scripts/start.js @@ -99,7 +99,7 @@ checkBrowsers(paths.appPath, isInteractive) clearConsole() } console.log(chalk.cyan('Starting the development server...\n')) - openBrowser(constants.DEV_APP_URL ? `${constants.DEV_APP_URL}:${process.env.PORT || 3000}` : urls.localUrlForBrowser) + openBrowser(urls.localUrlForBrowser) }) const SIGNALS = ['SIGINT', 'SIGTERM'] diff --git a/src/actions/challenges.js b/src/actions/challenges.js index eca362f6..d9aafda0 100644 --- a/src/actions/challenges.js +++ b/src/actions/challenges.js @@ -13,8 +13,10 @@ import { fetchResourceRoles, fetchChallengeTimelines, fetchChallengeTracks, + fetchGroupDetail, updateChallenge, patchChallenge, + deleteChallenge as deleteChallengeAPI, createChallenge as createChallengeAPI, createResource as createResourceAPI, deleteResource as deleteResourceAPI @@ -38,6 +40,9 @@ import { CREATE_CHALLENGE_PENDING, CREATE_CHALLENGE_SUCCESS, CREATE_CHALLENGE_FAILURE, + DELETE_CHALLENGE_PENDING, + DELETE_CHALLENGE_SUCCESS, + DELETE_CHALLENGE_FAILURE, LOAD_CHALLENGE_RESOURCES } from '../config/constants' import { loadProject } from './projects' @@ -182,6 +187,14 @@ export function loadChallengeDetails (projectId, challengeId) { } } +/** + * Loads group details + */ +export function loadGroupDetails (groupIds) { + const promiseAll = groupIds.map(id => fetchGroupDetail(id)) + return Promise.all(promiseAll) +} + /** * Update challenge details * @@ -267,6 +280,26 @@ export function partiallyUpdateChallengeDetails (challengeId, partialChallengeDe } } +export function deleteChallenge (challengeId) { + return async (dispatch) => { + dispatch({ + type: DELETE_CHALLENGE_PENDING + }) + + return deleteChallengeAPI(challengeId).then((challenge) => { + return dispatch({ + type: DELETE_CHALLENGE_SUCCESS, + challengeDetails: challenge + }) + }).catch((error) => { + dispatch({ + type: DELETE_CHALLENGE_FAILURE + }) + throw error + }) + } +} + export function loadTimelineTemplates () { return async (dispatch) => { const timelineTemplates = await fetchTimelineTemplates() diff --git a/src/components/ChallengeEditor/AssignedMember-Field/AssignedMember-Field.module.scss b/src/components/ChallengeEditor/AssignedMember-Field/AssignedMember-Field.module.scss index 4f22fc27..33eea080 100644 --- a/src/components/ChallengeEditor/AssignedMember-Field/AssignedMember-Field.module.scss +++ b/src/components/ChallengeEditor/AssignedMember-Field/AssignedMember-Field.module.scss @@ -45,5 +45,10 @@ .readOnlyValue { margin-bottom: 0.5rem; // the same like `label` to be aligned } + + .assignSelfField { + margin-left: 20px; + padding-top: 6px; + } } diff --git a/src/components/ChallengeEditor/AssignedMember-Field/index.js b/src/components/ChallengeEditor/AssignedMember-Field/index.js index 4731518d..cf52873f 100644 --- a/src/components/ChallengeEditor/AssignedMember-Field/index.js +++ b/src/components/ChallengeEditor/AssignedMember-Field/index.js @@ -7,12 +7,13 @@ import cn from 'classnames' import styles from './AssignedMember-Field.module.scss' import SelectUserAutocomplete from '../../SelectUserAutocomplete' -const AssignedMemberField = ({ challenge, onChange, assignedMemberDetails, readOnly }) => { +const AssignedMemberField = ({ challenge, onAssignSelf, onChange, assignedMemberDetails, readOnly }) => { const value = assignedMemberDetails ? { // if we know assigned member details, then show user `handle`, otherwise fallback to `userId` label: assignedMemberDetails.handle, value: assignedMemberDetails.userId + '' } : null + return (
@@ -28,6 +29,15 @@ const AssignedMemberField = ({ challenge, onChange, assignedMemberDetails, readO /> )}
+ { + !readOnly && +
+ { + e.preventDefault() + onAssignSelf() + }}>Assign to me +
+ }
) } @@ -41,7 +51,8 @@ AssignedMemberField.propTypes = { challenge: PropTypes.shape().isRequired, onChange: PropTypes.func, assignedMemberDetails: PropTypes.shape(), - readOnly: PropTypes.bool + readOnly: PropTypes.bool, + onAssignSelf: PropTypes.func } export default AssignedMemberField diff --git a/src/components/ChallengeEditor/BillingAccount-Field/index.js b/src/components/ChallengeEditor/BillingAccount-Field/index.js index 6b376899..80576bad 100644 --- a/src/components/ChallengeEditor/BillingAccount-Field/index.js +++ b/src/components/ChallengeEditor/BillingAccount-Field/index.js @@ -13,14 +13,12 @@ const BillingAccountField = ({ accounts, onUpdateSelect, challenge }) => {
({ label: template.name, value: template.name, name: template.name }))} placeholder='Select' - labelKey='name' - valueKey='name' - clearable={false} - value={currentTemplate} + isClearable={false} + value={currentTemplate && { label: currentTemplate.name, value: currentTemplate.name }} onChange={(e) => resetPhase(e)} /> )} diff --git a/src/components/ChallengeEditor/ChallengeView/index.js b/src/components/ChallengeEditor/ChallengeView/index.js index 3dbd0de1..b2316299 100644 --- a/src/components/ChallengeEditor/ChallengeView/index.js +++ b/src/components/ChallengeEditor/ChallengeView/index.js @@ -1,4 +1,4 @@ -import React, { useState } from 'react' +import React, { useState, useEffect } from 'react' import _ from 'lodash' import { Helmet } from 'react-helmet' import PropTypes from 'prop-types' @@ -21,6 +21,7 @@ import PhaseInput from '../../PhaseInput' import LegacyLinks from '../../LegacyLinks' import AssignedMemberField from '../AssignedMember-Field' import { getResourceRoleByName } from '../../../util/tc' +import { loadGroupDetails } from '../../../actions/challenges' import Tooltip from '../../Tooltip' import { MESSAGE, REVIEW_TYPES } from '../../../config/constants' @@ -40,6 +41,18 @@ const ChallengeView = ({ const challengeTrack = _.find(metadata.challengeTracks, { id: challenge.trackId }) const [openAdvanceSettings, setOpenAdvanceSettings] = useState(false) + const [groups, setGroups] = useState('') + + useEffect(() => { + if (challenge.groups && challenge.groups.length > 0) { + loadGroupDetails(challenge.groups).then(res => { + const groups = _.map(res, 'name').join(', ') + setGroups(groups) + }) + } else { + setGroups('') + } + }, [challenge.groups]) const getResourceFromProps = (name) => { const { resourceRoles } = metadata @@ -167,7 +180,7 @@ const ChallengeView = ({
{openAdvanceSettings && (
- Groups: {challenge.groups ? challenge.groups.join(', ') : ''} + Groups: {groups}
)} { diff --git a/src/components/ChallengeEditor/Groups-Field/index.js b/src/components/ChallengeEditor/Groups-Field/index.js index f9624450..a0c014a2 100644 --- a/src/components/ChallengeEditor/Groups-Field/index.js +++ b/src/components/ChallengeEditor/Groups-Field/index.js @@ -1,38 +1,69 @@ import React from 'react' import PropTypes from 'prop-types' -import Select from '../../Select' +import AsyncSelect from '../../Select/AsyncSelect' import cn from 'classnames' import styles from './Groups-Field.module.scss' +import _ from 'lodash' +import { fetchGroups } from '../../../services/challenges' +import { AUTOCOMPLETE_MIN_LENGTH, AUTOCOMPLETE_DEBOUNCE_TIME_MS } from '../../../config/constants' + +const GroupsField = ({ onUpdateMultiSelect, challenge }) => { + const [groups, setGroups] = React.useState([]) + + const onInputChange = React.useCallback(_.debounce(async (inputValue, callback) => { + if (!inputValue) return + const preparedValue = inputValue.trim() + if (preparedValue.length < AUTOCOMPLETE_MIN_LENGTH) { + return [] + } + const data = await fetchGroups({ name: inputValue }) + const suggestions = data.map(suggestion => ({ + label: suggestion.name, + value: suggestion.id + })) + callback && callback(suggestions) + }, AUTOCOMPLETE_DEBOUNCE_TIME_MS), []) + + React.useEffect(() => { + Promise.all( + challenge.groups + .map(group => fetchGroups({}, `/${group}`)) + ).then(groups => { + setGroups(groups.map(group => ({ + label: group.name, + value: group.id + }))) + }).catch(console.error) + }, []) -const GroupsField = ({ groups, onUpdateMultiSelect, challenge }) => { return (
- } - { !isDesignChallenge && + { !isDesignChallenge && !isTask && communityOption() }
@@ -77,14 +79,12 @@ const ReviewTypeField = ({ reviewers, challenge, onUpdateOthers, onUpdateSelect isInternal && ( ({ label: tag, value: tag }))} onChange={(value) => onUpdateMultiSelect(value, 'tags')} />)}
diff --git a/src/components/ChallengeEditor/Terms-Field/index.js b/src/components/ChallengeEditor/Terms-Field/index.js index 2faf8643..4d05e086 100644 --- a/src/components/ChallengeEditor/Terms-Field/index.js +++ b/src/components/ChallengeEditor/Terms-Field/index.js @@ -6,6 +6,17 @@ import styles from './Terms-Field.module.scss' const TermsField = ({ terms, challenge, onUpdateMultiSelect }) => { const mapOps = item => ({ label: item.title, value: item.id }) + + const [currTerms, setCurrTerms] = React.useState([]) + + React.useEffect(() => { + const challengeTerms = new Set(challenge.terms) + const defaultValue = terms + .filter(term => challengeTerms.has(term.id)) + .map(mapOps) + setCurrTerms(defaultValue) + }, []) + return (
@@ -15,11 +26,14 @@ const TermsField = ({ terms, challenge, onUpdateMultiSelect }) => { t.isActive)} - value={challenge.typeId} + options={_.filter(types, t => t.isActive).map(type => ({ label: type.name, value: type.id }))} placeholder='Work Format' - labelKey='name' - valueKey='id' - clearable={false} - onChange={(e) => onUpdateSelect(e.id, false, 'typeId')} - disabled={disabled} + isClearable={false} + onChange={(e) => onUpdateSelect(e.value, false, 'typeId')} + isDisabled={disabled} />
diff --git a/src/components/ChallengeEditor/index.js b/src/components/ChallengeEditor/index.js index 935b65ad..35eee6a8 100644 --- a/src/components/ChallengeEditor/index.js +++ b/src/components/ChallengeEditor/index.js @@ -68,6 +68,7 @@ class ChallengeEditor extends Component { super(props) this.state = { isLaunch: false, + isDeleteLaunch: false, isConfirm: false, isClose: false, isOpenAdvanceSettings: false, @@ -90,6 +91,7 @@ class ChallengeEditor extends Component { this.onUpdateOthers = this.onUpdateOthers.bind(this) this.onUpdateCheckbox = this.onUpdateCheckbox.bind(this) this.onUpdateAssignedMember = this.onUpdateAssignedMember.bind(this) + this.onAssignSelf = this.onAssignSelf.bind(this) this.addFileType = this.addFileType.bind(this) this.removeFileType = this.removeFileType.bind(this) this.updateFileTypesMetadata = this.updateFileTypesMetadata.bind(this) @@ -121,6 +123,8 @@ 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.onDeleteChallenge = this.onDeleteChallenge.bind(this) + this.deleteModalLaunch = this.deleteModalLaunch.bind(this) } componentDidMount () { @@ -131,6 +135,27 @@ class ChallengeEditor extends Component { this.resetChallengeData(this.setState.bind(this)) } + deleteModalLaunch () { + if (!this.state.isDeleteLaunch) { + this.setState({ isDeleteLaunch: true }) + } + } + + async onDeleteChallenge () { + const { deleteChallenge, challengeDetails, history } = this.props + try { + this.setState({ isSaving: true }) + // Call action to delete the challenge + await deleteChallenge(challengeDetails.id) + this.setState({ isSaving: false }) + this.resetModal() + history.push(`/projects/${challengeDetails.projectId}/challenges`) + } catch (e) { + const error = _.get(e, 'response.data.message', 'Unable to Delete the challenge') + this.setState({ isSaving: false, error }) + } + } + /** * Validates challenge and if its valid calling an autosave method * @@ -155,7 +180,9 @@ class ChallengeEditor extends Component { try { const copilotResource = this.getResourceFromProps('Copilot') const copilotFromResources = copilotResource ? copilotResource.memberHandle : '' - const reviewerResource = this.getResourceFromProps('Reviewer') + const reviewerResource = + (challengeDetails.type === 'First2Finish' || challengeDetails.type === 'Task') + ? this.getResourceFromProps('Iterative Reviewer') : this.getResourceFromProps('Reviewer') const reviewerFromResources = reviewerResource ? reviewerResource.memberHandle : '' setState({ isConfirm: false, isLaunch: false }) const challengeData = this.updateAttachmentlist(challengeDetails, attachments) @@ -206,7 +233,7 @@ class ChallengeEditor extends Component { } resetModal () { - this.setState({ isLoading: false, isConfirm: false, isLaunch: false, error: null, isCloseTask: false }) + this.setState({ isLoading: false, isConfirm: false, isLaunch: false, error: null, isCloseTask: false, isDeleteLaunch: false }) } /** @@ -335,6 +362,22 @@ class ChallengeEditor extends Component { }) } + /** + * Update Assigned Member to Current User + */ + onAssignSelf () { + const { loggedInUser } = this.props + + const assignedMemberDetails = { + handle: loggedInUser.handle, + userId: loggedInUser.userId + } + + this.setState({ + assignedMemberDetails + }) + } + /** * Update Single Select * @param option The select option @@ -708,7 +751,7 @@ class ChallengeEditor extends Component { onUpdateMultiSelect (options, field) { const { challenge } = this.state let newChallenge = { ...challenge } - newChallenge[field] = options ? options.split(',') : [] + newChallenge[field] = options ? options.map(option => option.value) : [] this.setState({ challenge: newChallenge }, () => { this.validateChallenge() @@ -735,7 +778,7 @@ class ChallengeEditor extends Component { } collectChallengeData (status) { - const { attachments } = this.props + const { attachments, metadata } = this.props const challenge = pick([ 'phases', 'typeId', @@ -752,6 +795,7 @@ class ChallengeEditor extends Component { 'prizeSets', 'winners' ], this.state.challenge) + const isTask = _.find(metadata.challengeTypes, { id: challenge.typeId, isTask: true }) challenge.legacy = _.assign(this.state.challenge.legacy, { reviewType: challenge.reviewType }) @@ -762,6 +806,10 @@ class ChallengeEditor extends Component { return { ...p, prizes } }) challenge.status = status + if (status === 'Active' && isTask) { + challenge.startDate = moment().format() + } + if (this.state.challenge.id) { challenge.attachmentIds = _.map(attachments, item => item.id) } @@ -796,7 +844,7 @@ class ChallengeEditor extends Component { const avlTemplates = this.getAvailableTimelineTemplates() // chooses first available timeline template or fallback template for the new challenge const defaultTemplate = avlTemplates && avlTemplates.length > 0 ? avlTemplates[0] : STD_DEV_TIMELINE_TEMPLATE - + const isTask = _.find(metadata.challengeTypes, { id: typeId, isTask: true }) const newChallenge = { status: 'New', projectId: this.props.projectId, @@ -805,7 +853,7 @@ class ChallengeEditor extends Component { trackId, startDate: moment().add(1, 'days').format(), legacy: { - reviewType: isDesignChallenge ? REVIEW_TYPES.INTERNAL : REVIEW_TYPES.COMMUNITY + reviewType: isTask || isDesignChallenge ? REVIEW_TYPES.INTERNAL : REVIEW_TYPES.COMMUNITY }, descriptionFormat: 'markdown', timelineTemplateId: defaultTemplate.id, @@ -818,6 +866,10 @@ class ChallengeEditor extends Component { } try { const action = await createChallenge(newChallenge) + if (isTask) { + await this.updateResource(action.challengeDetails.id, 'Iterative Reviewer', action.challengeDetails.createdBy, action.challengeDetails.reviewer) + action.challengeDetails.reviewer = action.challengeDetails.createdBy + } const draftChallenge = { data: action.challengeDetails } @@ -880,9 +932,12 @@ class ChallengeEditor extends Component { case 'copilot': await this.updateResource(challengeId, 'Copilot', this.state.challenge.copilot, prevValue) break - case 'reviewer': - await this.updateResource(challengeId, 'Reviewer', this.state.challenge.reviewer, prevValue) + case 'reviewer': { + const { type } = this.state.challenge + const useIterativeReview = type === 'First2Finish' || type === 'Task' + await this.updateResource(challengeId, useIterativeReview ? 'Iterative Review' : 'Reviewer', this.state.challenge.reviewer, prevValue) break + } } } else { let patchObject = (changedField === 'reviewType') @@ -955,7 +1010,7 @@ class ChallengeEditor extends Component { try { const challengeId = this.getCurrentChallengeId() // state can have updated assigned member (in cases where user changes assignments without refreshing the page) - const { challenge: { copilot, reviewer }, assignedMemberDetails: assignedMember } = this.state + const { challenge: { copilot, reviewer, type }, assignedMemberDetails: assignedMember } = this.state const oldMemberHandle = _.get(oldAssignedMember, 'handle') const assignedMemberHandle = _.get(assignedMember, 'handle') // assigned member has been updated @@ -965,8 +1020,13 @@ class ChallengeEditor extends Component { const action = await updateChallengeDetails(challengeId, challenge) const { copilot: previousCopilot, reviewer: previousReviewer } = this.state.draftChallenge.data if (copilot !== previousCopilot) await this.updateResource(challengeId, 'Copilot', copilot, previousCopilot) - if (reviewer !== previousReviewer) await this.updateResource(challengeId, 'Reviewer', reviewer, previousReviewer) - + if (type === 'First2Finish' || type === 'Task') { + const iterativeReviewer = this.getResourceFromProps('Iterative Reviewer') + const previousIterativeReviewer = iterativeReviewer && iterativeReviewer.memberHandle + if (reviewer !== previousIterativeReviewer) await this.updateResource(challengeId, 'Iterative Reviewer', reviewer, previousIterativeReviewer) + } else { + if (reviewer !== previousReviewer) await this.updateResource(challengeId, 'Reviewer', reviewer, previousReviewer) + } const draftChallenge = { data: action.challengeDetails } draftChallenge.data.copilot = copilot draftChallenge.data.reviewer = reviewer @@ -1326,6 +1386,7 @@ class ChallengeEditor extends Component { challenge={challenge} onChange={this.onUpdateAssignedMember} assignedMemberDetails={assignedMemberDetails} + onAssignSelf={this.onAssignSelf} /> )} @@ -1354,10 +1415,10 @@ class ChallengeEditor extends Component { {/* remove terms field and use default term */} {false && ()} - + )} - { + {!isTask && (
+ )} + { + this.state.isDeleteLaunch && !this.state.isConfirm && ( + + ) } { showTimeline && (
{getTitle(isNew)}
+ {!isNew && this.props.challengeDetails.status === 'New' && }
* Required
@@ -1469,7 +1544,9 @@ ChallengeEditor.propTypes = { updateChallengeDetails: PropTypes.func.isRequired, createChallenge: PropTypes.func, replaceResourceInRole: PropTypes.func, - partiallyUpdateChallengeDetails: PropTypes.func.isRequired + partiallyUpdateChallengeDetails: PropTypes.func.isRequired, + deleteChallenge: PropTypes.func.isRequired, + loggedInUser: PropTypes.shape().isRequired } export default withRouter(ChallengeEditor) diff --git a/src/components/ChallengesComponent/ChallengeCard/ChallengeCard.module.scss b/src/components/ChallengesComponent/ChallengeCard/ChallengeCard.module.scss index bac62146..89614ec3 100644 --- a/src/components/ChallengesComponent/ChallengeCard/ChallengeCard.module.scss +++ b/src/components/ChallengesComponent/ChallengeCard/ChallengeCard.module.scss @@ -257,6 +257,31 @@ } } +.deleteButton { + height: 22px; + width: 86px; + border-radius: 11.5px; + display: flex; + justify-content: center; + align-items: center; + background-color: $tc-red; + border-color: $tc-red; + cursor: pointer; + + span { + @include roboto; + + font-size: 14px; + font-weight: 400; + line-height: 17px; + color: $white; + text-transform: capitalize; + display: flex; + justify-content: center; + align-items: center; + } +} + .icon { vertical-align: bottom; } diff --git a/src/components/ChallengesComponent/ChallengeCard/index.js b/src/components/ChallengesComponent/ChallengeCard/index.js index 9da8123f..67b15a9d 100644 --- a/src/components/ChallengesComponent/ChallengeCard/index.js +++ b/src/components/ChallengesComponent/ChallengeCard/index.js @@ -96,14 +96,18 @@ const getPhaseInfo = (c) => { * @param onUpdateLaunch * @returns {*} */ -const hoverComponents = (challenge, onUpdateLaunch) => { +const hoverComponents = (challenge, onUpdateLaunch, deleteModalLaunch) => { const communityAppUrl = `${COMMUNITY_APP_URL}/challenges/${challenge.id}` const directUrl = `${DIRECT_PROJECT_URL}/contest/detail?projectId=${challenge.legacyId}` const orUrl = `${ONLINE_REVIEW_URL}/review/actions/ViewProjectDetails?pid=${challenge.legacyId}` // NEW projects never have Legacy challenge created, so don't show links and "Activate" button for them at all if (challenge.status.toUpperCase() === CHALLENGE_STATUS.NEW) { - return null + return ( + + ) } return challenge.legacyId ? ( @@ -177,10 +181,13 @@ class ChallengeCard extends React.Component { this.state = { isConfirm: false, isLaunch: false, + isDeleteLaunch: false, isSaving: false } this.onUpdateConfirm = this.onUpdateConfirm.bind(this) this.onUpdateLaunch = this.onUpdateLaunch.bind(this) + this.onDeleteChallenge = this.onDeleteChallenge.bind(this) + this.deleteModalLaunch = this.deleteModalLaunch.bind(this) this.resetModal = this.resetModal.bind(this) this.onLaunchChallenge = this.onLaunchChallenge.bind(this) } @@ -195,8 +202,14 @@ class ChallengeCard extends React.Component { } } + deleteModalLaunch () { + if (!this.state.isDeleteLaunch) { + this.setState({ isDeleteLaunch: true }) + } + } + resetModal () { - this.setState({ isConfirm: false, isLaunch: false }) + this.setState({ isConfirm: false, isLaunch: false, isDeleteLaunch: false }) } async onLaunchChallenge () { @@ -205,10 +218,15 @@ class ChallengeCard extends React.Component { const { challenge } = this.props try { this.setState({ isSaving: true }) - // call action to update the challenge with a new status - await partiallyUpdateChallengeDetails(challenge.id, { + const isTask = _.get(challenge, 'task.isTask', false) + const payload = { status: 'Active' - }) + } + if (isTask) { + payload.startDate = moment().format() + } + // call action to update the challenge with a new status + await partiallyUpdateChallengeDetails(challenge.id, payload) this.setState({ isLaunch: true, isConfirm: challenge.id, isSaving: false }) } catch (e) { const error = _.get(e, 'response.data.message', 'Unable to activate the challenge') @@ -216,12 +234,39 @@ class ChallengeCard extends React.Component { } } + async onDeleteChallenge () { + const { deleteChallenge, challenge } = this.props + try { + this.setState({ isSaving: true }) + // Call action to delete the challenge + await deleteChallenge(challenge.id) + this.setState({ isSaving: false }) + this.resetModal() + } catch (e) { + const error = _.get(e, 'response.data.message', 'Unable to Delete the challenge') + this.setState({ isSaving: false, error }) + } + } + render () { - const { isLaunch, isConfirm, isSaving } = this.state + const { isLaunch, isConfirm, isSaving, isDeleteLaunch } = this.state const { challenge, shouldShowCurrentPhase, reloadChallengeList } = this.props const { phaseMessage, endTime } = getPhaseInfo(challenge) return (
+ { + isDeleteLaunch && !isConfirm && ( + + ) + } { isLaunch && !isConfirm && ( {endTime} )}
- {hoverComponents(challenge, this.onUpdateLaunch, this.props.showError)} + {hoverComponents(challenge, this.onUpdateLaunch, this.deleteModalLaunch)}
@@ -282,16 +327,15 @@ class ChallengeCard extends React.Component { ChallengeCard.defaultPrps = { shouldShowCurrentPhase: true, - showError: () => {}, reloadChallengeList: () => {} } ChallengeCard.propTypes = { challenge: PropTypes.object, shouldShowCurrentPhase: PropTypes.bool, - showError: PropTypes.func, reloadChallengeList: PropTypes.func, - partiallyUpdateChallengeDetails: PropTypes.func.isRequired + partiallyUpdateChallengeDetails: PropTypes.func.isRequired, + deleteChallenge: PropTypes.func.isRequired } export default withRouter(ChallengeCard) diff --git a/src/components/ChallengesComponent/ChallengeList/index.js b/src/components/ChallengesComponent/ChallengeList/index.js index fe33891d..941c27eb 100644 --- a/src/components/ChallengesComponent/ChallengeList/index.js +++ b/src/components/ChallengesComponent/ChallengeList/index.js @@ -102,7 +102,8 @@ class ChallengeList extends Component { page, perPage, totalChallenges, - partiallyUpdateChallengeDetails + partiallyUpdateChallengeDetails, + deleteChallenge } = this.props if (warnMessage) { return @@ -211,9 +212,9 @@ class ChallengeList extends Component { ) @@ -256,7 +257,8 @@ ChallengeList.propTypes = { page: PropTypes.number.isRequired, perPage: PropTypes.number.isRequired, totalChallenges: PropTypes.number.isRequired, - partiallyUpdateChallengeDetails: PropTypes.func.isRequired + partiallyUpdateChallengeDetails: PropTypes.func.isRequired, + deleteChallenge: PropTypes.func.isRequired } export default ChallengeList diff --git a/src/components/ChallengesComponent/index.js b/src/components/ChallengesComponent/index.js index c173492f..eb386d49 100644 --- a/src/components/ChallengesComponent/index.js +++ b/src/components/ChallengesComponent/index.js @@ -26,7 +26,8 @@ const ChallengesComponent = ({ page, perPage, totalChallenges, - partiallyUpdateChallengeDetails + partiallyUpdateChallengeDetails, + deleteChallenge }) => { return ( @@ -86,6 +87,7 @@ const ChallengesComponent = ({ perPage={perPage} totalChallenges={totalChallenges} partiallyUpdateChallengeDetails={partiallyUpdateChallengeDetails} + deleteChallenge={deleteChallenge} /> )}
@@ -109,7 +111,8 @@ ChallengesComponent.propTypes = { page: PropTypes.number.isRequired, perPage: PropTypes.number.isRequired, totalChallenges: PropTypes.number.isRequired, - partiallyUpdateChallengeDetails: PropTypes.func.isRequired + partiallyUpdateChallengeDetails: PropTypes.func.isRequired, + deleteChallenge: PropTypes.func.isRequired } ChallengesComponent.defaultProps = { diff --git a/src/components/PhaseInput/index.js b/src/components/PhaseInput/index.js index 1e4b4e4a..2a87c6a6 100644 --- a/src/components/PhaseInput/index.js +++ b/src/components/PhaseInput/index.js @@ -77,11 +77,9 @@ class PhaseInput extends Component {