From 4dbd361e8d5e52302e86aaf6ba8f87de783731c8 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Wed, 14 Jun 2023 16:04:26 +1000 Subject: [PATCH 01/12] Pull more emails for >50 registrants https://github.com/topcoder-platform/work-manager/issues/1545 https://github.com/topcoder-platform/work-manager/issues/1544 --- .../Submissions/Submissions.module.scss | 161 +------ .../ChallengeEditor/Submissions/index.js | 450 +++++++++--------- src/services/challenges.js | 12 +- 3 files changed, 261 insertions(+), 362 deletions(-) diff --git a/src/components/ChallengeEditor/Submissions/Submissions.module.scss b/src/components/ChallengeEditor/Submissions/Submissions.module.scss index 67b0e0e8..59149d21 100644 --- a/src/components/ChallengeEditor/Submissions/Submissions.module.scss +++ b/src/components/ChallengeEditor/Submissions/Submissions.module.scss @@ -141,7 +141,7 @@ $base-unit: 5px; } } - .head { + .headTable { font-weight: 500; font-size: 15px; line-height: 15px; @@ -149,41 +149,20 @@ $base-unit: 5px; padding-top: 13px; color: $tc-gray-50; background: #e8e8e8; - display: flex; border-radius: 5px 5px 0 0; + height: 43px; - @include xs-to-sm { - display: none; + th { + padding-left: 20px; } - .col { - display: block !important; - color: $tc-gray-50; - - &.col-1 { - color: black; - } + * { + white-space: nowrap; } - } - - .sub-head { - font-weight: 500; - font-size: 13px; - line-height: 15px; - padding-bottom: 7px; - padding-top: 7px; - color: $tc-gray-50; - background: #faf9fa; - display: flex; - border-bottom: 1px solid $tc-gray-10; @include xs-to-sm { display: none; } - - .handle { - color: $tc-dark-blue-110; - } } .lock { @@ -233,45 +212,6 @@ $base-unit: 5px; .col { text-align: center; - - &.col-1 { - flex: 22; - display: flex; - - .col { - flex: 1; - } - } - - &.col-2 { - flex: 35; - display: flex; - color: $tc-gray-50; - padding-left: 10px; - - a { - color: black; - } - - .col { - flex: 1; - } - } - - &.col-3 { - flex: 45; - display: flex; - color: $tc-gray-50; - - .col { - flex: 1; - } - } - - &.col-4 { - flex: 20; - color: $tc-gray-50; - } } .handle { @@ -279,95 +219,27 @@ $base-unit: 5px; } &.non-mm { - .head { + .headTable { font-weight: 500; font-size: 13px; line-height: 15px; padding-bottom: 14px; color: $tc-gray-50; - display: flex; border-bottom: 1px solid $tc-gray-10; background: transparent; - gap: 20px; @include xs-to-sm { display: none; } } - .row { + .rowTable { color: $tc-gray-70; - display: flex; border-bottom: 1px solid $tc-gray-10; font-size: 15px; - gap: 20px; - } - - .col-1 { - display: flex; - padding-left: 30px; - text-align: left; - width: 10%; - justify-content: flex-start; - display: flex; - min-width: 140px; - - @include xs-to-sm { - min-width: none; - } - } - - .col-2 { - // width: 10%; - display: flex; - width: 50px; - margin-left: 20px; - justify-content: flex-start; - display: flex; - flex-shrink: 0; - } - - .col-3 { - display: flex; - width: 120px; - flex-shrink: 0; - } - - .col-9 { - display: flex; - width: 256px; - flex-shrink: 0; } - .col-4 { - display: flex; - width: 150px; - flex-shrink: 0; - } - - .col-5 { - display: flex; - width: 150px; - flex-shrink: 0; - } - - .col-6 { - display: flex; - width: 300px; - flex-shrink: 0; - } - - .col-7 { - display: flex; - width: 150px; - flex-shrink: 0; - } - - .col-8 { - display: flex; - width: 50px; - flex-shrink: 0; - + .col-8Table { button { padding: 0; border: none; @@ -382,12 +254,14 @@ $base-unit: 5px; } } -.submissionsContainer { - display: flex; - flex-direction: column; +.submissionsContainerTable { width: auto; max-width: 100%; overflow: auto; + + table { + width: 100%; + } } .tooltip { @@ -505,12 +379,13 @@ $base-unit: 5px; } } -.col-body { +.col-bodyTable { + padding-left: 20px; + a, button, span { white-space: nowrap; overflow: hidden; - text-overflow: ellipsis; } -} \ No newline at end of file +} diff --git a/src/components/ChallengeEditor/Submissions/index.js b/src/components/ChallengeEditor/Submissions/index.js index 173d6f5a..7aba7d92 100644 --- a/src/components/ChallengeEditor/Submissions/index.js +++ b/src/components/ChallengeEditor/Submissions/index.js @@ -327,231 +327,247 @@ class SubmissionsComponent extends React.Component {
{canDownloadSubmission ? (
) : null} -
-
- {!isF2F && !isBugHunt && ( - - )} - - - - -
- Submission ID (UUID) -
-
- Legacy submission ID -
- {canDownloadSubmission ? (
- Actions -
) : null} -
- {sortedSubmissions.map(s => { - const rating = s.registrant && !_.isNil(s.registrant.rating) - ? s.registrant.rating - : '-' - const memberHandle = _.get(s.registrant, 'memberHandle', '') - const email = _.get(s.registrant, 'email', '') - const submissionDate = moment(s.created).format('MMM DD, YYYY HH:mm') - return ( -
+
+ + + {!isF2F && !isBugHunt && ( -
- - {rating} - -
+ )} - -
- - {email} - -
-
- - {submissionDate} - -
- -
- - {s.id} - -
-
- - {s.legacySubmissionId} - -
- {canDownloadSubmission ? (
+ Username +
+ +
+ + +
+ + + + + {canDownloadSubmission ? () : null} + + + + {sortedSubmissions.map(s => { + const rating = s.registrant && !_.isNil(s.registrant.rating) + ? s.registrant.rating + : '-' + const memberHandle = _.get(s.registrant, 'memberHandle', '') + const email = _.get(s.registrant, 'email', '') + const submissionDate = moment(s.created).format('MMM DD, YYYY HH:mm') + return ( + + {!isF2F && !isBugHunt && ( + + )} + + + + + + + {canDownloadSubmission ? () : null} + + ) + })} + +
+ + + + - ) : null} - - ) - })} + + + + Submission ID (UUID) + + Legacy submission ID + + Actions +
+ + {rating} + + + + {memberHandle} + + + + {email} + + + + {submissionDate} + + + + {!_.isEmpty(s.review) && s.review[0].score + ? parseFloat(s.review[0].score).toFixed(2) + : 'N/A'} + ‌ ‌/ ‌ + {s.reviewSummation && s.reviewSummation[0].aggregateScore + ? parseFloat(s.reviewSummation[0].aggregateScore).toFixed(2) + : 'N/A'} + + + + {s.id} + + + + {s.legacySubmissionId} + + + +
{canDownloadSubmission ? (
diff --git a/src/services/challenges.js b/src/services/challenges.js index 51380f84..31e00f92 100644 --- a/src/services/challenges.js +++ b/src/services/challenges.js @@ -227,8 +227,16 @@ export async function createResource (resource) { * @returns {Promise<*>} */ export async function fetchResources (challengeId) { - const response = await axiosInstance.get(`${RESOURCES_API_URL}?challengeId=${challengeId}`) - return _.get(response, 'data', []) + let page = 0 + let totalPage = 1 + let resouces = [] + while (page < totalPage) { + page += 1 + const response = await axiosInstance.get(`${RESOURCES_API_URL}?challengeId=${challengeId}&page=${page}&perPage=5000`) + resouces = [...resouces, ..._.get(response, 'data', [])] + totalPage = parseInt(response.headers['x-total-pages']) + } + return resouces } /** From 1475da02dbdc7738cf0eba8a27e935a1e6b403d4 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Thu, 15 Jun 2023 13:41:49 +1000 Subject: [PATCH 02/12] Change Registrants tab to Resources tab / fix emails for large number of registrants --- .../ChallengeViewTabs/index.js | 45 +- .../ChallengeEditor/Registrants/index.js | 578 ------------------ .../ChallengeEditor/Resources/index.js | 386 ++++++++++++ .../styles.module.scss} | 247 +++----- src/services/user.js | 22 +- 5 files changed, 495 insertions(+), 783 deletions(-) delete mode 100644 src/components/ChallengeEditor/Registrants/index.js create mode 100644 src/components/ChallengeEditor/Resources/index.js rename src/components/ChallengeEditor/{Registrants/Registrants.module.scss => Resources/styles.module.scss} (53%) diff --git a/src/components/ChallengeEditor/ChallengeViewTabs/index.js b/src/components/ChallengeEditor/ChallengeViewTabs/index.js index 10a56a55..c82b4470 100644 --- a/src/components/ChallengeEditor/ChallengeViewTabs/index.js +++ b/src/components/ChallengeEditor/ChallengeViewTabs/index.js @@ -11,7 +11,7 @@ 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 { CHALLENGE_STATUS, MESSAGE } from '../../../config/constants' @@ -55,7 +55,7 @@ const ChallengeViewTabs = ({ if (!loggedInUser) { return null } - const loggedInUserResourceTmps = _.filter(challengeResources, { memberId: `${loggedInUser.userId}` }) + const loggedInUserResourceTmps = _.filter(_.cloneDeep(challengeResources), { memberId: `${loggedInUser.userId}` }) let loggedInUserResourceTmp = null if (loggedInUserResourceTmps.length > 0) { loggedInUserResourceTmp = loggedInUserResourceTmps[0] @@ -97,6 +97,15 @@ const ChallengeViewTabs = ({ } }, [metadata, challengeResources, challengeSubmissions]) + const allResources = useMemo(() => { + const { resourceRoles } = metadata + return challengeResources.map(rs => { + 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 => { @@ -223,22 +232,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 && ( _.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..23438b31 --- /dev/null +++ b/src/components/ChallengeEditor/Resources/index.js @@ -0,0 +1,386 @@ +/* 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 } from '../../../config/constants' +import ReactSVG from 'react-svg' +import { getRatingLevel, sortList } from '../../../util/tc' +import styles from './styles.module.scss' + +const assets = require.context('../../../assets/images', false, /svg/) +const ArrowDown = './arrow-down.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 + } + + this.sortResources = this.sortResources.bind(this) + this.getResourcesSortParam = this.getResourcesSortParam.bind(this) + this.updateSortedResources = this.updateSortedResources.bind(this) + this.onSortChange = this.onSortChange.bind(this) + this.setSelectedTab = this.setSelectedTab.bind(this) + } + + componentDidMount () { + this.updateSortedResources() + } + + componentDidUpdate (prevProps) { + const { resources, resourcesSort } = this.props + if ( + !_.isEqual(prevProps.resources, resources) || + !_.isEqual(prevProps.resourcesSort, resourcesSort) + ) { + this.updateSortedResources() + } + } + 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 }) + } + + /** + * 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 } = this.props + const { track } = challenge + + const { sortedResources, selectedTab } = this.state + + const { field, sort } = this.getResourcesSortParam() + const revertSort = sort === 'desc' ? 'asc' : 'desc' + const isDesign = track.toLowerCase() === 'design' + + return ( +
+ +
+ + + + {!isDesign && ( + + )} + + + + + + + {sortedResources.map(r => { + return ( + + + + + + + ) + })} + +
+ + + + + + + +
+ {r.role} + + + + {r.memberHandle} + + + + {r.email} + + {formatDate(r.created)} +
+
+
+ ) + } +} + +Resources.defaultProps = { + results: [], + checkpointResults: {}, + resourcesSort: {} +} + +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, + resources: PT.arrayOf(PT.shape()), + resourcesSort: PT.shape({ + field: PT.string, + sort: PT.string + }) +} 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..345ad833 100644 --- a/src/components/ChallengeEditor/Registrants/Registrants.module.scss +++ b/src/components/ChallengeEditor/Resources/styles.module.scss @@ -14,56 +14,58 @@ $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; + } } } @@ -82,30 +84,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 +110,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 +168,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 +193,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/services/user.js b/src/services/user.js index f483b0d9..dc396969 100644 --- a/src/services/user.js +++ b/src/services/user.js @@ -32,13 +32,21 @@ export async function searchProfiles (fields, queryObject = {}, limit) { * @returns {Promise<*>} */ export async function searchProfilesByUserIds (userIds, fields = 'userId,handle,firstName,lastName,email', limit) { - return searchProfiles( - fields, - { - userIds - }, - limit - ) + const chunkSize = 100 + let userInfos = [] + for (let i = 0; i < userIds.length; i += chunkSize) { + const chunkUserIds = userIds.slice(i, i + chunkSize) + const userInfosTmp = await searchProfiles( + fields, + { + userIds: chunkUserIds + }, + limit + ) + userInfos = [...userInfos, ...userInfosTmp] + } + + return userInfos } /** From a31fcc9107936eb9cdeb75b4aa64b5c6f639901b Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Thu, 15 Jun 2023 15:31:35 +1000 Subject: [PATCH 03/12] Fix chunk size for grabbing member emails --- .../ChallengeViewTabs/index.js | 28 ++----------------- src/services/user.js | 7 +++-- 2 files changed, 7 insertions(+), 28 deletions(-) diff --git a/src/components/ChallengeEditor/ChallengeViewTabs/index.js b/src/components/ChallengeEditor/ChallengeViewTabs/index.js index c82b4470..01bff12f 100644 --- a/src/components/ChallengeEditor/ChallengeViewTabs/index.js +++ b/src/components/ChallengeEditor/ChallengeViewTabs/index.js @@ -13,7 +13,7 @@ import LegacyLinks from '../../LegacyLinks' import ForumLink from '../../ForumLink' import ResourcesTab from '../Resources' import Submissions from '../Submissions' -import { checkAdmin, checkReadOnlyRoles, getResourceRoleByName } from '../../../util/tc' +import { checkAdmin, checkReadOnlyRoles } from '../../../util/tc' import { CHALLENGE_STATUS, MESSAGE } from '../../../config/constants' import Tooltip from '../../Tooltip' import CancelDropDown from '../Cancel-Dropdown' @@ -75,28 +75,6 @@ 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 - ) - // 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]) - const allResources = useMemo(() => { const { resourceRoles } = metadata return challengeResources.map(rs => { @@ -108,12 +86,12 @@ const ChallengeViewTabs = ({ 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) diff --git a/src/services/user.js b/src/services/user.js index dc396969..b52c72a1 100644 --- a/src/services/user.js +++ b/src/services/user.js @@ -32,10 +32,11 @@ export async function searchProfiles (fields, queryObject = {}, limit) { * @returns {Promise<*>} */ export async function searchProfilesByUserIds (userIds, fields = 'userId,handle,firstName,lastName,email', limit) { - const chunkSize = 100 + const chunkSize = 50 let userInfos = [] - for (let i = 0; i < userIds.length; i += chunkSize) { - const chunkUserIds = userIds.slice(i, i + chunkSize) + const uniqueUserIds = _.uniq(userIds) + for (let i = 0; i < uniqueUserIds.length; i += chunkSize) { + const chunkUserIds = uniqueUserIds.slice(i, i + chunkSize) const userInfosTmp = await searchProfiles( fields, { From 8f008921e698e1765fe7940a943220d6beca97de Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Tue, 20 Jun 2023 08:32:34 +1000 Subject: [PATCH 04/12] Allow for add / delete users on the Resources tab https://topcoder.atlassian.net/browse/PROD-4270 --- src/actions/challenges.js | 37 ++++- src/assets/images/ico-trash.svg | 3 + .../ChallengeViewTabs/index.js | 50 +++++-- .../ChallengeEditor/Resources/index.js | 36 ++++- .../Resources/styles.module.scss | 7 + .../ChallengeEditor/ResourcesAdd/index.js | 139 ++++++++++++++++++ .../ResourcesAdd/styles.module.scss | 101 +++++++++++++ .../ResourcesDeleteModal/index.js | 55 +++++++ .../ResourcesDeleteModal/styles.module.scss | 72 +++++++++ src/config/constants.js | 6 + src/containers/ChallengeEditor/index.js | 16 +- src/reducers/challenges.js | 2 - src/util/tc.js | 11 +- 13 files changed, 510 insertions(+), 25 deletions(-) create mode 100644 src/assets/images/ico-trash.svg create mode 100644 src/components/ChallengeEditor/ResourcesAdd/index.js create mode 100644 src/components/ChallengeEditor/ResourcesAdd/styles.module.scss create mode 100644 src/components/ChallengeEditor/ResourcesDeleteModal/index.js create mode 100644 src/components/ChallengeEditor/ResourcesDeleteModal/styles.module.scss diff --git a/src/actions/challenges.js b/src/actions/challenges.js index 3fd6399a..5f8fbbd5 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,18 +663,41 @@ 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 }) + + try { + const newResource = await createResourceAPI(resource) + let userEmail = email + if (!userEmail) { + const memberInfos = await searchProfilesByUserIds([userId]) + if (memberInfos.length > 0) { + userEmail = memberInfos[0].email + } + } + dispatch({ + type: CREATE_CHALLENGE_RESOURCE_SUCCESS, + payload: { + ...newResource, + email: userEmail + } + }) + } catch (error) { + dispatch({ + type: CREATE_CHALLENGE_RESOURCE_FAILURE + }) + } } } 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/ChallengeViewTabs/index.js b/src/components/ChallengeEditor/ChallengeViewTabs/index.js index 01bff12f..d8df64fa 100644 --- a/src/components/ChallengeEditor/ChallengeViewTabs/index.js +++ b/src/components/ChallengeEditor/ChallengeViewTabs/index.js @@ -13,12 +13,13 @@ import LegacyLinks from '../../LegacyLinks' import ForumLink from '../../ForumLink' import ResourcesTab from '../Resources' import Submissions from '../Submissions' -import { checkAdmin, checkReadOnlyRoles } 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,9 +48,13 @@ 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) { @@ -60,7 +65,6 @@ const ChallengeViewTabs = ({ if (loggedInUserResourceTmps.length > 0) { loggedInUserResourceTmp = loggedInUserResourceTmps[0] loggedInUserResourceTmp.resources = loggedInUserResourceTmps - const { resourceRoles } = metadata if (resourceRoles) { let roles = [] _.forEach(loggedInUserResourceTmps, resource => { @@ -74,12 +78,21 @@ const ChallengeViewTabs = ({ }, [loggedInUser, challengeResources, metadata] ) + const canEditResource = useMemo( + () => + ((loggedInUserResource && + checkEditResourceRoles(loggedInUserResource.roles)) || + checkAdmin(token)) && + selectedTab === 1, + [loggedInUserResource, token, selectedTab] + ) const allResources = useMemo(() => { - const { resourceRoles } = metadata return challengeResources.map(rs => { - const roleInfo = _.find(resourceRoles, { id: rs.roleId }) - rs.role = roleInfo ? roleInfo.name : '' + if (!rs.role) { + const roleInfo = _.find(resourceRoles, { id: rs.roleId }) + rs.role = roleInfo ? roleInfo.name : '' + } return rs }) }, [metadata, challengeResources]) @@ -180,9 +193,14 @@ const ChallengeViewTabs = ({ )}
)} - {enableEdit && ( + {enableEdit && !canEditResource && ( )} + {canEditResource && ( + { + setShowAddResourceModal(true) + }} /> + )} {isSelfService && isDraft && (isAdmin || isSelfServiceCopilot || enableEdit) && (
)} - + {!canEditResource ? () : null}
@@ -264,7 +282,12 @@ const ChallengeViewTabs = ({ /> )} {selectedTab === 1 && ( - + )} {selectedTab === 2 && ( )} + {showAddResourceModal ? ( setShowAddResourceModal(false)} + challenge={challenge} + loggedInUser={loggedInUser} + resourceRoles={resourceRoles} + createResource={createResource} + />) : null}
) } @@ -304,6 +334,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/Resources/index.js b/src/components/ChallengeEditor/Resources/index.js index 23438b31..716617b7 100644 --- a/src/components/ChallengeEditor/Resources/index.js +++ b/src/components/ChallengeEditor/Resources/index.js @@ -12,9 +12,11 @@ import { getTCMemberURL } from '../../../config/constants' import ReactSVG from 'react-svg' import { getRatingLevel, sortList } from '../../../util/tc' 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'], { @@ -59,7 +61,8 @@ export default class Resources extends React.Component { field: '', sort: '' }, - selectedTab: 0 + selectedTab: 0, + showDeleteResourceModal: null } this.sortResources = this.sortResources.bind(this) @@ -180,10 +183,10 @@ export default class Resources extends React.Component { } render () { - const { challenge } = this.props + const { challenge, canEditResource, deleteResource } = this.props const { track } = challenge - const { sortedResources, selectedTab } = this.state + const { sortedResources, selectedTab, showDeleteResourceModal } = this.state const { field, sort } = this.getResourcesSortParam() const revertSort = sort === 'desc' ? 'asc' : 'desc' @@ -312,6 +315,12 @@ export default class Resources extends React.Component {
+ + {canEditResource ? ( + Actions + ) : null} @@ -343,12 +352,29 @@ export default class Resources extends React.Component { {formatDate(r.created)} + + {canEditResource ? ( + + ) : null} ) })}
+ {showDeleteResourceModal ? ( this.setState({ showDeleteResourceModal: null })} + resource={showDeleteResourceModal} + deleteResource={deleteResource} + />) : null} ) } @@ -382,5 +408,7 @@ Resources.propTypes = { resourcesSort: PT.shape({ field: PT.string, sort: PT.string - }) + }), + canEditResource: PT.bool.isRequired, + deleteResource: PT.func.isRequired } diff --git a/src/components/ChallengeEditor/Resources/styles.module.scss b/src/components/ChallengeEditor/Resources/styles.module.scss index 345ad833..b33d83fb 100644 --- a/src/components/ChallengeEditor/Resources/styles.module.scss +++ b/src/components/ChallengeEditor/Resources/styles.module.scss @@ -66,6 +66,13 @@ $tc-white: #FFFFFF; white-space: nowrap; overflow: hidden; } + + button { + padding: 0; + border: none; + background-color: transparent; + outline: none; + } } } diff --git a/src/components/ChallengeEditor/ResourcesAdd/index.js b/src/components/ChallengeEditor/ResourcesAdd/index.js new file mode 100644 index 00000000..935e7d84 --- /dev/null +++ b/src/components/ChallengeEditor/ResourcesAdd/index.js @@ -0,0 +1,139 @@ +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 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) + } + }} + onAssignSelf={rs => { + const assignedMemberDetails = { + handle: loggedInUser.handle, + userId: loggedInUser.userId, + email: loggedInUser.email + } + + setAssignedMemberDetails({ + assignedMemberDetails + }) + }} + /> + +
+
+ +
+
+