diff --git a/package-lock.json b/package-lock.json index 2c61a7d0..4d9c938e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14513,6 +14513,96 @@ "react-lifecycles-compat": "^3.0.4" } }, + "rc-dropdown": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/rc-dropdown/-/rc-dropdown-3.2.2.tgz", + "integrity": "sha512-oA9VYYg+jQaPRdFoYFfBn5EAQk2NlL6H0vR2v6JG/8i4HEfUq8p1TTt6HyQ/dGxLe8lpnK+nM7WCjgZT/cpSRQ==", + "requires": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.6", + "rc-trigger": "^5.0.4" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.7.tgz", + "integrity": "sha512-9E9FJowqAsytyOY6LG+1KuueckRL+aQW+mKvXRXnuFGyRAyepJPmEo9vgMfXUA6O9u3IeEdv9MAkppFcaQwogQ==", + "requires": { + "regenerator-runtime": "^0.13.4" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "rc-align": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/rc-align/-/rc-align-4.0.11.tgz", + "integrity": "sha512-n9mQfIYQbbNTbefyQnRHZPWuTEwG1rY4a9yKlIWHSTbgwI+XUMGRYd0uJ5pE2UbrNX0WvnMBA1zJ3Lrecpra/A==", + "requires": { + "@babel/runtime": "^7.10.1", + "classnames": "2.x", + "dom-align": "^1.7.0", + "lodash": "^4.17.21", + "rc-util": "^5.3.0", + "resize-observer-polyfill": "^1.5.1" + } + }, + "rc-trigger": { + "version": "5.2.10", + "resolved": "https://registry.npmjs.org/rc-trigger/-/rc-trigger-5.2.10.tgz", + "integrity": "sha512-FkUf4H9BOFDaIwu42fvRycXMAvkttph9AlbCZXssZDVzz2L+QZ0ERvfB/4nX3ZFPh1Zd+uVGr1DEDeXxq4J1TA==", + "requires": { + "@babel/runtime": "^7.11.2", + "classnames": "^2.2.6", + "rc-align": "^4.0.0", + "rc-motion": "^2.0.0", + "rc-util": "^5.5.0" + } + }, + "rc-util": { + "version": "5.16.1", + "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.16.1.tgz", + "integrity": "sha512-kSCyytvdb3aRxQacS/71ta6c+kBWvM1v8/2h9d/HaNWauc3qB8pLnF20PJ8NajkNN8gb+rR1l0eWO+D4Pz+LLQ==", + "requires": { + "@babel/runtime": "^7.12.5", + "react-is": "^16.12.0", + "shallowequal": "^1.1.0" + } + } + } + }, + "rc-motion": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/rc-motion/-/rc-motion-2.4.4.tgz", + "integrity": "sha512-ms7n1+/TZQBS0Ydd2Q5P4+wJTSOrhIrwNxLXCZpR7Fa3/oac7Yi803HDALc2hLAKaCTQtw9LmQeB58zcwOsqlQ==", + "requires": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-util": "^5.2.1" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.7.tgz", + "integrity": "sha512-9E9FJowqAsytyOY6LG+1KuueckRL+aQW+mKvXRXnuFGyRAyepJPmEo9vgMfXUA6O9u3IeEdv9MAkppFcaQwogQ==", + "requires": { + "regenerator-runtime": "^0.13.4" + } + }, + "rc-util": { + "version": "5.16.1", + "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.16.1.tgz", + "integrity": "sha512-kSCyytvdb3aRxQacS/71ta6c+kBWvM1v8/2h9d/HaNWauc3qB8pLnF20PJ8NajkNN8gb+rR1l0eWO+D4Pz+LLQ==", + "requires": { + "@babel/runtime": "^7.12.5", + "react-is": "^16.12.0", + "shallowequal": "^1.1.0" + } + } + } + }, "rc-time-picker": { "version": "3.7.3", "resolved": "https://registry.npmjs.org/rc-time-picker/-/rc-time-picker-3.7.3.tgz", @@ -15388,6 +15478,11 @@ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" }, + "resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==" + }, "resolve": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.8.1.tgz", diff --git a/src/actions/challenges.js b/src/actions/challenges.js index ffc82956..7c7e3d66 100644 --- a/src/actions/challenges.js +++ b/src/actions/challenges.js @@ -46,7 +46,8 @@ import { DELETE_CHALLENGE_PENDING, DELETE_CHALLENGE_SUCCESS, DELETE_CHALLENGE_FAILURE, - LOAD_CHALLENGE_RESOURCES + LOAD_CHALLENGE_RESOURCES, + CHALLENGE_STATUS } from '../config/constants' import { loadProject } from './projects' import { removeChallengeFromPhaseProduct, saveChallengeAsPhaseProduct } from '../services/projects' @@ -58,7 +59,7 @@ import { removeChallengeFromPhaseProduct, saveChallengeAsPhaseProduct } from '.. /** * Loads active challenges of project by page */ -export function loadChallengesByPage (page, projectId, status, filterChallengeName = null) { +export function loadChallengesByPage (page, projectId, status, filterChallengeName = null, selfService = false, userHandle = null) { return (dispatch, getState) => { dispatch({ type: LOAD_CHALLENGES_PENDING, @@ -85,6 +86,12 @@ export function loadChallengesByPage (page, projectId, status, filterChallengeNa } else if (!(_.isInteger(projectId) && projectId > 0)) { filters['status'] = 'Active' } + if (selfService) { + filters.selfService = true + if (userHandle && filters.status.toUpperCase() !== CHALLENGE_STATUS.DRAFT) { + filters.selfServiceCopilot = userHandle + } + } return fetchChallenges(filters, { page, diff --git a/src/actions/sidebar.js b/src/actions/sidebar.js index 1abd3c90..2a463df4 100644 --- a/src/actions/sidebar.js +++ b/src/actions/sidebar.js @@ -52,6 +52,18 @@ export function loadProjects (filterProjectName = '', myProjects = true) { } } +/** + * Unloads projects of the authenticated user + */ +export function unloadProjects () { + return (dispatch) => { + dispatch({ + type: LOAD_PROJECTS_SUCCESS, + projects: [] + }) + } +} + /** * Reset active params. e.g activeProjectId */ diff --git a/src/components/ChallengeEditor/ChallengeSchedule-Field/index.js b/src/components/ChallengeEditor/ChallengeSchedule-Field/index.js index 8fca8678..328477d2 100644 --- a/src/components/ChallengeEditor/ChallengeSchedule-Field/index.js +++ b/src/components/ChallengeEditor/ChallengeSchedule-Field/index.js @@ -100,14 +100,12 @@ class ChallengeScheduleField extends Component { phase.scheduledStartDate = startDate phase.scheduledEndDate = moment(startDate).add(phase.duration || 0, 'hours').toDate() phase.actualStartDate = phase.scheduledStartDate - phase.actualEndDate = phase.scheduledEndDate } else { const preIndex = _.findIndex(phases, (p) => p.id === phase.predecessor) // `Invalid phase predecessor: ${phase.predecessor}` phase.scheduledStartDate = phases[preIndex].scheduledEndDate phase.scheduledEndDate = moment(phase.scheduledStartDate).add(phase.duration || 0, 'hours').toDate() phase.actualStartDate = phase.scheduledStartDate - phase.actualEndDate = phase.scheduledEndDate } } diff --git a/src/components/ChallengeEditor/ChallengeView/index.js b/src/components/ChallengeEditor/ChallengeView/index.js index fa9adddb..aa7141b3 100644 --- a/src/components/ChallengeEditor/ChallengeView/index.js +++ b/src/components/ChallengeEditor/ChallengeView/index.js @@ -37,7 +37,10 @@ const ChallengeView = ({ enableEdit, onLaunchChallenge, onCloseTask, - projectPhases + projectPhases, + assignYourselfCopilot, + showRejectChallengeModal, + loggedInUser }) => { const selectedType = _.find(metadata.challengeTypes, { id: challenge.typeId }) const challengeTrack = _.find(metadata.challengeTracks, { id: challenge.trackId }) @@ -74,7 +77,7 @@ const ChallengeView = ({ const reviewerFromResources = reviewerResource ? reviewerResource.memberHandle : '' let copilot, reviewer if (challenge) { - copilot = challenge.copilot + copilot = challenge.copilot || (challenge.legacy && challenge.legacy.selfServiceCopilot) reviewer = challenge.reviewer } copilot = copilot || copilotFromResources @@ -103,18 +106,18 @@ const ChallengeView = ({ {selectedMilestone && -
- Milestone: {selectedMilestone ? ( - - {selectedMilestone.name} - - ) : ''} -
+
+ Milestone: {selectedMilestone ? ( + + {selectedMilestone.name} + + ) : ''} +
}
Track: - {}} /> + { }} />
Type: {selectedType ? selectedType.name : ''} @@ -130,10 +133,11 @@ const ChallengeView = ({
{isTask && - } + } + copilot, + selfService: challenge.legacy.selfService + }} copilots={challengeResources} assignYourselfCopilot={assignYourselfCopilot} showRejectChallengeModal={showRejectChallengeModal} readOnly loggedInUser={loggedInUser} />
{ const [selectedTab, setSelectedTab] = useState(0) @@ -80,6 +84,22 @@ const ChallengeViewTabs = ({ const isTask = _.get(challenge, 'task.isTask', false) + const isSelfService = challenge.legacy.selfService + const isDraft = challenge.status.toUpperCase() === CHALLENGE_STATUS.DRAFT + const isSelfServiceCopilot = challenge.legacy.selfServiceCopilot === loggedInUser.handle + const isAdmin = checkAdmin(token) + const canApprove = isSelfServiceCopilot && isDraft && isSelfService + const hasBillingAccount = _.get(projectDetail, 'billingAccountId') !== null + // only challenges that have a billing account can be launched AND + // if this isn't self-service, permit launching if the challenge is draft + // 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 = hasBillingAccount && + ((!isSelfService && isDraft) || + ((isSelfServiceCopilot || isAdmin) && + challenge.status.toUpperCase() === CHALLENGE_STATUS.APPROVED)) + return (
@@ -94,7 +114,7 @@ const ChallengeViewTabs = ({ styles.actionButtonsLeft )} > - { isTask ? () + {isTask ? () : () }
@@ -107,13 +127,14 @@ const ChallengeViewTabs = ({ styles.actionButtonsRight )} > - {(challenge.status === 'Draft' || challenge.status === 'New') &&
} - {challenge.status === 'Draft' && ( + {(isDraft || challenge.status === 'New') && !isSelfService && + (
)} + {canLaunch && (
{challenge.legacyId || isTask ? ( ) : ( @@ -124,6 +145,15 @@ const ChallengeViewTabs = ({ )}
)} + {canApprove && ( +
+ +
+ )} {isTask && challenge.status === 'Active' && (
{assignedMemberDetails ? ( @@ -138,9 +168,18 @@ const ChallengeViewTabs = ({ )}
)} - {enableEdit && ( + {enableEdit && !isSelfService && ( )} + {isSelfService && isDraft && (isAdmin || isSelfServiceCopilot) && ( +
+ +
+ )}
@@ -208,6 +247,10 @@ const ChallengeViewTabs = ({ onLaunchChallenge={onLaunchChallenge} onCloseTask={onCloseTask} projectPhases={projectPhases} + assignYourselfCopilot={assignYourselfCopilot} + showRejectChallengeModal={showRejectChallengeModal} + onApproveChallenge={onApproveChallenge} + loggedInUser={loggedInUser} /> )} {selectedTab === 1 && ( @@ -244,7 +287,11 @@ ChallengeViewTabs.propTypes = { onLaunchChallenge: PropTypes.func, cancelChallenge: PropTypes.func.isRequired, onCloseTask: PropTypes.func, - projectPhases: PropTypes.arrayOf(PropTypes.object) + projectPhases: PropTypes.arrayOf(PropTypes.object), + assignYourselfCopilot: PropTypes.func.isRequired, + showRejectChallengeModal: PropTypes.func.isRequired, + loggedInUser: PropTypes.object.isRequired, + onApproveChallenge: PropTypes.func } export default ChallengeViewTabs diff --git a/src/components/ChallengeEditor/Copilot-Field/Copilot-Field.module.scss b/src/components/ChallengeEditor/Copilot-Field/Copilot-Field.module.scss index d50a3f10..c288cf00 100644 --- a/src/components/ChallengeEditor/Copilot-Field/Copilot-Field.module.scss +++ b/src/components/ChallengeEditor/Copilot-Field/Copilot-Field.module.scss @@ -44,6 +44,7 @@ display: flex; flex-direction: row; flex-wrap: wrap; + width: 100%; div { margin-right: 13px; diff --git a/src/components/ChallengeEditor/Copilot-Field/index.js b/src/components/ChallengeEditor/Copilot-Field/index.js index 25b24540..9e6392c8 100644 --- a/src/components/ChallengeEditor/Copilot-Field/index.js +++ b/src/components/ChallengeEditor/Copilot-Field/index.js @@ -1,23 +1,39 @@ -import React from 'react' -import PropTypes from 'prop-types' -import styles from './Copilot-Field.module.scss' import cn from 'classnames' import _ from 'lodash' +import PropTypes from 'prop-types' +import React from 'react' + +import { PrimaryButton } from '../../Buttons' import CopilotCard from '../../CopilotCard' -const CopilotField = ({ copilots, challenge, onUpdateOthers, readOnly }) => { +import styles from './Copilot-Field.module.scss' + +const CopilotField = ({ copilots, challenge, onUpdateOthers, readOnly, assignYourselfCopilot, loggedInUser }) => { let errMessage = 'Please set a copilot' - const selectedCopilot = _.find(copilots, { handle: challenge.copilot }) + const handleProperty = copilots.handle ? 'handle' : 'memberHandle' + const selectedCopilot = _.find(copilots, { [handleProperty]: challenge.copilot }) + const selectedCopilotHandle = selectedCopilot ? selectedCopilot[handleProperty] : undefined const copilotFee = _.find(challenge.prizeSets, p => p.type === 'copilot', []) - console.log(copilotFee) + const selfService = challenge.selfService + const copilotIsSelf = loggedInUser && selectedCopilotHandle === loggedInUser.handle + const assignButtonText = `${selectedCopilot && copilotIsSelf ? 'Una' : 'A'}ssign Yourself` + const showAssignButton = loggedInUser && (!selectedCopilotHandle || copilotIsSelf) + if (readOnly) { return (
- +
- {selectedCopilot && (
- + {(selectedCopilot || selfService) && (
+ {(selectedCopilot && )} + {((selfService && showAssignButton) &&
+ +
)}
)}
) @@ -26,7 +42,7 @@ const CopilotField = ({ copilots, challenge, onUpdateOthers, readOnly }) => { <>
- +
{ @@ -49,7 +65,7 @@ const CopilotField = ({ copilots, challenge, onUpdateOthers, readOnly }) => { CopilotField.defaultProps = { copilots: [], - onUpdateOthers: () => {}, + onUpdateOthers: () => { }, readOnly: false } @@ -57,7 +73,9 @@ CopilotField.propTypes = { copilots: PropTypes.arrayOf(PropTypes.shape()).isRequired, challenge: PropTypes.shape().isRequired, onUpdateOthers: PropTypes.func, - readOnly: PropTypes.bool + readOnly: PropTypes.bool, + assignYourselfCopilot: PropTypes.func.isRequired, + loggedInUser: PropTypes.object } export default CopilotField diff --git a/src/components/ChallengeEditor/index.js b/src/components/ChallengeEditor/index.js index ae5d8cbd..1f0b67ba 100644 --- a/src/components/ChallengeEditor/index.js +++ b/src/components/ChallengeEditor/index.js @@ -181,7 +181,7 @@ class ChallengeEditor extends Component { } } - async resetChallengeData (setState = () => {}) { + async resetChallengeData (setState = () => { }) { const { isNew, challengeDetails, metadata, attachments, challengeId, assignedMemberDetails } = this.props if ( challengeDetails && @@ -214,11 +214,13 @@ class ChallengeEditor extends Component { setState({ challenge: challengeDetail, assignedMemberDetails, - draftChallenge: { data: { - ..._.cloneDeep(challengeDetails), - copilot: challengeData.copilot, - reviewer: challengeData.reviewer - } }, + draftChallenge: { + data: { + ..._.cloneDeep(challengeDetails), + copilot: challengeData.copilot, + reviewer: challengeData.reviewer + } + }, isLoading: false, isOpenAdvanceSettings, currentTemplate @@ -506,7 +508,7 @@ class ChallengeEditor extends Component { if (fileTypesMetadataIndex > -1) { fileTypesMetadata = { ...newChallenge.metadata[fileTypesMetadataIndex] } newChallenge.metadata[fileTypesMetadataIndex] = fileTypesMetadata - // if not yet, create an empty record in metadata + // if not yet, create an empty record in metadata } else { fileTypesMetadata = { name: 'fileTypes', value: '[]' } newChallenge.metadata.push(fileTypesMetadata) @@ -1038,7 +1040,8 @@ class ChallengeEditor extends Component { newChallenge.phases = _.cloneDeep(draftChallenge.data.phases) this.setState({ draftChallenge, - challenge: newChallenge }) + challenge: newChallenge + }) } else { this.setState({ draftChallenge }) } @@ -1065,7 +1068,7 @@ class ChallengeEditor extends Component { return challengeId } - async updateAllChallengeInfo (status, cb = () => {}) { + async updateAllChallengeInfo (status, cb = () => { }) { const { updateChallengeDetails, assignedMemberDetails: oldAssignedMember, projectDetail } = this.props if (this.state.isSaving) return this.setState({ isSaving: true }, async () => { @@ -1095,11 +1098,13 @@ class ChallengeEditor extends Component { const draftChallenge = { data: action.challengeDetails } draftChallenge.data.copilot = copilot draftChallenge.data.reviewer = reviewer - this.setState({ isLaunch: true, + this.setState({ + isLaunch: true, isConfirm: newChallenge.id, draftChallenge, challenge: newChallenge, - isSaving: false }, cb) + isSaving: false + }, cb) } catch (e) { const error = this.formatResponseError(e) || `Unable to update the challenge to status ${status}` this.setState({ isSaving: false, error }, cb) @@ -1228,7 +1233,10 @@ class ChallengeEditor extends Component { projectDetail, attachments, projectPhases, - challengeId + challengeId, + assignYourselfCopilot, + challengeResources, + loggedInUser } = this.props if (_.isEmpty(challenge)) { return
Error loading challenge
@@ -1353,7 +1361,7 @@ class ChallengeEditor extends Component { /> ) - // if some information for closing task is missing, ask to complete it + // if some information for closing task is missing, ask to complete it } else { const formattedErrors = validationErrors.length === 1 ? validationErrors[0] : ( validationErrors.slice(0, -1).join(', ') + ' and ' + validationErrors[validationErrors.length - 1] @@ -1410,7 +1418,7 @@ class ChallengeEditor extends Component {
*/}
- { !this.state.hasValidationErrors ? ( + {!this.state.hasValidationErrors ? ( ) : ( @@ -1460,6 +1468,7 @@ class ChallengeEditor extends Component { const activeProjectMilestones = projectPhases.filter(phase => phase.status === MILESTONE_STATUS.ACTIVE) const currentChallengeId = this.getCurrentChallengeId() const showTimeline = false // disables the timeline for time being https://github.com/topcoder-platform/challenge-engine-ui/issues/706 + const copilotResources = metadata.members || challengeResources const challengeForm = isNew ? (
@@ -1468,11 +1477,11 @@ class ChallengeEditor extends Component { {projectDetail.version === 'v4' && } - { useTask && () } + {useTask && ()}
{showDesignChallengeWarningModel && designChallengeModal} - { errorContainer } - { actionButtons } + {errorContainer} + {actionButtons} ) : (
e.preventDefault()}> @@ -1489,7 +1498,7 @@ class ChallengeEditor extends Component {
Track: - {}} /> + { }} />
Type: {selectedType ? selectedType.name : ''} @@ -1509,7 +1518,7 @@ class ChallengeEditor extends Component { /> )} {projectDetail.version === 'v4' && } - +
- { isOpenAdvanceSettings && ( + {isOpenAdvanceSettings && ( {/* remove terms field and use default term */} @@ -1580,7 +1589,7 @@ class ChallengeEditor extends Component { /> ) } - { showTimeline && ( + {showTimeline && ( {/* hide until challenge API change is pushed to PROD https://github.com/topcoder-platform/challenge-api/issues/348 */} - { false && - { errorContainer } - { actionButtons } + {errorContainer} + {actionButtons} ) @@ -1644,11 +1653,11 @@ class ChallengeEditor extends Component {
* Required
- { activateModal } - { draftModal } - { closeTaskModal } + {activateModal} + {draftModal} + {closeTaskModal}
- { challengeForm } + {challengeForm}
@@ -1689,7 +1698,8 @@ ChallengeEditor.propTypes = { partiallyUpdateChallengeDetails: PropTypes.func.isRequired, deleteChallenge: PropTypes.func.isRequired, loggedInUser: PropTypes.shape().isRequired, - projectPhases: PropTypes.arrayOf(PropTypes.object).isRequired + projectPhases: PropTypes.arrayOf(PropTypes.object).isRequired, + assignYourselfCopilot: PropTypes.func.isRequired } export default withRouter(ChallengeEditor) diff --git a/src/components/ChallengesComponent/ChallengeCard/index.js b/src/components/ChallengesComponent/ChallengeCard/index.js index 4a0695a2..80d5fc30 100644 --- a/src/components/ChallengesComponent/ChallengeCard/index.js +++ b/src/components/ChallengesComponent/ChallengeCard/index.js @@ -163,15 +163,17 @@ const hoverComponents = (challenge, onUpdateLaunch, deleteModalLaunch) => { ) } -const renderStatus = (status) => { +const renderStatus = (status, getStatusText) => { switch (status) { case CHALLENGE_STATUS.ACTIVE: + case CHALLENGE_STATUS.APPROVED: case CHALLENGE_STATUS.NEW: case CHALLENGE_STATUS.DRAFT: case CHALLENGE_STATUS.COMPLETED: - return () + const statusText = getStatusText ? getStatusText(status) : status + return () default: - return ({status}) + return ({statusText}) } } @@ -276,7 +278,7 @@ class ChallengeCard extends React.Component { render () { const { isLaunch, isConfirm, isSaving, isDeleteLaunch, isCheckChalengePermission, hasEditChallengePermission } = this.state - const { challenge, shouldShowCurrentPhase, reloadChallengeList, isBillingAccountExpired } = this.props + const { challenge, shouldShowCurrentPhase, reloadChallengeList, isBillingAccountExpired, disableHover, getStatusText } = this.props const { phaseMessage, endTime } = getPhaseInfo(challenge) const deleteMessage = isCheckChalengePermission ? 'Checking permissions...' @@ -313,7 +315,7 @@ class ChallengeCard extends React.Component { /> ) } - { isLaunch && isConfirm && ( + {isLaunch && isConfirm && ( - ) } + )}
@@ -335,14 +337,14 @@ class ChallengeCard extends React.Component { {renderLastUpdated(challenge)} - {renderStatus(challenge.status.toUpperCase())} + {renderStatus(challenge.status.toUpperCase(), getStatusText)} {shouldShowCurrentPhase && ( {phaseMessage} {endTime} )}
- {hoverComponents(challenge, this.onUpdateLaunch, this.deleteModalLaunch)} + {(disableHover ? View Challenge : hoverComponents(challenge, this.onUpdateLaunch, this.deleteModalLaunch))}
@@ -361,7 +363,7 @@ class ChallengeCard extends React.Component { ChallengeCard.defaultPrps = { shouldShowCurrentPhase: true, - reloadChallengeList: () => {} + reloadChallengeList: () => { } } ChallengeCard.propTypes = { @@ -370,7 +372,9 @@ ChallengeCard.propTypes = { reloadChallengeList: PropTypes.func, partiallyUpdateChallengeDetails: PropTypes.func.isRequired, deleteChallenge: PropTypes.func.isRequired, - isBillingAccountExpired: PropTypes.bool + isBillingAccountExpired: PropTypes.bool, + disableHover: PropTypes.bool, + getStatusText: PropTypes.func } export default withRouter(ChallengeCard) diff --git a/src/components/ChallengesComponent/ChallengeList/index.js b/src/components/ChallengesComponent/ChallengeList/index.js index d3398b5b..e739e1d1 100644 --- a/src/components/ChallengesComponent/ChallengeList/index.js +++ b/src/components/ChallengesComponent/ChallengeList/index.js @@ -48,10 +48,10 @@ class ChallengeList extends Component { * @param {String} projectStatus project status */ updateSearchParam (searchText, projectStatus) { - const { status, filterChallengeName, loadChallengesByPage, activeProjectId } = this.props + const { status, filterChallengeName, loadChallengesByPage, activeProjectId, selfService } = this.props this.setState({ searchText }, () => { if (status !== projectStatus || searchText !== filterChallengeName) { - loadChallengesByPage(1, activeProjectId, projectStatus, searchText) + loadChallengesByPage(1, activeProjectId, projectStatus, searchText, selfService, this.getHandle()) } }) } @@ -62,9 +62,9 @@ class ChallengeList extends Component { */ handlePageChange (pageNumber) { const { searchText } = this.state - const { page, loadChallengesByPage, activeProjectId, status } = this.props + const { page, loadChallengesByPage, activeProjectId, status, selfService } = this.props if (page !== pageNumber) { - loadChallengesByPage(pageNumber, activeProjectId, status, searchText) + loadChallengesByPage(pageNumber, activeProjectId, status, searchText, selfService, this.getHandle()) } } @@ -73,8 +73,8 @@ class ChallengeList extends Component { */ reloadChallengeList () { const { searchText } = this.state - const { page, loadChallengesByPage, activeProjectId, status } = this.props - loadChallengesByPage(page, activeProjectId, status, searchText) + const { page, loadChallengesByPage, activeProjectId, status, selfService } = this.props + loadChallengesByPage(page, activeProjectId, status, searchText, selfService, this.getHandle()) } /** @@ -92,6 +92,22 @@ class ChallengeList extends Component { this.setState({ errorMessage: null }) } + getStatusTextFunc (selfService) { + const draftText = selfService ? 'Waiting for approval' : 'Draft' + return (status) => { + switch (status) { + case CHALLENGE_STATUS.DRAFT: + return draftText + default: + return status + } + } + } + + getHandle () { + return this.props.auth && this.props.auth.user ? this.props.auth.user.handle : null + } + render () { const { searchText, errorMessage } = this.state const { @@ -104,7 +120,8 @@ class ChallengeList extends Component { totalChallenges, partiallyUpdateChallengeDetails, deleteChallenge, - isBillingAccountExpired + isBillingAccountExpired, + selfService } = this.props if (warnMessage) { return @@ -112,6 +129,7 @@ class ChallengeList extends Component { let selectedTab = 0 switch (status) { + case CHALLENGE_STATUS.APPROVED: case CHALLENGE_STATUS.NEW: selectedTab = 1 break @@ -167,7 +185,8 @@ class ChallengeList extends Component { break } case 1: { - this.directUpdateSearchParam(searchText, CHALLENGE_STATUS.NEW) + const status = selfService ? CHALLENGE_STATUS.APPROVED : CHALLENGE_STATUS.NEW + this.directUpdateSearchParam(searchText, status) break } case 2: { @@ -185,11 +204,11 @@ class ChallengeList extends Component { } }}> - Active - New - Draft - Completed - Cancelled + {(selfService ? 'Assigned challenges' : 'Active')} + {(selfService ? 'Approved' : 'New')} + {this.getStatusTextFunc(selfService)(CHALLENGE_STATUS.DRAFT)} + {(!selfService && Completed)} + {(!selfService && Cancelled)} @@ -197,13 +216,16 @@ class ChallengeList extends Component { )} { challenges.length === 0 && ( - + ) } { challenges.length > 0 && (
-
Challenges Name
+
Challenge Name
Last Updated
Status
{(selectedTab === 0) && (
Current phase
)} @@ -225,6 +247,8 @@ class ChallengeList extends Component { partiallyUpdateChallengeDetails={partiallyUpdateChallengeDetails} deleteChallenge={deleteChallenge} isBillingAccountExpired={isBillingAccountExpired} + disableHover={selfService} + getStatusText={this.getStatusTextFunc(selfService)} /> ) @@ -269,7 +293,9 @@ ChallengeList.propTypes = { totalChallenges: PropTypes.number.isRequired, partiallyUpdateChallengeDetails: PropTypes.func.isRequired, deleteChallenge: PropTypes.func.isRequired, - isBillingAccountExpired: PropTypes.bool + isBillingAccountExpired: PropTypes.bool, + selfService: PropTypes.bool, + auth: PropTypes.object.isRequired } export default ChallengeList diff --git a/src/components/ChallengesComponent/ChallengeStatus/ChallengeStatus.module.scss b/src/components/ChallengesComponent/ChallengeStatus/ChallengeStatus.module.scss index 7fde3c54..12ac0156 100644 --- a/src/components/ChallengesComponent/ChallengeStatus/ChallengeStatus.module.scss +++ b/src/components/ChallengesComponent/ChallengeStatus/ChallengeStatus.module.scss @@ -1,12 +1,13 @@ @import "../../../styles/includes"; .container { - height: 22px; + min-height: 22px; width: 86px; border-radius: 3px; display: flex; justify-content: center; align-items: center; + text-align: center; span { @include roboto; diff --git a/src/components/ChallengesComponent/ChallengeStatus/index.js b/src/components/ChallengesComponent/ChallengeStatus/index.js index d11fd980..a5d074ea 100644 --- a/src/components/ChallengesComponent/ChallengeStatus/index.js +++ b/src/components/ChallengesComponent/ChallengeStatus/index.js @@ -12,21 +12,23 @@ import styles from './ChallengeStatus.module.scss' const statuses = { [CHALLENGE_STATUS.ACTIVE]: styles.green, + [CHALLENGE_STATUS.APPROVED]: styles.yellow, [CHALLENGE_STATUS.NEW]: styles.yellow, [CHALLENGE_STATUS.DRAFT]: styles.gray, [CHALLENGE_STATUS.COMPLETED]: styles.blue } -const ChallengeStatus = ({ status }) => { +const ChallengeStatus = ({ status, statusText }) => { return (
- {_.startCase(_.toLower(status))} + {_.startCase(_.toLower(statusText))}
) } ChallengeStatus.propTypes = { - status: PropTypes.string + status: PropTypes.string, + statusText: PropTypes.string } export default ChallengeStatus diff --git a/src/components/ChallengesComponent/NoChallenge/index.js b/src/components/ChallengesComponent/NoChallenge/index.js index c781701c..86f51307 100644 --- a/src/components/ChallengesComponent/NoChallenge/index.js +++ b/src/components/ChallengesComponent/NoChallenge/index.js @@ -5,13 +5,21 @@ import React from 'react' import PropTypes from 'prop-types' import styles from './NoChallenge.module.scss' -const NoChallenge = ({ activeProject }) => { +const NoChallenge = ({ + activeProject, + selfService +}) => { + let noChallengeMessage + if (selfService || !!activeProject) { + noChallengeMessage = selfService ? 'There are' : 'You have' + noChallengeMessage += ' no challenges at the moment' + } else { + noChallengeMessage = 'Please select a project to view challenges' + } + return (
- { - activeProject - ? (

You have no challenges at the moment

) : (

Please select a project to view challenges

) - } +

{noChallengeMessage}

) } @@ -24,7 +32,8 @@ NoChallenge.propTypes = { activeProject: PropTypes.shape({ id: PropTypes.number, name: PropTypes.string - }) + }), + selfService: PropTypes.bool } export default NoChallenge diff --git a/src/components/ChallengesComponent/index.js b/src/components/ChallengesComponent/index.js index 06966a26..82f34bb2 100644 --- a/src/components/ChallengesComponent/index.js +++ b/src/components/ChallengesComponent/index.js @@ -27,7 +27,9 @@ const ChallengesComponent = ({ totalChallenges, partiallyUpdateChallengeDetails, deleteChallenge, - isBillingAccountExpired + isBillingAccountExpired, + selfService, + auth }) => { return ( @@ -41,7 +43,7 @@ const ChallengesComponent = ({ __html: xss(activeProject ? activeProject.name : '') }} /> - { activeProject && activeProject.id && ( + {activeProject && activeProject.id && ( (View Project) @@ -76,6 +78,8 @@ const ChallengesComponent = ({ partiallyUpdateChallengeDetails={partiallyUpdateChallengeDetails} deleteChallenge={deleteChallenge} isBillingAccountExpired={isBillingAccountExpired} + selfService={selfService} + auth={auth} /> )}
@@ -101,7 +105,9 @@ ChallengesComponent.propTypes = { totalChallenges: PropTypes.number.isRequired, partiallyUpdateChallengeDetails: PropTypes.func.isRequired, deleteChallenge: PropTypes.func.isRequired, - isBillingAccountExpired: PropTypes.bool + isBillingAccountExpired: PropTypes.bool, + selfService: PropTypes.bool, + auth: PropTypes.object.isRequired } ChallengesComponent.defaultProps = { diff --git a/src/components/CopilotCard/index.js b/src/components/CopilotCard/index.js index af97c490..c34bcaa0 100644 --- a/src/components/CopilotCard/index.js +++ b/src/components/CopilotCard/index.js @@ -8,11 +8,12 @@ const assets = require.context('../../assets/images', false, /svg/) const CopilotCard = ({ copilot, selectedCopilot, onUpdateOthers }) => { const icon = './user.svg' + const copilotHandle = copilot.handle || copilot.memberHandle return ( -
onUpdateOthers({ field: 'copilot', value: copilot.handle })}> +
onUpdateOthers({ field: 'copilot', value: copilotHandle })}> {copilot.photoURL && copilot} {!copilot.photoURL && } - {copilot.handle} + {copilotHandle}
) } diff --git a/src/components/Sidebar/index.js b/src/components/Sidebar/index.js index 023f7fef..eb3cec34 100644 --- a/src/components/Sidebar/index.js +++ b/src/components/Sidebar/index.js @@ -11,7 +11,9 @@ import styles from './Sidebar.module.scss' import { isBetaMode } from '../../util/cookie' const Sidebar = ({ - projectId, resetSidebarActiveParams + projectId, + resetSidebarActiveParams, + selfService }) => { return (
@@ -21,12 +23,21 @@ const Sidebar = ({ {isBetaMode() && beta}
-
+
All Work
+ { + isBetaMode() && ( + +
+ Self-Service Opportunities +
+ + ) + } -
+
Give Application Feedback
@@ -40,7 +51,8 @@ const Sidebar = ({ Sidebar.propTypes = { projectId: PropTypes.string, - resetSidebarActiveParams: PropTypes.func + resetSidebarActiveParams: PropTypes.func, + selfService: PropTypes.bool } export default Sidebar diff --git a/src/config/constants.js b/src/config/constants.js index 58bcff97..b05d9e10 100644 --- a/src/config/constants.js +++ b/src/config/constants.js @@ -167,6 +167,7 @@ export const CHALLENGE_STATUS = { ACTIVE: 'ACTIVE', NEW: 'NEW', DRAFT: 'DRAFT', + APPROVED: 'APPROVED', COMPLETED: 'COMPLETED', CANCELLED: 'CANCELLED' } diff --git a/src/containers/ChallengeEditor/ChallengeEditor.module.scss b/src/containers/ChallengeEditor/ChallengeEditor.module.scss index 3a019247..4a837b17 100644 --- a/src/containers/ChallengeEditor/ChallengeEditor.module.scss +++ b/src/containers/ChallengeEditor/ChallengeEditor.module.scss @@ -25,4 +25,39 @@ display: flex; justify-content: center; align-items: center; +} + +.rejectChallengeContainer { + + .cancelReasonContainer { + margin-top: 25px; + width: 100%; + text-align: center; + + textarea.cancelReason { + width: 75%; + } + } + + .rejectButtonContainer { + display: flex; + margin: 0px 30px; + flex-direction: row; + justify-content: flex-end; + flex-shrink: 0; + width: 70%; + + button { + height: 40px; + margin-right: 30px; + + span { + padding: 0 20px; + } + } + + .button:last-of-type { + margin-right: 0; + } + } } \ No newline at end of file diff --git a/src/containers/ChallengeEditor/index.js b/src/containers/ChallengeEditor/index.js index 8345dd94..10cfe5fd 100644 --- a/src/containers/ChallengeEditor/index.js +++ b/src/containers/ChallengeEditor/index.js @@ -6,8 +6,9 @@ import moment from 'moment' import ChallengeEditorComponent from '../../components/ChallengeEditor' import ChallengeViewTabs from '../../components/ChallengeEditor/ChallengeViewTabs' import Loader from '../../components/Loader' -import { checkAdmin } from '../../util/tc' +import { checkAdmin, getResourceRoleByName } from '../../util/tc' import styles from './ChallengeEditor.module.scss' +import modalStyles from '../../components/Modal/ConfirmationModal.module.scss' import { loadTimelineTemplates, @@ -39,6 +40,9 @@ import { SUBMITTER_ROLE_UUID, MESSAGE } from '../../config/constants' import { patchChallenge } from '../../services/challenges' import ConfirmationModal from '../../components/Modal/ConfirmationModal' import AlertModal from '../../components/Modal/AlertModal' +import Modal from '../../components/Modal' +import PrimaryButton from '../../components/Buttons/PrimaryButton' +import OutlineButton from '../../components/Buttons/OutlineButton' const theme = { container: styles.modalContainer @@ -53,7 +57,9 @@ class ChallengeEditor extends Component { mountedWithCreatePage, isLaunching: false, showSuccessModal: false, - showLaunchModal: false + showLaunchModal: false, + showRejectModal: false, + cancelReason: null } this.onLaunchChallenge = this.onLaunchChallenge.bind(this) @@ -65,6 +71,12 @@ class ChallengeEditor extends Component { this.onCloseTask = this.onCloseTask.bind(this) this.closeTask = this.closeTask.bind(this) this.fetchProjectDetails = this.fetchProjectDetails.bind(this) + this.assignYourselfCopilot = this.assignYourselfCopilot.bind(this) + this.showRejectChallengeModal = this.showRejectChallengeModal.bind(this) + this.closeRejectModal = this.closeRejectModal.bind(this) + this.rejectChallenge = this.rejectChallenge.bind(this) + this.onChangeCancelReason = this.onChangeCancelReason.bind(this) + this.onApproveChallenge = this.onApproveChallenge.bind(this) } componentDidMount () { @@ -191,6 +203,20 @@ class ChallengeEditor extends Component { } } + async onApproveChallenge () { + const { partiallyUpdateChallengeDetails, challengeDetails } = this.props + const newStatus = 'Approved' + await partiallyUpdateChallengeDetails(challengeDetails.id, { + status: newStatus + }) + this.setState({ + challengeDetails: { + ...challengeDetails, + status: newStatus + } + }) + } + async cancelChallenge (challenge, cancelReason) { const { partiallyUpdateChallengeDetails, history } = this.props @@ -216,6 +242,10 @@ class ChallengeEditor extends Component { this.setState({ showSuccessModal: false }) } + closeRejectModal () { + this.setState({ showRejectModal: false }) + } + async activateChallenge () { const { partiallyUpdateChallengeDetails } = this.props if (this.state.isLaunching) return @@ -306,6 +336,54 @@ class ChallengeEditor extends Component { } } + async assignYourselfCopilot () { + const { challengeDetails, loggedInUser, metadata, replaceResourceInRole } = this.props + + // get the resource roles and new/old resource values + const copilotRole = getResourceRoleByName(metadata.resourceRoles, 'Copilot') + const approverRole = getResourceRoleByName(metadata.resourceRoles, 'Approver') + const screenerRole = getResourceRoleByName(metadata.resourceRoles, 'Primary Screener') + const copilotHandle = loggedInUser.handle + const challengeId = challengeDetails.id + const oldPilot = challengeDetails.legacy.selfServiceCopilot + const newPilot = oldPilot === copilotHandle ? null : copilotHandle + + // replace the roles + await replaceResourceInRole(challengeId, copilotRole.id, newPilot, oldPilot) + await replaceResourceInRole(challengeId, approverRole.id, newPilot, oldPilot) + await replaceResourceInRole(challengeId, screenerRole.id, newPilot, oldPilot) + + this.setState({ + challengeDetails: { + ...challengeDetails, + legacy: { + ...challengeDetails.legacy, + selfServiceCopilot: newPilot + } + } + }) + } + + showRejectChallengeModal () { + this.setState({ showRejectModal: true }) + } + + async rejectChallenge () { + const { challengeDetails } = this.props + const { cancelReason } = this.state + const partialChallenge = { + status: 'Cancelled - Requirements Infeasible', + cancelReason: cancelReason + } + const updatedChallenge = await patchChallenge(challengeDetails.id, partialChallenge) + this.setState({ challengeDetails: updatedChallenge }) + this.closeRejectModal() + } + + onChangeCancelReason (reason) { + this.setState({ cancelReason: reason }) + } + render () { const { match, @@ -330,7 +408,8 @@ class ChallengeEditor extends Component { deleteChallenge, loggedInUser, projectPhases, - isProjectPhasesLoading + isProjectPhasesLoading, + showRejectChallengeModal // members } = this.props const { @@ -340,7 +419,9 @@ class ChallengeEditor extends Component { showCloseTaskModal, showSuccessModal, suceessMessage, - challengeDetails + challengeDetails, + showRejectModal, + cancelReason } = this.state if (isProjectLoading || isLoading || isProjectPhasesLoading) return const challengeId = _.get(match.params, 'challengeId', null) @@ -392,11 +473,37 @@ class ChallengeEditor extends Component { onClose={this.closeSuccessModal} /> ) + const rejectModalContainerClasses = `${modalStyles.contentContainer} ${styles.rejectChallengeContainer}` + const rejectModal = ( + +
+
Reject Challenge
+ Please provide a reason for rejecting "{challengeDetails.name}?" +
+