diff --git a/src/actions/challenges.js b/src/actions/challenges.js index 3fd6399a..73855f2e 100644 --- a/src/actions/challenges.js +++ b/src/actions/challenges.js @@ -35,7 +35,9 @@ import { REMOVE_ATTACHMENT_FAILURE, REMOVE_ATTACHMENT_PENDING, REMOVE_ATTACHMENT_SUCCESS, - CREATE_CHALLENGE_RESOURCE, + CREATE_CHALLENGE_RESOURCE_PENDING, + CREATE_CHALLENGE_RESOURCE_SUCCESS, + CREATE_CHALLENGE_RESOURCE_FAILURE, DELETE_CHALLENGE_RESOURCE, PAGE_SIZE, UPDATE_CHALLENGE_DETAILS_PENDING, @@ -661,17 +663,50 @@ export function deleteResource (challengeId, roleId, memberHandle) { * @param {UUID} challengeId id of the challenge for which resource is to be created * @param {UUID} roleId id of the role, the resource should be in * @param {String} memberHandle handle of the resource + * @param {String} email email of member + * @param {String} userId id of member */ -export function createResource (challengeId, roleId, memberHandle) { +export function createResource (challengeId, roleId, memberHandle, email, userId) { const resource = { challengeId, roleId, memberHandle } - return (dispatch, getState) => { - return dispatch({ - type: CREATE_CHALLENGE_RESOURCE, - payload: createResourceAPI(resource) + return async (dispatch, getState) => { + dispatch({ + type: CREATE_CHALLENGE_RESOURCE_PENDING + }) + + let newResource + try { + newResource = await createResourceAPI(resource) + } catch (error) { + const errorMessage = _.get(error, 'response.data.message', 'Create resource fail.') + dispatch({ + type: CREATE_CHALLENGE_RESOURCE_FAILURE + }) + return { + success: false, + errorMessage + } + } + + let userEmail = email + if (!userEmail && userId) { + try { + const memberInfos = await searchProfilesByUserIds([userId]) + if (memberInfos.length > 0) { + userEmail = memberInfos[0].email + } + } catch (error) { + } + } + dispatch({ + type: CREATE_CHALLENGE_RESOURCE_SUCCESS, + payload: { + ...newResource, + email: userEmail + } }) } } diff --git a/src/assets/images/ico-trash.svg b/src/assets/images/ico-trash.svg new file mode 100644 index 00000000..4253c2a0 --- /dev/null +++ b/src/assets/images/ico-trash.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/ChallengeEditor/AssignedMember-Field/AssignedMember-Field.module.scss b/src/components/ChallengeEditor/AssignedMember-Field/AssignedMember-Field.module.scss index f9892657..e2030bea 100644 --- a/src/components/ChallengeEditor/AssignedMember-Field/AssignedMember-Field.module.scss +++ b/src/components/ChallengeEditor/AssignedMember-Field/AssignedMember-Field.module.scss @@ -24,12 +24,15 @@ } &.col1 { - max-width: 185px; - min-width: 185px; + width: 100px; margin-right: 14px; white-space: nowrap; display: flex; align-items: center; + + &.showAssignToMe { + width: 185px; + } } &.col2 { diff --git a/src/components/ChallengeEditor/AssignedMember-Field/index.js b/src/components/ChallengeEditor/AssignedMember-Field/index.js index cf52873f..8202e5af 100644 --- a/src/components/ChallengeEditor/AssignedMember-Field/index.js +++ b/src/components/ChallengeEditor/AssignedMember-Field/index.js @@ -7,7 +7,15 @@ import cn from 'classnames' import styles from './AssignedMember-Field.module.scss' import SelectUserAutocomplete from '../../SelectUserAutocomplete' -const AssignedMemberField = ({ challenge, onAssignSelf, onChange, assignedMemberDetails, readOnly }) => { +const AssignedMemberField = ({ + challenge, + onAssignSelf, + onChange, + assignedMemberDetails, + readOnly, + showAssignToMe, + label +}) => { const value = assignedMemberDetails ? { // if we know assigned member details, then show user `handle`, otherwise fallback to `userId` label: assignedMemberDetails.handle, @@ -16,8 +24,14 @@ const AssignedMemberField = ({ challenge, onAssignSelf, onChange, assignedMember return (
-
- +
+
{readOnly ? ( @@ -30,13 +44,13 @@ const AssignedMemberField = ({ challenge, onAssignSelf, onChange, assignedMember )}
{ - !readOnly && -
+ (!readOnly && showAssignToMe) + ? (
{ e.preventDefault() onAssignSelf() }}>Assign to me -
+
) : null }
) @@ -44,7 +58,9 @@ const AssignedMemberField = ({ challenge, onAssignSelf, onChange, assignedMember AssignedMemberField.defaultProps = { assignedMemberDetails: null, - readOnly: false + readOnly: false, + showAssignToMe: true, + label: 'Assigned Member' } AssignedMemberField.propTypes = { @@ -52,7 +68,9 @@ AssignedMemberField.propTypes = { onChange: PropTypes.func, assignedMemberDetails: PropTypes.shape(), readOnly: PropTypes.bool, - onAssignSelf: PropTypes.func + showAssignToMe: PropTypes.bool, + onAssignSelf: PropTypes.func, + label: PropTypes.string } export default AssignedMemberField diff --git a/src/components/ChallengeEditor/ChallengeViewTabs/index.js b/src/components/ChallengeEditor/ChallengeViewTabs/index.js index 10a56a55..c8cb0007 100644 --- a/src/components/ChallengeEditor/ChallengeViewTabs/index.js +++ b/src/components/ChallengeEditor/ChallengeViewTabs/index.js @@ -11,14 +11,15 @@ import ChallengeViewComponent from '../ChallengeView' import { PrimaryButton } from '../../Buttons' import LegacyLinks from '../../LegacyLinks' import ForumLink from '../../ForumLink' -import Registrants from '../Registrants' +import ResourcesTab from '../Resources' import Submissions from '../Submissions' -import { checkAdmin, checkReadOnlyRoles, getResourceRoleByName } from '../../../util/tc' +import { checkAdmin, checkEditResourceRoles, checkReadOnlyRoles } from '../../../util/tc' import { CHALLENGE_STATUS, MESSAGE } from '../../../config/constants' import Tooltip from '../../Tooltip' import CancelDropDown from '../Cancel-Dropdown' import 'react-tabs/style/react-tabs.css' import styles from './ChallengeViewTabs.module.scss' +import ResourcesAdd from '../ResourcesAdd' function getSelectorStyle (selectedView, currentView) { return cn(styles['challenge-selector-common'], { @@ -47,20 +48,23 @@ const ChallengeViewTabs = ({ assignYourselfCopilot, showRejectChallengeModal, loggedInUser, - onApproveChallenge + onApproveChallenge, + createResource, + deleteResource }) => { const [selectedTab, setSelectedTab] = useState(0) + const [showAddResourceModal, setShowAddResourceModal] = useState(false) + const { resourceRoles } = metadata const loggedInUserResource = useMemo( () => { if (!loggedInUser) { return null } - const loggedInUserResourceTmps = _.filter(challengeResources, { memberId: `${loggedInUser.userId}` }) + const loggedInUserResourceTmps = _.cloneDeep(_.filter(challengeResources, { memberId: `${loggedInUser.userId}` })) let loggedInUserResourceTmp = null if (loggedInUserResourceTmps.length > 0) { loggedInUserResourceTmp = loggedInUserResourceTmps[0] loggedInUserResourceTmp.resources = loggedInUserResourceTmps - const { resourceRoles } = metadata if (resourceRoles) { let roles = [] _.forEach(loggedInUserResourceTmps, resource => { @@ -74,37 +78,38 @@ const ChallengeViewTabs = ({ }, [loggedInUser, challengeResources, metadata] ) - - const registrants = useMemo(() => { - const { resourceRoles } = metadata - const role = getResourceRoleByName(resourceRoles, 'Submitter') - if (role && challengeResources) { - const registrantList = challengeResources.filter( - resource => resource.roleId === role.id + const canEditResource = useMemo( + () => { + return selectedTab === 1 && + ( + ( + loggedInUserResource && + checkEditResourceRoles(loggedInUserResource.roles) + ) || + checkAdmin(token) ) - // Add submission date to registrants - registrantList.forEach((r, i) => { - const submission = (challengeSubmissions || []).find(s => { - return '' + s.memberId === '' + r.memberId - }) - if (submission) { - registrantList[i].submissionDate = submission.created - } - }) - return registrantList - } else { - return [] - } - }, [metadata, challengeResources, challengeSubmissions]) + }, + [loggedInUserResource, token, selectedTab] + ) + + const allResources = useMemo(() => { + return challengeResources.map(rs => { + if (!rs.role) { + const roleInfo = _.find(resourceRoles, { id: rs.roleId }) + rs.role = roleInfo ? roleInfo.name : '' + } + return rs + }) + }, [metadata, challengeResources]) const submissions = useMemo(() => { return _.map(challengeSubmissions, s => { - s.registrant = _.find(registrants, r => { + s.registrant = _.find(allResources, r => { return +r.memberId === s.memberId }) return s }) - }, [challengeSubmissions, registrants]) + }, [challengeSubmissions, allResources]) const isTask = _.get(challenge, 'task.isTask', false) @@ -193,9 +198,14 @@ const ChallengeViewTabs = ({ )}
)} - {enableEdit && ( + {enableEdit && !canEditResource && ( )} + {canEditResource && ( + { + setShowAddResourceModal(true) + }} /> + )} {isSelfService && isDraft && (isAdmin || isSelfServiceCopilot || enableEdit) && (
)} - + {!canEditResource ? () : null}
@@ -223,22 +233,20 @@ const ChallengeViewTabs = ({ > DETAILS - {registrants.length ? ( - { - setSelectedTab(1) - }} - onKeyPress={e => { - setSelectedTab(1) - }} - className={getSelectorStyle(selectedTab, 1)} - > - REGISTRANTS ({registrants.length}) - - ) : null} + { + setSelectedTab(1) + }} + onKeyPress={e => { + setSelectedTab(1) + }} + className={getSelectorStyle(selectedTab, 1)} + > + RESOURCES + {challengeSubmissions.length ? ( )} {selectedTab === 1 && ( - + )} {selectedTab === 2 && ( )} + {showAddResourceModal ? ( setShowAddResourceModal(false)} + challenge={challenge} + loggedInUser={loggedInUser} + resourceRoles={resourceRoles} + createResource={createResource} + />) : null}
) } @@ -319,6 +341,8 @@ ChallengeViewTabs.propTypes = { onCloseTask: PropTypes.func, projectPhases: PropTypes.arrayOf(PropTypes.object), assignYourselfCopilot: PropTypes.func.isRequired, + createResource: PropTypes.func.isRequired, + deleteResource: PropTypes.func.isRequired, showRejectChallengeModal: PropTypes.func.isRequired, loggedInUser: PropTypes.object.isRequired, onApproveChallenge: PropTypes.func diff --git a/src/components/ChallengeEditor/Registrants/index.js b/src/components/ChallengeEditor/Registrants/index.js deleted file mode 100644 index a3eb804c..00000000 --- a/src/components/ChallengeEditor/Registrants/index.js +++ /dev/null @@ -1,578 +0,0 @@ -/* eslint jsx-a11y/no-static-element-interactions:0 */ -/** - * Registrants tab component. - */ - -import React from 'react' -import PT from 'prop-types' -import moment from 'moment' -import _ from 'lodash' -import cn from 'classnames' -import { getTCMemberURL } from '../../../config/constants' -import ReactSVG from 'react-svg' -import { getRatingLevel, sortList } from '../../../util/tc' -import styles from './Registrants.module.scss' - -const assets = require.context('../../../assets/images', false, /svg/) -const CheckMark = './check-mark.svg' -const ArrowDown = './arrow-down.svg' - -function formatDate (date) { - if (!date) return '-' - return moment(date) - .local() - .format('MMM DD, YYYY HH:mm') -} - -function getDate (arr, handle) { - const results = arr - .filter( - a => _.toString(a.createdBy || a.memberHandle) === _.toString(handle) - ) - .sort( - (a, b) => - new Date(b.submissionTime || b.submissionDate).getTime() - - new Date(a.submissionTime || a.submissionDate).getTime() - ) - return results[0] - ? results[0].submissionTime || results[0].submissionDate - : '' -} - -function passedCheckpoint (checkpoints, handle, results) { - const mine = checkpoints.filter( - c => _.toString(c.createdBy) === _.toString(handle) - ) - return _.some(mine, m => - _.find(results, r => r.submissionId === m.submissionId) - ) -} - -function getPlace (results, handle, places) { - const found = _.find( - results, - w => - _.toString(w.memberHandle) === _.toString(handle) && - w.placement <= places && - w.submissionStatus !== 'Failed Review' - ) - - if (found) { - return found.placement - } - return -1 -} - -export default class Registrants extends React.Component { - constructor (props, context) { - super(props, context) - - this.state = { - sortedRegistrants: [], - registrantsSort: { - field: '', - sort: '' - } - } - - this.getCheckPoint = this.getCheckPoint.bind(this) - this.getCheckPointDate = this.getCheckPointDate.bind(this) - this.getFlagFirstTry = this.getFlagFirstTry.bind(this) - this.sortRegistrants = this.sortRegistrants.bind(this) - this.getRegistrantsSortParam = this.getRegistrantsSortParam.bind(this) - this.updateSortedRegistrants = this.updateSortedRegistrants.bind(this) - this.onSortChange = this.onSortChange.bind(this) - } - - componentDidMount () { - this.updateSortedRegistrants() - } - - componentDidUpdate (prevProps) { - const { registrants, registrantsSort } = this.props - if ( - !_.isEqual(prevProps.registrants, registrants) || - !_.isEqual(prevProps.registrantsSort, registrantsSort) - ) { - this.updateSortedRegistrants() - } - } - onSortChange (sort) { - this.setState({ - registrantsSort: sort - }) - this.updateSortedRegistrants() - } - /** - * Get checkpoint date of registrant - */ - getCheckPointDate () { - const { challenge } = this.props - const checkpointPhase = (challenge.phases || []).find( - x => x.name === 'Checkpoint Submission' - ) - return moment( - checkpointPhase - ? checkpointPhase.actualEndDate || checkpointPhase.scheduledEndDate - : 0 - ) - } - - /** - * Get checkpoint of registrant - * @param {Object} registrant registrant info - */ - getCheckPoint (registrant) { - const { challenge } = this.props - const checkpoints = challenge.checkpoints || [] - const checkpointDate = this.getCheckPointDate() - - const twoRounds = - challenge.round1Introduction && challenge.round2Introduction - - let checkpoint - if (twoRounds) { - checkpoint = getDate(checkpoints, registrant.memberHandle) - if ( - !checkpoint && - moment(registrant.submissionDate).isBefore(checkpointDate) - ) { - checkpoint = registrant.submissionDate - } - } - - return checkpoint - } - - /** - * Get final of registrant - * @param {Object} registrant get checkpoint of registrant - */ - getFinal (registrant) { - let final - if (moment(registrant.submissionDate).isAfter(this.getCheckPointDate())) { - final = registrant.submissionDate - } - return final - } - - /** - * Check if it have flag for first try - * @param {Object} registrant registrant info - */ - getFlagFirstTry (registrant) { - const { notFoundCountryFlagUrl } = this.props - if ( - !registrant.countryInfo || - notFoundCountryFlagUrl[registrant.countryInfo.countryCode] - ) { - return null - } - - return registrant.countryInfo.countryFlag - } - - /** - * Get registrans sort parameter - */ - getRegistrantsSortParam () { - const { registrantsSort } = this.state - let { field, sort } = registrantsSort - if (!field) { - field = 'Registration Date' // default field for registrans sorting - } - if (!sort) { - sort = 'asc' // default order for registrans sorting - } - - return { - field, - sort - } - } - - /** - * Update sorted registrant array - */ - updateSortedRegistrants () { - const { registrants } = this.props - const sortedRegistrants = _.cloneDeep(registrants) - this.sortRegistrants(sortedRegistrants) - this.setState({ sortedRegistrants }) - } - - /** - * Sort array of registrant - * @param {Array} registrants array of registrant - */ - sortRegistrants (registrants) { - const { field, sort } = this.getRegistrantsSortParam() - return sortList(registrants, field, sort, (a, b) => { - let valueA = 0 - let valueB = 0 - let valueIsString = false - switch (field) { - case 'Country': { - valueA = a.countryCode - valueB = b.countryCode - valueIsString = true - break - } - case 'Rating': { - valueA = a.rating - valueB = b.rating - break - } - case 'Username': { - valueA = `${a.memberHandle}`.toLowerCase() - valueB = `${b.memberHandle}`.toLowerCase() - valueIsString = true - break - } - case 'Email': { - valueA = `${a.email}`.toLowerCase() - valueB = `${b.email}`.toLowerCase() - valueIsString = true - break - } - case 'Registration Date': { - valueA = new Date(a.created) - valueB = new Date(b.created) - break - } - case 'Round 1 Submitted Date': { - const checkpointA = this.getCheckPoint(a) - const checkpointB = this.getCheckPoint(b) - if (checkpointA) { - valueA = new Date(checkpointA) - } - if (checkpointB) { - valueB = new Date(checkpointB) - } - break - } - case 'Submitted Date': { - const checkpointA = this.getFinal(a) - const checkpointB = this.getFinal(b) - if (checkpointA) { - valueA = new Date(checkpointA) - } - if (checkpointB) { - valueB = new Date(checkpointB) - } - break - } - default: - } - - return { - valueA, - valueB, - valueIsString - } - }) - } - - render () { - const { challenge, checkpointResults, results } = this.props - const { prizeSets, track } = challenge - - const { sortedRegistrants } = this.state - const { field, sort } = this.getRegistrantsSortParam() - const revertSort = sort === 'desc' ? 'asc' : 'desc' - const isDesign = track.toLowerCase() === 'design' - - const placementPrizes = _.find(prizeSets, { type: 'placement' }) - const { prizes } = placementPrizes || [] - - const checkpoints = challenge.checkpoints || [] - - const twoRounds = - challenge.round1Introduction && challenge.round2Introduction - const places = (prizes && prizes.length) || 0 - console.log(styles) - return ( -
-
- {!isDesign && ( - - )} - - - - {twoRounds && ( - - )} - -
-
- {sortedRegistrants.map(r => { - const placement = getPlace(results, r.memberHandle, places) - let checkpoint = this.getCheckPoint(r) - if (checkpoint) { - checkpoint = formatDate(checkpoint) - } - const final = this.getFinal(r) - - return ( -
- {!isDesign && ( -
-
- Rating -
-
- - {!_.isNil(r.rating) && r.rating !== 0 ? r.rating : '-'} - -
-
- )} -
-
-
- Email -
- {r.email} -
-
-
- Registration Date -
- {formatDate(r.created)} -
- {twoRounds && ( -
-
- Round 1 Submitted Date -
-
- {checkpoint} - {passedCheckpoint( - checkpoints, - r.memberHandle, - checkpointResults - ) && ( - - )} -
-
- )} -
-
- {twoRounds ? 'Round 2 ' : ''} - Submitted Date -
-
- {formatDate(final)} - {placement > 0 && ( - - {placement} - - )} -
-
-
- ) - })} -
-
- ) - } -} - -Registrants.defaultProps = { - results: [], - checkpointResults: {}, - registrantsSort: {}, - notFoundCountryFlagUrl: {}, - onGetFlagImageFail: {}, - registrants: [] -} - -Registrants.propTypes = { - challenge: PT.shape({ - phases: PT.arrayOf( - PT.shape({ - actualEndDate: PT.string, - name: PT.string.isRequired, - scheduledEndDate: PT.string - }) - ).isRequired, - checkpoints: PT.arrayOf(PT.shape()), - subTrack: PT.any, - prizeSets: PT.arrayOf(PT.shape()).isRequired, - registrants: PT.arrayOf(PT.shape()).isRequired, - round1Introduction: PT.string, - round2Introduction: PT.string, - type: PT.string, - track: PT.string - }).isRequired, - results: PT.arrayOf(PT.shape()), - checkpointResults: PT.shape(), - registrants: PT.arrayOf(PT.shape()), - registrantsSort: PT.shape({ - field: PT.string, - sort: PT.string - }), - notFoundCountryFlagUrl: PT.objectOf(PT.bool).isRequired -} diff --git a/src/components/ChallengeEditor/Resources/index.js b/src/components/ChallengeEditor/Resources/index.js new file mode 100644 index 00000000..ac2db43f --- /dev/null +++ b/src/components/ChallengeEditor/Resources/index.js @@ -0,0 +1,491 @@ +/* eslint jsx-a11y/no-static-element-interactions:0 */ +/** + * Resources tab component. + */ + +import React from 'react' +import PT from 'prop-types' +import moment from 'moment' +import _ from 'lodash' +import cn from 'classnames' +import { getTCMemberURL, CHALLENGE_STATUS } from '../../../config/constants' +import ReactSVG from 'react-svg' +import { getRatingLevel, sortList } from '../../../util/tc' +import { getCurrentPhase } from '../../../util/phase' +import styles from './styles.module.scss' +import ResourcesDeleteModal from '../ResourcesDeleteModal' + +const assets = require.context('../../../assets/images', false, /svg/) +const ArrowDown = './arrow-down.svg' +const Trash = './ico-trash.svg' + +function getSelectorStyle (selectedView, currentView) { + return cn(styles['challenge-selector-common'], { + [styles['challenge-selected-view']]: selectedView === currentView, + [styles['challenge-unselected-view']]: selectedView !== currentView + }) +} + +function formatDate (date) { + if (!date) return '-' + return moment(date) + .local() + .format('MMM DD, YYYY HH:mm') +} + +const tabs = [ + { + name: 'All', + roles: null + }, + { + name: 'Submitters', + roles: ['submitter'] + }, + { + name: 'Reviewers', + roles: ['reviewer'] + }, + { + name: 'Managers, Copilots & Observers', + roles: ['manager', 'copilot', 'observer'] + } +] + +export default class Resources extends React.Component { + constructor (props, context) { + super(props, context) + + this.state = { + sortedResources: [], + resourcesSort: { + field: '', + sort: '' + }, + selectedTab: 0, + showDeleteResourceModal: null, + exceptionResourceIdDeleteList: {} + } + + this.sortResources = this.sortResources.bind(this) + this.getResourcesSortParam = this.getResourcesSortParam.bind(this) + this.updateSortedResources = this.updateSortedResources.bind(this) + this.updateExceptionHandlesDelete = this.updateExceptionHandlesDelete.bind(this) + this.onSortChange = this.onSortChange.bind(this) + this.setSelectedTab = this.setSelectedTab.bind(this) + } + + componentDidMount () { + this.updateSortedResources() + this.updateExceptionHandlesDelete() + } + + componentDidUpdate (prevProps) { + const { + resources, + resourcesSort, + submissions, + challenge, + loggedInUserResource + } = this.props + if ( + !_.isEqual(prevProps.resources, resources) || + !_.isEqual(prevProps.resourcesSort, resourcesSort) + ) { + this.updateSortedResources() + } + if ( + !_.isEqual(prevProps.submissions, submissions) || + !_.isEqual(prevProps.challenge, challenge) || + !_.isEqual(prevProps.resources, resources) || + !_.isEqual(prevProps.loggedInUserResource, loggedInUserResource) + ) { + this.updateExceptionHandlesDelete() + } + } + + onSortChange (sort) { + this.setState({ + resourcesSort: sort + }) + this.updateSortedResources() + } + + /** + * Get registrans sort parameter + */ + getResourcesSortParam () { + const { resourcesSort } = this.state + let { field, sort } = resourcesSort + if (!field) { + field = 'Registration Date' // default field for registrans sorting + } + if (!sort) { + sort = 'asc' // default order for registrans sorting + } + + return { + field, + sort + } + } + + /** + * Update sorted registrant array + */ + updateSortedResources () { + const { resources } = this.props + const { selectedTab } = this.state + const roles = tabs[selectedTab].roles + const sortedResources = _.cloneDeep(_.filter(resources, (rs) => { + if (!roles) { + return true + } + const matchRoles = _.filter(roles, role => `${rs.role}`.toLowerCase().indexOf(role) >= 0) + return matchRoles.length > 0 + })) + this.sortResources(sortedResources) + this.setState({ sortedResources }) + } + + /** + * Update exception handles delete + */ + updateExceptionHandlesDelete () { + const { + submissions, + challenge, + resources, + loggedInUserResource + } = this.props + const currentPhase = getCurrentPhase(challenge).toLowerCase() + const isCurrentPhasesNotSubmissionOrRegistration = _.every(['submission', 'registration'], (phase) => currentPhase.indexOf(phase) < 0) + const exceptionHandlesDeleteList = {} + // The creator of the challenge can't be deleted + exceptionHandlesDeleteList[challenge.createdBy] = true + _.forEach(submissions, (s) => { + // do not allow to delete submitters who submitted + exceptionHandlesDeleteList[s.createdBy] = true + }) + + const exceptionResourceIdDeleteList = {} + _.forEach(resources, (resourceItem) => { + if (exceptionHandlesDeleteList[resourceItem.memberHandle]) { + exceptionResourceIdDeleteList[resourceItem.id] = true + } + if ( + // if the challenge is in New or Draft status + // we will allow removing reviewers and copilots + _.some([ + CHALLENGE_STATUS.DRAFT, + CHALLENGE_STATUS.NEW + ], (status) => challenge.status.toUpperCase() === status) + ) { + if ( + // Copilots can't delete themselves from the challenge + loggedInUserResource && + _.some(loggedInUserResource.roles, (role) => `${role}`.toLowerCase().indexOf('copilot') >= 0) && + loggedInUserResource.memberHandle === resourceItem.memberHandle + ) { + exceptionResourceIdDeleteList[resourceItem.id] = true + } + } else if ( + // If the current phase is not submission or registration + // then we will disable removing reviewers and copilots. + _.some(['reviewer', 'copilot'], (role) => `${resourceItem.role}`.toLowerCase().indexOf(role) >= 0) && + isCurrentPhasesNotSubmissionOrRegistration + ) { + exceptionResourceIdDeleteList[resourceItem.id] = true + } + }) + this.setState({ exceptionResourceIdDeleteList }) + } + + /** + * Sort array of registrant + * @param {Array} resources array of registrant + */ + sortResources (resources) { + const { field, sort } = this.getResourcesSortParam() + return sortList(resources, field, sort, (a, b) => { + let valueA = 0 + let valueB = 0 + let valueIsString = false + switch (field) { + case 'Role': { + valueA = a.role + valueB = b.role + break + } + case 'Handle': { + valueA = `${a.memberHandle}`.toLowerCase() + valueB = `${b.memberHandle}`.toLowerCase() + valueIsString = true + break + } + case 'Email': { + valueA = `${a.email}`.toLowerCase() + valueB = `${b.email}`.toLowerCase() + valueIsString = true + break + } + case 'Registration Date': { + valueA = new Date(a.created) + valueB = new Date(b.created) + break + } + default: + } + + return { + valueA, + valueB, + valueIsString + } + }) + } + + setSelectedTab (selectedTab) { + const { resourcesSort } = this.state + this.setState({ selectedTab }) + + setTimeout(() => { + this.onSortChange(resourcesSort) + }) + } + + render () { + const { challenge, canEditResource, deleteResource } = this.props + const { track } = challenge + + const { sortedResources, selectedTab, showDeleteResourceModal, exceptionResourceIdDeleteList } = this.state + + const { field, sort } = this.getResourcesSortParam() + const revertSort = sort === 'desc' ? 'asc' : 'desc' + const isDesign = track.toLowerCase() === 'design' + + return ( +
+ +
+ + + + {!isDesign && ( + + )} + + + + + {canEditResource ? () : null} + + + + {sortedResources.map(r => { + return ( + + + + + + + {(canEditResource && !exceptionResourceIdDeleteList[r.id]) ? ( + ) : null} + + ) + })} + +
+ + + + + + + + + Actions +
+ {r.role} + + + + {r.memberHandle} + + + + {r.email} + + {formatDate(r.created)} + + +
+
+ {showDeleteResourceModal ? ( this.setState({ showDeleteResourceModal: null })} + resource={showDeleteResourceModal} + deleteResource={deleteResource} + />) : null} +
+ ) + } +} + +Resources.defaultProps = { + results: [], + checkpointResults: {}, + resourcesSort: {}, + submissions: [], + loggedInUserResource: null +} + +Resources.propTypes = { + challenge: PT.shape({ + phases: PT.arrayOf( + PT.shape({ + actualEndDate: PT.string, + name: PT.string.isRequired, + scheduledEndDate: PT.string + }) + ).isRequired, + checkpoints: PT.arrayOf(PT.shape()), + subTrack: PT.any, + prizeSets: PT.arrayOf(PT.shape()).isRequired, + resources: PT.arrayOf(PT.shape()).isRequired, + round1Introduction: PT.string, + round2Introduction: PT.string, + type: PT.string, + track: PT.string + }).isRequired, + submissions: PT.arrayOf(PT.shape()), + resources: PT.arrayOf(PT.shape()), + resourcesSort: PT.shape({ + field: PT.string, + sort: PT.string + }), + canEditResource: PT.bool.isRequired, + deleteResource: PT.func.isRequired, + loggedInUserResource: PT.any +} diff --git a/src/components/ChallengeEditor/Registrants/Registrants.module.scss b/src/components/ChallengeEditor/Resources/styles.module.scss similarity index 53% rename from src/components/ChallengeEditor/Registrants/Registrants.module.scss rename to src/components/ChallengeEditor/Resources/styles.module.scss index 2bcde7b0..b33d83fb 100644 --- a/src/components/ChallengeEditor/Registrants/Registrants.module.scss +++ b/src/components/ChallengeEditor/Resources/styles.module.scss @@ -14,56 +14,65 @@ $member-blue: #4c50d9; $member-yellow: #f2c900; $member-red: #ea1900; -.container { +$tc-dark-blue: #0681ff; +$tc-white: #FFFFFF; + +.containerTable { @include roboto; padding-bottom: 197px; + width: 100%; max-width: 966px; margin: 0 auto; margin-top: 50px; + overflow: auto; + + table { + width: 100%; + } @include xs-to-sm { padding: 0 30px 30px; } } -.head { +.headTable { font-weight: 500; font-size: 13px; line-height: 15px; padding-top: 13px; padding-bottom: 13px; color: $tc-gray-50; - display: flex; border-bottom: 1px solid $tc-gray-10; - gap: 20px; - - @include xs-to-sm { - display: none; - } -} + height: 43px; -.body { - @include xs-to-sm { - display: flex; - flex-direction: column; - align-items: center; + th { + padding-left: 20px; } } -.row { - display: flex; - gap: 20px; +.rowTable { line-height: 50px; color: $tc-gray-70; font-size: 15px; border-bottom: 1px solid $tc-gray-10; - @include xs-to-sm { - flex-direction: column; - padding-top: 15px; - padding-bottom: 15px; - width: 100%; + td { + padding-left: 20px; + + a, + button, + span { + white-space: nowrap; + overflow: hidden; + } + + button { + padding: 0; + border: none; + background-color: transparent; + outline: none; + } } } @@ -82,30 +91,6 @@ $member-red: #ea1900; font-weight: 500; } -.col-1, -.col-2, -.col-3, -.col-4, -.col-5, -.col-6, -.col-7 { - display: flex; - align-items: center; - border: none; - outline: none; - font: inherit; - color: inherit; - padding: 0; - - @include xs-to-sm { - padding-left: 0; - line-height: 20px; - width: auto; - flex-direction: column; - align-items: flex-start; - } -} - .col-arrow { display: flex; padding-left: 5px; @@ -132,124 +117,16 @@ $member-red: #ea1900; transform: scale(1, -1); } -.col-1 { - width: 150px; - padding-left: 20px; - color: $tc-gray-50; - - > span { - line-height: 15px; - - :global { - div { - display: flex; - justify-items: center; - height: 100%; - } - } - } - - @include xs-to-sm { - padding-left: 0; - margin-bottom: 10px; - line-height: 20px; - width: auto; - } -} - -.col-2 { - width: 100px; - flex-shrink: 0; - margin-left: 20px; - - @include xs-to-sm { - padding-left: 0; - margin-bottom: 10px; - line-height: 20px; - width: auto; - } -} - -.col-3 { - width: 100px; - flex-shrink: 0; - - @include xs-to-sm { - font-size: 15px; - line-height: 25px; - margin-bottom: 10px; - width: auto; - } - - a, - a:hover, - a:visited { - &:hover { - text-decoration: underline; - } - } -} - -.col-7 { - width: 200px; - margin-left: 20px; - flex-shrink: 0; - - span { - max-width: 100%; - text-overflow: ellipsis; - overflow: hidden; - } - - @include xs-to-sm { - padding-left: 0; - margin-bottom: 10px; - line-height: 20px; - width: auto; - } -} - -.col-4 { - width: 200px; - flex-shrink: 0; - - @include xs-to-sm { - margin-bottom: 15px; - line-height: 20px; - width: auto; - } -} - -.col-5 { - display: flex; - align-items: center; - - @include xs-to-sm { - margin-bottom: 15px; - line-height: 20px; - flex-direction: column; - align-items: flex-start; - width: auto; - } -} - -.col-6 { - display: flex; - align-items: center; - width: 200px; - flex-shrink: 0; - - @include xs-to-sm { - line-height: 20px; - flex-direction: column; - align-items: flex-start; - width: auto; - } -} - .table-header { cursor: pointer; background: transparent; + display: flex; + align-items: center; + border: none; + outline: none; + font: inherit; + color: inherit; + padding: 0; &:hover { color: #006ad7; @@ -298,24 +175,6 @@ $member-red: #ea1900; color: $tc-bronze-130; } -.design { - .col-3 { - width: 212px; - } - - .col-4 { - width: 231px; - } - - .col-5 { - width: 265px; - } - - .col-6 { - width: 258px; - } -} - .tooltip { font-size: 14px; margin: 10px 10px 0 10px; @@ -341,3 +200,40 @@ $member-red: #ea1900; .level-5 { color: $member-red !important; } + +.challenge-view-selector { + display: flex; + flex-wrap: wrap; + justify-content: center; + margin-top: 20px; + position: relative; + border-bottom: 1px solid silver; + text-transform: uppercase; + + @include xs-to-sm { + flex-wrap: nowrap; + justify-content: flex-start; + overflow: auto; + } + + .challenge-selector-common { + font-family: roboto, sans-serif; + font-size: 13px; + line-height: 30px; + margin: 10px 20px 0; + cursor: pointer; + white-space: nowrap; + } + + .challenge-selected-view { + font-weight: 700; + color: $tc-dark-blue; + border-bottom: 3px solid $tc-dark-blue; + } + + .challenge-unselected-view { + font-weight: 400; + color: $tc-gray-70; + border-bottom: 3px hidden $tc-white; + } +} diff --git a/src/components/ChallengeEditor/ResourcesAdd/index.js b/src/components/ChallengeEditor/ResourcesAdd/index.js new file mode 100644 index 00000000..2494960d --- /dev/null +++ b/src/components/ChallengeEditor/ResourcesAdd/index.js @@ -0,0 +1,136 @@ +import React, { useMemo, useState } from 'react' +import PropTypes from 'prop-types' +import cn from 'classnames' +import styles from './styles.module.scss' +import { PrimaryButton } from '../../Buttons' +import Modal from '../../Modal' +import AssignedMemberField from '../AssignedMember-Field' +import Select from '../../Select' +import _ from 'lodash' + +const theme = { + container: styles.modalContainer +} +const roles = [ + 'Reviewer', + 'Iterative Reviewer', + 'Observer', + 'Client Manager', + 'Primary Screener', + 'Final Reviewer', + 'Manager', + 'Copilot', + 'Checkpoint Screener', + 'Checkpoint Reviewer', + 'Specification Submitter', + 'Specification Reviewer' +] + +const ResourcesAdd = ({ + challenge, + onClose, + createResource, + loggedInUser, + resourceRoles +}) => { + const [assignedMemberDetails, setAssignedMemberDetails] = useState(null) + const [isCreatingResource, setIsCreatingResource] = useState(false) + const [selectedRole, setSelectedRole] = useState(null) + const [errorMessage, setErrorMessage] = useState(null) + + const roleOptions = useMemo( + () => + roles.map(r => { + const matchRole = _.find(resourceRoles, { name: r }) + return { + label: r, + value: matchRole ? matchRole.id : null + } + }), + [resourceRoles] + ) + return ( + +
+
Add Resource
+
+ { + if (option && option.value) { + setAssignedMemberDetails({ + handle: option.label, + userId: parseInt(option.value, 10) + }) + } else { + setAssignedMemberDetails(null) + } + }} + showAssignToMe={false} + label='Member' + /> + +
+
+ +
+
+