From da863c34558beb5d3c6ca2abb8b964c944a8bfb7 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Tue, 25 Oct 2022 09:14:27 +1100 Subject: [PATCH 01/48] Enhance project screen https://github.com/topcoder-platform/work-manager/issues/1427 --- src/actions/challenges.js | 54 +++- src/assets/images/sort-icon.svg | 4 + .../ChallengeCard/ChallengeCard.module.scss | 211 +++---------- .../ChallengeCard/index.js | 143 +++------ .../ChallengeList/ChallengeList.module.scss | 125 +++++++- .../ChallengeList/index.js | 279 ++++++++++++++---- .../ChallengeTag/ChallengeTag.module.scss | 100 ++++--- .../ChallengesComponent/ChallengeTag/index.js | 41 +-- src/components/ChallengesComponent/index.js | 22 +- src/containers/Challenges/index.js | 35 ++- src/reducers/challenges.js | 8 + src/styles/_colors.scss | 8 + src/util/tc.js | 12 + 13 files changed, 618 insertions(+), 424 deletions(-) create mode 100644 src/assets/images/sort-icon.svg diff --git a/src/actions/challenges.js b/src/actions/challenges.js index 943cc558..f20ecd26 100644 --- a/src/actions/challenges.js +++ b/src/actions/challenges.js @@ -59,7 +59,18 @@ import { removeChallengeFromPhaseProduct, saveChallengeAsPhaseProduct } from '.. /** * Loads active challenges of project by page */ -export function loadChallengesByPage (page, projectId, status, filterChallengeName = null, selfService = false, userHandle = null) { +export function loadChallengesByPage ( + page, + projectId, + status, + filterChallengeName = null, + selfService = false, + userHandle = null, + filterChallengeType = {}, + filterDate = {}, + filterSortBy = null, + filterSortOrder = null +) { return (dispatch, getState) => { dispatch({ type: LOAD_CHALLENGES_PENDING, @@ -67,14 +78,39 @@ export function loadChallengesByPage (page, projectId, status, filterChallengeNa projectId: projectId, status, filterChallengeName, + filterChallengeType, + filterDate, + filterSortBy, + filterSortOrder, perPage: PAGE_SIZE, page }) const filters = { - sortBy: 'updated', + sortBy: 'startDate', sortOrder: 'desc' } + if (_.isObject(filterChallengeType) && filterChallengeType.value) { + filters['type'] = filterChallengeType.value + } + if (_.isObject(filterDate) && filterDate.startDateStart) { + filters['startDateStart'] = filterDate.startDateStart + } + if (_.isObject(filterDate) && filterDate.startDateEnd) { + filters['startDateEnd'] = filterDate.startDateEnd + } + if (_.isObject(filterDate) && filterDate.endDateStart) { + filters['endDateStart'] = filterDate.endDateStart + } + if (_.isObject(filterDate) && filterDate.endDateEnd) { + filters['endDateEnd'] = filterDate.endDateEnd + } + if (filterSortBy) { + filters['sortBy'] = filterSortBy + } + if (filterSortOrder) { + filters['sortOrder'] = filterSortOrder + } if (!_.isEmpty(filterChallengeName)) { filters['name'] = filterChallengeName } @@ -113,14 +149,24 @@ export function loadChallengesByPage (page, projectId, status, filterChallengeNa /** * Loads active challenges of project */ -export function loadChallenges (projectId, status, filterChallengeName = null) { +export function loadChallenges ( + projectId, + status, + filterChallengeName = null, + filterChallengeType = null, + filterSortBy, + filterSortOrder +) { return (dispatch, getState) => { dispatch({ type: LOAD_CHALLENGES_PENDING, challenges: [], projectId: projectId ? `${projectId}` : '', status, - filterChallengeName + filterChallengeName, + filterChallengeType, + filterSortBy, + filterSortOrder }) const filters = {} diff --git a/src/assets/images/sort-icon.svg b/src/assets/images/sort-icon.svg new file mode 100644 index 00000000..1defb57d --- /dev/null +++ b/src/assets/images/sort-icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/components/ChallengesComponent/ChallengeCard/ChallengeCard.module.scss b/src/components/ChallengesComponent/ChallengeCard/ChallengeCard.module.scss index 91f3213d..96134426 100644 --- a/src/components/ChallengesComponent/ChallengeCard/ChallengeCard.module.scss +++ b/src/components/ChallengesComponent/ChallengeCard/ChallengeCard.module.scss @@ -1,144 +1,59 @@ @import "../../../styles/includes"; .item { - width: 100%; display: flex; - justify-content: space-between; - font-size: 16px; + font-size: 14px; + gap: 30px; height: 100%; + padding: 0 20px; + .editingContainer { display: none; } .iconsContainer { display: flex; } - &:hover {@import "../../../styles/includes"; - - .item { - width: 100%; - display: flex; - justify-content: space-between; - font-size: 16px; - height: 100%; - &:hover { - cursor: pointer; - background-color: $lighter-gray; - } - - .block { - color: $text-color; - word-break: break-all; - } - - .light-text { - color: $light-text; - } - - .linkGroup { - display: flex; - justify-content: space-between; - width: 100%; - padding-left: 20px; - padding-right: 20px; - - .link, - .link:hover, - .link:visited { - color: $status-blue; - margin: 0; - text-decoration-line: underline; - } - - span.link, - span.link:hover, - span.link:visited { - color: $inactive; - cursor: default; - } - - &.onlyOne { - justify-content: center; - } - } - - - } - - .icon { - vertical-align: bottom; - } - - .faIcon { - color: $gray; - margin-right: 10px; - margin-left: 20px; - } - - .faIconContainer { - flex:1; - width: 100%; - display: flex; - justify-items: flex-end; - align-items: center; - - span { - flex-grow: 1; - flex-shrink: 0; - color: $text-color; - } - } - - .col1 { - flex: 6; - flex-wrap: nowrap; - display: flex; - text-decoration: none; - - .name { - flex:1; - display: flex; - flex-direction: column; - } - align-items: center; - padding-left: 20px; - } + .col1 { + display: flex; + flex: 1; + flex-direction: column; + justify-content: center; + } - .col2 { - flex: 2; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - text-decoration: none; - } + .col2 { + display: flex; + flex: 2; + flex-direction: column; + justify-content: center; + } - .col3 { - flex: 2; - display: flex; - flex-direction: column; - justify-content: center; - } + .col3 { + display: flex; + flex: 1; + flex-direction: column; + justify-content: center; + } - .col4 { - flex: 2; - display: flex; - flex-wrap: wrap; - width: 100%; - align-items: center; - padding-right: 20px; - justify-content: center; - } + .col4 { + display: flex; + width: 30px; + flex-direction: column; + justify-content: center; + } - cursor: pointer; - background-color: $lighter-gray; + .col5 { + display: flex; + width: 80px; + flex-direction: column; + justify-content: center; + } - .editingContainer { - display: flex; - } - .iconsContainer { - display: none; - } + .col6 { + display: flex; + width: 40px; + flex-direction: column; + justify-content: center; } .block { @@ -305,54 +220,6 @@ } } -.col1 { - flex: 6; - flex-wrap: nowrap; - display: flex; - - .name { - flex:1; - display: flex; - flex-direction: column; - } - - align-items: center; - padding-left: 20px; -} - -.col2 { - flex: 2; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - - .statusText { - color: $text-color; - font-size: 14px; - overflow-wrap: break-word; - word-wrap: break-word; - //hyphens: auto; - max-width: 100px; - } -} - -.col3 { - flex: 2; - display: flex; - flex-direction: column; - justify-content: center; -} - -.col4 { - flex: 2; - display: flex; - flex-wrap: wrap; - width: 100%; - align-items: center; - padding-right: 20px; -} - .modalContainer { padding: 0; position: fixed; diff --git a/src/components/ChallengesComponent/ChallengeCard/index.js b/src/components/ChallengesComponent/ChallengeCard/index.js index 80d5fc30..b5d99e62 100644 --- a/src/components/ChallengesComponent/ChallengeCard/index.js +++ b/src/components/ChallengesComponent/ChallengeCard/index.js @@ -8,12 +8,10 @@ import cn from 'classnames' import { withRouter, Link } from 'react-router-dom' import moment from 'moment' import 'moment-duration-format' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faFile, faUser } from '@fortawesome/free-solid-svg-icons' import ChallengeStatus from '../ChallengeStatus' import ChallengeTag from '../ChallengeTag' import styles from './ChallengeCard.module.scss' -import { getFormattedDuration, formatDate } from '../../../util/date' +import { formatDate } from '../../../util/date' import { CHALLENGE_STATUS, COMMUNITY_APP_URL, DIRECT_PROJECT_URL, MESSAGE, ONLINE_REVIEW_URL } from '../../../config/constants' import ConfirmationModal from '../../Modal/ConfirmationModal' import { checkChallengeEditPermission } from '../../../util/tc' @@ -24,76 +22,9 @@ const theme = { container: styles.modalContainer } -const STALLED_MSG = 'Stalled' -const DRAFT_MSG = 'In Draft' -const STALLED_TIME_LEFT_MSG = 'Challenge is currently on hold' -const FF_TIME_LEFT_MSG = 'Winner is working on fixes' - const PERMISSION_DELETE_MESSAGE_ERROR = "You don't have permission to delete this challenge" -/** - * Format the remaining time of a challenge phase - * @param phase Challenge phase - * @param status Challenge status - * @returns {*} - */ -const getTimeLeft = (phase, status) => { - if (!phase) return STALLED_TIME_LEFT_MSG - if (phase.phaseType === 'Final Fix') { - return FF_TIME_LEFT_MSG - } - let time = moment(phase.scheduledEndDate).diff() - const late = time < 0 - if (late) time = -time - - if (status !== CHALLENGE_STATUS.COMPLETED.toLowerCase()) { - const duration = getFormattedDuration(time) - return late ? `Late by ${duration}` : `${duration} to go` - } - - return moment(phase.scheduledEndDate).format('DD/MM/YYYY') -} - -/** - * Find current phase and remaining time of it - * @param c Challenge - * @returns {{phaseMessage: string, endTime: {late, text}}} - */ -const getPhaseInfo = (c) => { - const { currentPhaseNames, status, startDate, phases } = c - /* let checkPhases = (currentPhases && currentPhases.length > 0 ? currentPhases : allPhases) - if (_.isEmpty(checkPhases)) checkPhases = [] - let statusPhase = checkPhases - .filter(p => p.phaseType !== 'Registration') - .sort((a, b) => moment(a.scheduledEndTime).diff(b.scheduledEndTime))[0] - - if (!statusPhase && subTrack === 'FIRST_2_FINISH' && checkPhases.length) { - statusPhase = Object.clone(checkPhases[0]) - statusPhase.phaseType = 'Submission' - } */ - let phaseMessage = STALLED_MSG - // if (statusPhase) phaseMessage = statusPhase.phaseType - // else if (status === 'DRAFT') phaseMessage = DRAFT_MSG - var lowerStatus = status.toLowerCase() - if (lowerStatus === 'draft') { - phaseMessage = DRAFT_MSG - } else if (lowerStatus === 'active') { - if (!currentPhaseNames || currentPhaseNames.length === 0) { - var timeToStart = moment(startDate).diff() - if (timeToStart > 0) { - phaseMessage = `Scheduled in ${getFormattedDuration(timeToStart)}` - } - } else { - phaseMessage = currentPhaseNames.join('/') - } - } - const activePhases = phases.filter(p => !!p.isOpen) - const activePhase = activePhases.length > 0 ? activePhases[0] : null - const endTime = getTimeLeft(activePhase, lowerStatus) - return { phaseMessage, endTime } -} - /** * Render components when mouse hover * @param challenge @@ -177,15 +108,6 @@ const renderStatus = (status, getStatusText) => { } } -const renderLastUpdated = (challenge) => { - return ( - -
{formatDate(challenge.updated)}
-
{challenge.updatedBy}
- - ) -} - class ChallengeCard extends React.Component { constructor (props) { super(props) @@ -278,11 +200,12 @@ class ChallengeCard extends React.Component { render () { const { isLaunch, isConfirm, isSaving, isDeleteLaunch, isCheckChalengePermission, hasEditChallengePermission } = this.state - const { challenge, shouldShowCurrentPhase, reloadChallengeList, isBillingAccountExpired, disableHover, getStatusText } = this.props - const { phaseMessage, endTime } = getPhaseInfo(challenge) + const { challenge, reloadChallengeList, isBillingAccountExpired, disableHover, getStatusText, challengeTypes } = this.props const deleteMessage = isCheckChalengePermission ? 'Checking permissions...' : `Do you want to delete "${challenge.name}"?` + const directUrl = `${DIRECT_PROJECT_URL}/contest/detail?projectId=${challenge.legacyId}` + const communityAppUrl = `${COMMUNITY_APP_URL}/challenges/${challenge.id}` return (
@@ -318,7 +241,7 @@ class ChallengeCard extends React.Component { {isLaunch && isConfirm && ( )} - +
+ +
+ +
{challenge.name} - - {`Created by ${challenge.createdBy} at ${formatDate(challenge.created)}`}
- {renderLastUpdated(challenge)} - +
+ {formatDate(challenge.startDate)} +
+
+ {formatDate(challenge.endDate)} +
+
+ {challenge.numOfRegistrants} +
+
+ {challenge.numOfSubmissions} +
+
+ 0 +
+ {renderStatus(challenge.status.toUpperCase(), getStatusText)} - {shouldShowCurrentPhase && ( - {phaseMessage} - {endTime} - )} -
- {(disableHover ? View Challenge : hoverComponents(challenge, this.onUpdateLaunch, this.deleteModalLaunch))} +
+ {(disableHover ? Edit : hoverComponents(challenge, this.onUpdateLaunch, this.deleteModalLaunch))}
-
-
- - {challenge.numOfRegistrants || 0} -
-
- - {challenge.numOfSubmissions || 0} -
+
+ OR +
+
+ CA
) @@ -362,19 +293,19 @@ class ChallengeCard extends React.Component { } ChallengeCard.defaultPrps = { - shouldShowCurrentPhase: true, - reloadChallengeList: () => { } + reloadChallengeList: () => { }, + challengeTypes: [] } ChallengeCard.propTypes = { challenge: PropTypes.object, - shouldShowCurrentPhase: PropTypes.bool, reloadChallengeList: PropTypes.func, partiallyUpdateChallengeDetails: PropTypes.func.isRequired, deleteChallenge: PropTypes.func.isRequired, isBillingAccountExpired: PropTypes.bool, disableHover: PropTypes.bool, - getStatusText: PropTypes.func + getStatusText: PropTypes.func, + challengeTypes: PropTypes.arrayOf(PropTypes.shape()) } export default withRouter(ChallengeCard) diff --git a/src/components/ChallengesComponent/ChallengeList/ChallengeList.module.scss b/src/components/ChallengesComponent/ChallengeList/ChallengeList.module.scss index 428e1229..850a7461 100644 --- a/src/components/ChallengesComponent/ChallengeList/ChallengeList.module.scss +++ b/src/components/ChallengesComponent/ChallengeList/ChallengeList.module.scss @@ -34,6 +34,57 @@ .inactive { color: #BE405E; } + + .field { + @include upto-sm { + display: block; + padding-bottom: 10px; + } + + label { + @include roboto-bold(); + + font-size: 16px; + line-height: 19px; + font-weight: 500; + color: $tc-gray-80; + } + + &.input1 { + max-width: 185px; + min-width: 185px; + margin-right: 14px; + white-space: nowrap; + display: flex; + align-items: center; + flex-grow: 1; + + span { + color: $tc-red; + } + } + + &.input2.error { + color: $tc-red; + margin-top: -25px; + } + &.input2 { + align-self: flex-end; + width: 50%; + margin-bottom: auto; + margin-top: auto; + display: flex; + flex-direction: row; + max-width: 500px; + min-width: 500px; + } + + &.manageLink { + margin: 2px 12px; + text-decoration: none; + font-size: 12px; + } + } } .header { @@ -42,34 +93,61 @@ display: flex; justify-content: flex-start; padding: 0 20px; + font-size: 14px; + + gap: 30px; + + .sortable { + display: flex; + cursor: pointer; + } .col1 { display: flex; - justify-content: flex-start; - align-items: center; - flex: 6; + flex: 1; + flex-direction: column; + justify-content: center; } .col2 { display: flex; - justify-content: center; - align-items: center; flex: 2; + flex-direction: column; + justify-content: center; } .col3 { display: flex; - justify-content: flex-start; - align-items: center; - flex: 2; + flex: 1; + flex-direction: column; + justify-content: center; } .col4 { display: flex; - justify-content: flex-start; - align-items: center; - flex: 2; + width: 30px; + flex-direction: column; + justify-content: center; + } + + .col5 { + display: flex; + width: 80px; + flex-direction: column; + justify-content: center; } + + .col6 { + display: flex; + width: 40px; + flex-direction: column; + justify-content: center; + } +} + +.challengeInput { + width: 94% !important; + margin-left: -6px; } @-moz-document url-prefix() { @@ -115,10 +193,10 @@ .challengeItem { list-style: none; - min-height: 83px; width: 100%; border-top: 1px $gray solid; padding: 10px 0; + font-size: 14px; a { text-decoration: none; @@ -249,3 +327,26 @@ } } } + +.sortIcon { + width: 14px; + height: 12px; + margin-left: 5px; + margin-top: 2px; + height: 100%; + + &.asc { + transform: rotate(180deg); + } +} + +.filterItem { + display: flex; +} + +.to { + margin: 0 10px; + display: flex; + flex-direction: column; + justify-content: center; +} \ No newline at end of file diff --git a/src/components/ChallengesComponent/ChallengeList/index.js b/src/components/ChallengesComponent/ChallengeList/index.js index 5feec134..d78a3d58 100644 --- a/src/components/ChallengesComponent/ChallengeList/index.js +++ b/src/components/ChallengesComponent/ChallengeList/index.js @@ -1,11 +1,13 @@ /** * Component to render list of challenges */ -import { debounce, map } from 'lodash' +import _, { debounce, map } from 'lodash' import React, { Component } from 'react' import PropTypes from 'prop-types' import { DebounceInput } from 'react-debounce-input' -import { Tab, Tabs, TabList, TabPanel } from 'react-tabs' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faFile, faUser } from '@fortawesome/free-solid-svg-icons' +import DateTime from '@nateradebaugh/react-datetime' import Pagination from 'react-js-pagination' import cn from 'classnames' @@ -16,6 +18,8 @@ import styles from './ChallengeList.module.scss' import NoChallenge from '../NoChallenge' import ChallengeCard from '../ChallengeCard' import Message from '../Message' +import SortIcon from '../../../assets/images/sort-icon.svg' +import Select from '../../Select' import { CHALLENGE_STATUS @@ -33,7 +37,12 @@ class ChallengeList extends Component { super(props) this.state = { searchText: this.props.filterChallengeName, - errorMessage: null + errorMessage: null, + sortBy: this.props.filterSortBy || 'startDate', + sortOrder: this.props.filterSortOrder || 'desc', + challengeStatus: this.props.status, + challengeType: this.props.filterChallengeType, + challengeDate: this.props.filterDate } this.directUpdateSearchParam = this.updateSearchParam.bind(this) // update search param without debounce this.handlePageChange = this.handlePageChange.bind(this) // update search param without debounce @@ -41,6 +50,8 @@ class ChallengeList extends Component { this.hideError = this.hideError.bind(this) this.reloadChallengeList = this.reloadChallengeList.bind(this) this.updateSearchParam = debounce(this.updateSearchParam.bind(this), 1000) + this.updateSort = this.updateSort.bind(this) + this.update = debounce(this.updateSearchParam.bind(this), 1000) } /** @@ -48,11 +59,14 @@ class ChallengeList extends Component { * @param {String} searchText search text * @param {String} projectStatus project status */ - updateSearchParam (searchText, projectStatus) { - const { status, filterChallengeName, loadChallengesByPage, activeProjectId, selfService } = this.props - this.setState({ searchText }, () => { - if (status !== projectStatus || searchText !== filterChallengeName) { - loadChallengesByPage(1, activeProjectId, projectStatus, searchText, selfService, this.getHandle()) + updateSearchParam (searchText, projectStatus, challengeType = {}, challengeDate = {}) { + const { status, filterChallengeName, filterChallengeType, filterDate, loadChallengesByPage, activeProjectId, selfService } = this.props + this.setState({ searchText, challengeStatus: projectStatus, challengeType, challengeDate }, () => { + if (status !== projectStatus || + searchText !== filterChallengeName || + (challengeType || {}).value !== (filterChallengeType || {}).value || + !_.isEqual(filterDate, challengeDate)) { + loadChallengesByPage(1, activeProjectId, projectStatus, searchText, selfService, this.getHandle(), challengeType, challengeDate) } }) } @@ -112,8 +126,48 @@ class ChallengeList extends Component { return this.props.auth && this.props.auth.user ? this.props.auth.user.handle : null } + /** + * Hide error message + */ + updateSort (name) { + const { searchText, challengeType, sortBy, sortOrder, challengeDate } = this.state + const { page, activeProjectId, status, selfService, loadChallengesByPage } = this.props + let order = sortOrder === 'asc' ? 'desc' : 'asc' + + if (sortBy !== name) { + order = 'desc' + } + + loadChallengesByPage( + page, + activeProjectId, + status, + searchText, + selfService, + this.getHandle(), + challengeType, + challengeDate, + name, + order + ) + + this.setState({ + sortBy: name, + sortOrder: order + }) + } + render () { - const { searchText, errorMessage } = this.state + const { + searchText, + errorMessage, + sortBy, + sortOrder, + challengeStatus, + challengeType, + challengeDate + } = this.state + const { activeProject, warnMessage, @@ -129,12 +183,31 @@ class ChallengeList extends Component { billingEndDate, isBillingAccountLoadingFailed, isBillingAccountLoading, - selfService + selfService, + challengeTypes } = this.props if (warnMessage) { return } + const statusOptions = _.map(CHALLENGE_STATUS, (item) => ( + { + label: _.capitalize(item), + value: _.capitalize(item) + } + )) + + statusOptions.unshift({ + label: 'All Challenge Status', + value: null + }) + + const challengeTypesOptions = challengeTypes.map(item => ({ label: item.name, value: item.abbreviation })) + challengeTypesOptions.unshift({ + label: 'All Challenge Types', + value: null + }) + let selectedTab = 0 switch (status) { case CHALLENGE_STATUS.APPROVED: @@ -195,57 +268,84 @@ class ChallengeList extends Component { minLength={2} debounceTimeout={300} placeholder='Search Challenges' - onChange={(e) => this.updateSearchParam(e.target.value, status)} + onChange={(e) => this.updateSearchParam(e.target.value, status, challengeType, challengeDate)} value={searchText} />
- {activeProject && ( { - switch (index) { - case 0: { - this.directUpdateSearchParam(searchText, CHALLENGE_STATUS.ACTIVE) - break - } - case 1: { - const status = selfService ? CHALLENGE_STATUS.APPROVED : CHALLENGE_STATUS.NEW - this.directUpdateSearchParam(searchText, status) - break - } - case 2: { - this.directUpdateSearchParam(searchText, CHALLENGE_STATUS.DRAFT) - break - } - case 3: { - const status = selfService ? CHALLENGE_STATUS.NEW : CHALLENGE_STATUS.COMPLETED - this.directUpdateSearchParam(searchText, status) - break - } - case 4: { - this.directUpdateSearchParam(searchText, CHALLENGE_STATUS.CANCELLED) - break - } - } - }}> - { - selfService &&

Total Challenges: {totalChallenges}

- } - - {(selfService ? 'Assigned challenges' : 'Active')} - {(selfService ? 'Approved' : 'New')} - {this.getStatusTextFunc(selfService)(CHALLENGE_STATUS.DRAFT)} - {(!selfService && Completed)} - {(!selfService && Cancelled)} - { - selfService && checkAdmin(this.props.auth.token) && Draft - } - - - - -
)} +
+
+
+ +
+
+ this.updateSearchParam(searchText, challengeStatus, e, challengeDate)} + /> +
+
+ +
+
+ +
+
+ this.updateSearchParam(searchText, status, challengeType, { ...challengeDate, endDateStart: e })} + /> + + this.updateSearchParam(searchText, status, challengeType, { ...challengeDate, endDateEnd: e })} + /> +
+
+
{ challenges.length === 0 && ( 0 && (
-
Challenge Name
-
Last Updated
-
Status
- {(selectedTab === 0) && (
Current phase
)} -
 
+
+ Challenge Type +
+
this.updateSort('name')}> + + Challenge Name + { + sortBy === 'name' && ( + + ) + } + +
+
this.updateSort('startDate')}> + + Start Date + { + sortBy === 'startDate' && ( + + ) + } + +
+
this.updateSort('endDate')}> + + End Date + { + sortBy === 'endDate' && ( + + ) + } + +
+
+ + + +
+
+ +
+
+ Forums +
+
+ Status +
+
Edit
+
OR
+
CA
) } @@ -279,8 +424,9 @@ class ChallengeList extends Component { partiallyUpdateChallengeDetails={partiallyUpdateChallengeDetails} deleteChallenge={deleteChallenge} isBillingAccountExpired={isBillingAccountExpired} - disableHover={selfService} + disableHover getStatusText={this.getStatusTextFunc(selfService)} + challengeTypes={challengeTypes} /> ) @@ -317,6 +463,10 @@ ChallengeList.propTypes = { }), warnMessage: PropTypes.string, filterChallengeName: PropTypes.string, + filterChallengeType: PropTypes.shape(), + filterDate: PropTypes.shape(), + filterSortBy: PropTypes.string, + filterSortOrder: PropTypes.string, status: PropTypes.string, activeProjectId: PropTypes.number, loadChallengesByPage: PropTypes.func.isRequired, @@ -331,7 +481,8 @@ ChallengeList.propTypes = { isBillingAccountLoadingFailed: PropTypes.bool, isBillingAccountLoading: PropTypes.bool, selfService: PropTypes.bool, - auth: PropTypes.object.isRequired + auth: PropTypes.object.isRequired, + challengeTypes: PropTypes.arrayOf(PropTypes.shape()).isRequired } export default ChallengeList diff --git a/src/components/ChallengesComponent/ChallengeTag/ChallengeTag.module.scss b/src/components/ChallengesComponent/ChallengeTag/ChallengeTag.module.scss index 5b1a629e..026e5fc9 100644 --- a/src/components/ChallengesComponent/ChallengeTag/ChallengeTag.module.scss +++ b/src/components/ChallengesComponent/ChallengeTag/ChallengeTag.module.scss @@ -1,40 +1,66 @@ @import '../../../styles/includes'; -.tag { - display: inline-block; - padding: 0 15px; - border-radius: 3px; - background-color: $lighter-gray; - min-width: 54px; - text-align: center; - max-height: 22px; - height: 22px; - margin: 5px 0; - color: $dark-gray; - - span { - font-size: 15px; - line-height: 22px; - vertical-align: top; +$base-unit: 5px; +$track-space-10: $base-unit * 2; +$track-space-15: $base-unit * 3; +$track-space-20: $base-unit * 4; +$track-code-pad: ($base-unit * 2) - 2; +$corner-radius: 2px; + +.trackIcon { + display: flex; + width: $base-unit * 6 + 2; + height: $base-unit * 6 + 2; + margin-right: $track-space-20; + flex-direction: column; + + .mainIcon { + @include tc-label-md; + + text-align: center; + line-height: $track-space-15; + color: $white; + padding: $track-code-pad - 1 0 $track-code-pad; + border-radius: 4px; + cursor: pointer; + width: 100%; + height: 100%; + + &.CH { + background: $tc-green-40; + } + + &.F2F { + background: $tc-blue-30; + } + + &.TSK { + background: $tc-turquose-30; + } + + &.MM { + background: $track-code-red; + } + + &.RDM { + background: $track-code-purple; + } + + &.SKL { + background: $track-code-purplish; + } + + &.SRM { + background: $track-code-yellow; + } + + &.PC { + background: $track-code-grey; + } + + &.withTco { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + } } -} - -.dataScience { - color: $white; - background-color: $orange; -} - -.development { - color: $white; - background-color: $green; -} - -.qa { - color: $white; - background-color: $green; -} - -.design { - color: $white; - background-color: $light-blue; -} +} \ No newline at end of file diff --git a/src/components/ChallengesComponent/ChallengeTag/index.js b/src/components/ChallengesComponent/ChallengeTag/index.js index e5d73e5f..0522b98e 100644 --- a/src/components/ChallengesComponent/ChallengeTag/index.js +++ b/src/components/ChallengesComponent/ChallengeTag/index.js @@ -1,30 +1,33 @@ import React from 'react' import PropTypes from 'prop-types' -import cn from 'classnames' -import styles from './ChallengeTag.module.scss' - -import { CHALLENGE_TRACKS } from '../../../config/constants' -const ChallengeTag = ({ track, challengeType }) => { - const className = cn(styles.tag, { - [styles.dataScience]: track === CHALLENGE_TRACKS.DATA_SCIENCE, - [styles.development]: track === CHALLENGE_TRACKS.DEVELOP, - [styles.design]: track === CHALLENGE_TRACKS.DESIGN, - [styles.qa]: track === CHALLENGE_TRACKS.QA - }) +import styles from './ChallengeTag.module.scss' +import { getChallengeTypeAbbr } from '../../../util/tc' +export default function ChallengeTag ({ + type, challengeTypes +}) { + let abbreviation = getChallengeTypeAbbr(type, challengeTypes) + if (['CH', 'F2F', 'TSK', 'MM', 'RDM', 'SKL', 'SRM', 'PC'].indexOf(abbreviation) < 0) { + abbreviation = '' + } return ( -
-
- {challengeType} + +
+ {abbreviation === 'PC' ? 'P' : abbreviation}
-
+ ) } -ChallengeTag.propTypes = { - track: PropTypes.string, - challengeType: PropTypes.string +ChallengeTag.defaultProps = { + type: 'Development', + challengeTypes: [] } -export default ChallengeTag +ChallengeTag.propTypes = { + type: PropTypes.string, + challengeTypes: PropTypes.arrayOf(PropTypes.shape()) +} diff --git a/src/components/ChallengesComponent/index.js b/src/components/ChallengesComponent/index.js index 291376db..30ac0cfb 100644 --- a/src/components/ChallengesComponent/index.js +++ b/src/components/ChallengesComponent/index.js @@ -18,6 +18,10 @@ const ChallengesComponent = ({ isLoading, warnMessage, filterChallengeName, + filterChallengeType, + filterDate, + filterSortBy, + filterSortOrder, activeProject, status, loadChallengesByPage, @@ -33,7 +37,8 @@ const ChallengesComponent = ({ isBillingAccountLoadingFailed, isBillingAccountLoading, selfService, - auth + auth, + challengeTypes }) => { return ( @@ -73,6 +78,10 @@ const ChallengesComponent = ({ warnMessage={warnMessage} activeProject={activeProject} filterChallengeName={filterChallengeName} + filterChallengeType={filterChallengeType} + filterDate={filterDate} + filterSortBy={filterSortBy} + filterSortOrder={filterSortOrder} status={status} loadChallengesByPage={loadChallengesByPage} activeProjectId={activeProjectId} @@ -88,6 +97,7 @@ const ChallengesComponent = ({ isBillingAccountLoading={isBillingAccountLoading} selfService={selfService} auth={auth} + challengeTypes={challengeTypes} /> )}
@@ -105,6 +115,10 @@ ChallengesComponent.propTypes = { isLoading: PropTypes.bool, warnMessage: PropTypes.string, filterChallengeName: PropTypes.string, + filterChallengeType: PropTypes.shape(), + filterDate: PropTypes.shape(), + filterSortBy: PropTypes.string, + filterSortOrder: PropTypes.string, status: PropTypes.string, activeProjectId: PropTypes.number, loadChallengesByPage: PropTypes.func.isRequired, @@ -119,12 +133,14 @@ ChallengesComponent.propTypes = { isBillingAccountLoadingFailed: PropTypes.bool, isBillingAccountLoading: PropTypes.bool, selfService: PropTypes.bool, - auth: PropTypes.object.isRequired + auth: PropTypes.object.isRequired, + challengeTypes: PropTypes.arrayOf(PropTypes.shape()) } ChallengesComponent.defaultProps = { challenges: [], - isLoading: true + isLoading: true, + challengeTypes: [] } export default ChallengesComponent diff --git a/src/containers/Challenges/index.js b/src/containers/Challenges/index.js index 6a12f204..9461149c 100644 --- a/src/containers/Challenges/index.js +++ b/src/containers/Challenges/index.js @@ -10,10 +10,9 @@ import { DebounceInput } from 'react-debounce-input' import ChallengesComponent from '../../components/ChallengesComponent' import ProjectCard from '../../components/ProjectCard' import Loader from '../../components/Loader' -import { loadChallengesByPage, partiallyUpdateChallengeDetails, deleteChallenge } from '../../actions/challenges' +import { loadChallengesByPage, partiallyUpdateChallengeDetails, deleteChallenge, loadChallengeTypes } from '../../actions/challenges' import { loadProject } from '../../actions/projects' import { loadProjects, setActiveProject, resetSidebarActiveParams } from '../../actions/sidebar' -import { CHALLENGE_STATUS } from '../../config/constants' import styles from './Challenges.module.scss' import { checkAdmin } from '../../util/tc' @@ -29,7 +28,8 @@ class Challenges extends Component { } componentDidMount () { - const { activeProjectId, resetSidebarActiveParams, menu, projectId, selfService } = this.props + const { activeProjectId, resetSidebarActiveParams, menu, projectId, selfService, loadChallengeTypes } = this.props + loadChallengeTypes() if (menu === 'NULL' && activeProjectId !== -1) { resetSidebarActiveParams() } else if (projectId || selfService) { @@ -51,7 +51,7 @@ class Challenges extends Component { const { activeProjectId, projectDetail: reduxProjectInfo, projectId, challengeProjectId, loadProject, selfService } = props if (activeProjectId !== challengeProjectId || selfService) { const isAdmin = checkAdmin(this.props.auth.token) - this.props.loadChallengesByPage(1, projectId ? parseInt(projectId) : -1, CHALLENGE_STATUS.ACTIVE, '', selfService, isAdmin ? null : this.props.auth.user.handle) + this.props.loadChallengesByPage(1, projectId ? parseInt(projectId) : -1, '', '', selfService, isAdmin ? null : this.props.auth.user.handle) const projectLoading = window.localStorage.getItem('projectLoading') !== null if (!selfService && (!reduxProjectInfo || `${reduxProjectInfo.id}` !== projectId) && !projectLoading ) { @@ -79,6 +79,10 @@ class Challenges extends Component { isLoading, warnMessage, filterChallengeName, + filterChallengeType, + filterDate, + filterSortBy, + filterSortOrder, projects, activeProjectId, status, @@ -96,9 +100,11 @@ class Challenges extends Component { isBillingAccountLoadingFailed, isBillingAccountLoading, selfService, - auth + auth, + metadata } = this.props const { searchProjectName, onlyMyProjects } = this.state + const { challengeTypes = [] } = metadata const projectInfo = _.find(projects, { id: activeProjectId }) || {} const projectComponents = projects.map(p => (
  • @@ -154,6 +160,10 @@ class Challenges extends Component { challenges={challenges} isLoading={isLoading} filterChallengeName={filterChallengeName} + filterChallengeType={filterChallengeType} + filterDate={filterDate} + filterSortBy={filterSortBy} + filterSortOrder={filterSortOrder} status={status} activeProjectId={activeProjectId} loadChallengesByPage={loadChallengesByPage} @@ -169,6 +179,7 @@ class Challenges extends Component { isBillingAccountLoading={isBillingAccountLoading} selfService={selfService} auth={auth} + challengeTypes={challengeTypes} /> } @@ -188,6 +199,10 @@ Challenges.propTypes = { activeProjectId: PropTypes.number, warnMessage: PropTypes.string, filterChallengeName: PropTypes.string, + filterChallengeType: PropTypes.shape(), + filterDate: PropTypes.shape(), + filterSortBy: PropTypes.string, + filterSortOrder: PropTypes.string, status: PropTypes.string, resetSidebarActiveParams: PropTypes.func, page: PropTypes.number.isRequired, @@ -203,7 +218,11 @@ Challenges.propTypes = { isBillingAccountLoadingFailed: PropTypes.bool, isBillingAccountLoading: PropTypes.bool, selfService: PropTypes.bool, - auth: PropTypes.object.isRequired + auth: PropTypes.object.isRequired, + loadChallengeTypes: PropTypes.func, + metadata: PropTypes.shape({ + challengeTypes: PropTypes.array + }) } const mapStateToProps = ({ challenges, sidebar, projects, auth }) => ({ @@ -217,7 +236,8 @@ const mapStateToProps = ({ challenges, sidebar, projects, auth }) => ({ billingEndDate: projects.billingEndDate, isBillingAccountLoadingFailed: projects.isBillingAccountLoadingFailed, isBillingAccountLoading: projects.isBillingAccountLoading, - auth: auth + auth: auth, + metadata: challenges.metadata }) const mapDispatchToProps = { @@ -225,6 +245,7 @@ const mapDispatchToProps = { resetSidebarActiveParams, loadProject, loadProjects, + loadChallengeTypes, setActiveProject, partiallyUpdateChallengeDetails, deleteChallenge diff --git a/src/reducers/challenges.js b/src/reducers/challenges.js index 71f85fda..cdd1fb09 100644 --- a/src/reducers/challenges.js +++ b/src/reducers/challenges.js @@ -47,6 +47,10 @@ const initialState = { attachments: [], challenge: null, filterChallengeName: '', + filterChallengeType: {}, + filterDate: {}, + filterSortBy: 'startDate', + filterSortOrder: 'desc', failedToDelete: false, status: '', perPage: 0, @@ -84,6 +88,10 @@ export default function (state = initialState, action) { projectId: action.projectId, status: action.status, filterChallengeName: action.filterChallengeName, + filterChallengeType: action.filterChallengeType, + filterDate: action.filterDate, + filterSortBy: action.filterSortBy, + filterSortOrder: action.filterSortOrder, perPage: action.perPage, page: action.page } diff --git a/src/styles/_colors.scss b/src/styles/_colors.scss index 30b36b34..b640d949 100644 --- a/src/styles/_colors.scss +++ b/src/styles/_colors.scss @@ -44,6 +44,8 @@ $tc-green-30: #60C602; $tc-green-40: #35AC35; $tc-green-50: #127D60; +$tc-turquose-30: #0ab88a; + $tc-red: #BE405E; $tc-gray-00: #FFFFFF; @@ -64,3 +66,9 @@ $tc-handle-gray: #9D9FA0; $tc-prize-bg: #EBEBEB; $tc-multi-select-icon-bg-color: #c6def1; + +$track-code-purplish: linear-gradient(83.58deg, #7b21a7 2.28%, #1974ad 97.67%); +$track-code-red: linear-gradient(84.92deg, #880152 2.08%, #be4a1d 97.43%); +$track-code-yellow: inear-gradient(90deg, #9f9900 0%, #3b890b 100%); +$track-code-purple: linear-gradient(90deg, #652385 0%, #8c384c 100%); +$track-code-grey: linear-gradient(265.38deg, #323232 1.99%, #8c8c8c 98.19%); \ No newline at end of file diff --git a/src/util/tc.js b/src/util/tc.js index 9760a5de..792a8602 100644 --- a/src/util/tc.js +++ b/src/util/tc.js @@ -244,3 +244,15 @@ export function getFinalScore (submission) { } return finalScore } + +/** + * Get challenge type abbreviation + * @param {Object} challenge challenge info + */ +export function getChallengeTypeAbbr (track, challengeTypes) { + const type = _.find(challengeTypes, { name: track }) + if (type) { + return type.abbreviation + } + return null +} From b4b9af3dea43cdfc4f66872f076e34091b94abc5 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Sun, 30 Oct 2022 17:26:15 +1100 Subject: [PATCH 02/48] =?UTF-8?q?Updates=20for=20navigation,=20layout,=20a?= =?UTF-8?q?nd=20new=20=E2=80=9CAll=20Work=E2=80=9D=20tab=20contents?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/actions/challenges.js | 49 +- src/assets/images/ico-arrow-down.svg | 3 + src/assets/images/nav-active-item.svg | 4 + src/components/App/index.js | 14 +- .../ChallengeCard/index.js | 8 +- .../ChallengeList/ChallengeList.module.scss | 6 +- .../ChallengeList/index.js | 555 +++++++++++++----- src/components/ChallengesComponent/index.js | 137 +++-- src/components/Tab/Tab.module.scss | 144 +++++ src/components/Tab/index.js | 75 +++ src/components/TopBar/Topbar.module.scss | 8 + src/components/TopBar/index.js | 2 + .../TwoRowsLayout.module.scss} | 22 +- .../{TwoColsLayout => TwoRowsLayout}/index.js | 28 +- src/containers/Challenges/index.js | 214 ++++--- src/containers/Tab/index.js | 121 ++++ src/reducers/challenges.js | 1 + src/routes.js | 25 +- 18 files changed, 1055 insertions(+), 361 deletions(-) create mode 100644 src/assets/images/ico-arrow-down.svg create mode 100644 src/assets/images/nav-active-item.svg create mode 100644 src/components/Tab/Tab.module.scss create mode 100644 src/components/Tab/index.js rename src/components/{TwoColsLayout/TwoColsLayout.module.scss => TwoRowsLayout/TwoRowsLayout.module.scss} (53%) rename src/components/{TwoColsLayout => TwoRowsLayout}/index.js (51%) create mode 100644 src/containers/Tab/index.js diff --git a/src/actions/challenges.js b/src/actions/challenges.js index f20ecd26..89753d42 100644 --- a/src/actions/challenges.js +++ b/src/actions/challenges.js @@ -72,19 +72,35 @@ export function loadChallengesByPage ( filterSortOrder = null ) { return (dispatch, getState) => { - dispatch({ - type: LOAD_CHALLENGES_PENDING, - challenges: [], - projectId: projectId, - status, - filterChallengeName, - filterChallengeType, - filterDate, - filterSortBy, - filterSortOrder, - perPage: PAGE_SIZE, - page - }) + if (_.isObject(projectId)) { + dispatch({ + type: LOAD_CHALLENGES_PENDING, + challenges: [], + status, + filterProjectOption: projectId, + filterChallengeName, + filterChallengeType, + filterDate, + filterSortBy, + filterSortOrder, + perPage: PAGE_SIZE, + page + }) + } else { + dispatch({ + type: LOAD_CHALLENGES_PENDING, + challenges: [], + status, + projectId, + filterChallengeName, + filterChallengeType, + filterDate, + filterSortBy, + filterSortOrder, + perPage: PAGE_SIZE, + page + }) + } const filters = { sortBy: 'startDate', @@ -116,8 +132,13 @@ export function loadChallengesByPage ( } if (_.isInteger(projectId) && projectId > 0) { filters['projectId'] = projectId + } else if (_.isObject(projectId) && projectId.value > 0) { + filters['projectId'] = projectId.value } - if (!_.isEmpty(status)) { + + if (status === 'all') { + delete filters['status'] + } else if (!_.isEmpty(status)) { filters['status'] = status === '' ? undefined : _.startCase(status.toLowerCase()) } else if (!(_.isInteger(projectId) && projectId > 0)) { filters['status'] = 'Active' diff --git a/src/assets/images/ico-arrow-down.svg b/src/assets/images/ico-arrow-down.svg new file mode 100644 index 00000000..3ba23d39 --- /dev/null +++ b/src/assets/images/ico-arrow-down.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/images/nav-active-item.svg b/src/assets/images/nav-active-item.svg new file mode 100644 index 00000000..5ff4ba65 --- /dev/null +++ b/src/assets/images/nav-active-item.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/components/App/index.js b/src/components/App/index.js index 316f2e65..60ba3703 100644 --- a/src/components/App/index.js +++ b/src/components/App/index.js @@ -2,19 +2,17 @@ * Component that sets the general structure of the app */ import React from 'react' -import TwoColsLayout from '../TwoColsLayout' +import TwoRowsLayout from '../TwoRowsLayout' const App = (content, topbar, sidebar) => () => { return ( - - - {sidebar} - - + + {topbar || null} + {sidebar} {content} - - + + ) } diff --git a/src/components/ChallengesComponent/ChallengeCard/index.js b/src/components/ChallengesComponent/ChallengeCard/index.js index b5d99e62..6d9e5b1d 100644 --- a/src/components/ChallengesComponent/ChallengeCard/index.js +++ b/src/components/ChallengesComponent/ChallengeCard/index.js @@ -200,7 +200,7 @@ class ChallengeCard extends React.Component { render () { const { isLaunch, isConfirm, isSaving, isDeleteLaunch, isCheckChalengePermission, hasEditChallengePermission } = this.state - const { challenge, reloadChallengeList, isBillingAccountExpired, disableHover, getStatusText, challengeTypes } = this.props + const { setActiveProject, challenge, reloadChallengeList, isBillingAccountExpired, disableHover, getStatusText, challengeTypes } = this.props const deleteMessage = isCheckChalengePermission ? 'Checking permissions...' : `Do you want to delete "${challenge.name}"?` @@ -255,7 +255,7 @@ class ChallengeCard extends React.Component { - + setActiveProject(parseInt(challenge.projectId))}>
    {challenge.name}
    @@ -294,13 +294,15 @@ class ChallengeCard extends React.Component { ChallengeCard.defaultPrps = { reloadChallengeList: () => { }, - challengeTypes: [] + challengeTypes: [], + setActiveProject: () => {} } ChallengeCard.propTypes = { challenge: PropTypes.object, reloadChallengeList: PropTypes.func, partiallyUpdateChallengeDetails: PropTypes.func.isRequired, + setActiveProject: PropTypes.func, deleteChallenge: PropTypes.func.isRequired, isBillingAccountExpired: PropTypes.bool, disableHover: PropTypes.bool, diff --git a/src/components/ChallengesComponent/ChallengeList/ChallengeList.module.scss b/src/components/ChallengesComponent/ChallengeList/ChallengeList.module.scss index 850a7461..597b8114 100644 --- a/src/components/ChallengesComponent/ChallengeList/ChallengeList.module.scss +++ b/src/components/ChallengesComponent/ChallengeList/ChallengeList.module.scss @@ -7,6 +7,10 @@ justify-content: space-between; } +.dashboardRow { + align-items: flex-end; +} + .row { display: flex; justify-content: flex-end; @@ -349,4 +353,4 @@ display: flex; flex-direction: column; justify-content: center; -} \ No newline at end of file +} diff --git a/src/components/ChallengesComponent/ChallengeList/index.js b/src/components/ChallengesComponent/ChallengeList/index.js index d78a3d58..270a0d91 100644 --- a/src/components/ChallengesComponent/ChallengeList/index.js +++ b/src/components/ChallengesComponent/ChallengeList/index.js @@ -21,9 +21,7 @@ import Message from '../Message' import SortIcon from '../../../assets/images/sort-icon.svg' import Select from '../../Select' -import { - CHALLENGE_STATUS -} from '../../../config/constants' +import { CHALLENGE_STATUS } from '../../../config/constants' import { checkAdmin } from '../../../util/tc' require('bootstrap/scss/bootstrap.scss') @@ -40,6 +38,7 @@ class ChallengeList extends Component { errorMessage: null, sortBy: this.props.filterSortBy || 'startDate', sortOrder: this.props.filterSortOrder || 'desc', + challengeProjectOption: this.props.filterProjectOption, challengeStatus: this.props.status, challengeType: this.props.filterChallengeType, challengeDate: this.props.filterDate @@ -59,16 +58,54 @@ class ChallengeList extends Component { * @param {String} searchText search text * @param {String} projectStatus project status */ - updateSearchParam (searchText, projectStatus, challengeType = {}, challengeDate = {}) { - const { status, filterChallengeName, filterChallengeType, filterDate, loadChallengesByPage, activeProjectId, selfService } = this.props - this.setState({ searchText, challengeStatus: projectStatus, challengeType, challengeDate }, () => { - if (status !== projectStatus || + updateSearchParam ( + searchText, + projectStatus, + challengeType = {}, + challengeDate = {}, + projectOption = {} + ) { + const { + dashboard, + status, + filterChallengeName, + filterChallengeType, + filterProjectOption, + filterDate, + loadChallengesByPage, + activeProjectId, + selfService + } = this.props + let projectId = dashboard ? projectOption : activeProjectId + this.setState( + { + searchText, + projectOption, + challengeStatus: projectStatus, + challengeType, + challengeDate + }, + () => { + if ( + status !== projectStatus || searchText !== filterChallengeName || + (projectOption || {}).value !== (filterProjectOption || {}).value || (challengeType || {}).value !== (filterChallengeType || {}).value || - !_.isEqual(filterDate, challengeDate)) { - loadChallengesByPage(1, activeProjectId, projectStatus, searchText, selfService, this.getHandle(), challengeType, challengeDate) + !_.isEqual(filterDate, challengeDate) + ) { + loadChallengesByPage( + 1, + projectId, + !projectStatus ? 'all' : projectStatus, + searchText, + selfService, + this.getHandle(), + challengeType, + challengeDate + ) + } } - }) + ) } /** @@ -77,9 +114,30 @@ class ChallengeList extends Component { */ handlePageChange (pageNumber) { const { searchText } = this.state - const { page, loadChallengesByPage, activeProjectId, status, selfService } = this.props + const { + page, + loadChallengesByPage, + activeProjectId, + dashboard, + filterProjectOption, + status, + selfService, + filterChallengeType, + filterDate + } = this.props + + let projectId = dashboard ? filterProjectOption : activeProjectId if (page !== pageNumber) { - loadChallengesByPage(pageNumber, activeProjectId, status, searchText, selfService, this.getHandle()) + loadChallengesByPage( + pageNumber, + projectId, + status, + searchText, + selfService, + this.getHandle(), + filterChallengeType, + filterDate + ) } } @@ -88,8 +146,21 @@ class ChallengeList extends Component { */ reloadChallengeList () { const { searchText } = this.state - const { page, loadChallengesByPage, activeProjectId, status, selfService } = this.props - loadChallengesByPage(page, activeProjectId, status, searchText, selfService, this.getHandle()) + const { + page, + loadChallengesByPage, + activeProjectId, + status, + selfService + } = this.props + loadChallengesByPage( + page, + activeProjectId, + status, + searchText, + selfService, + this.getHandle() + ) } /** @@ -109,7 +180,7 @@ class ChallengeList extends Component { getStatusTextFunc (selfService) { const draftText = selfService ? 'Waiting for approval' : 'Draft' - return (status) => { + return status => { switch (status) { case CHALLENGE_STATUS.DRAFT: return draftText @@ -123,24 +194,41 @@ class ChallengeList extends Component { if (checkAdmin(this.props.auth.token)) { return null } - return this.props.auth && this.props.auth.user ? this.props.auth.user.handle : null + return this.props.auth && this.props.auth.user + ? this.props.auth.user.handle + : null } /** * Hide error message */ updateSort (name) { - const { searchText, challengeType, sortBy, sortOrder, challengeDate } = this.state - const { page, activeProjectId, status, selfService, loadChallengesByPage } = this.props + const { + searchText, + challengeType, + sortBy, + sortOrder, + challengeDate + } = this.state + const { + page, + activeProjectId, + status, + dashboard, + filterProjectOption, + selfService, + loadChallengesByPage + } = this.props let order = sortOrder === 'asc' ? 'desc' : 'asc' if (sortBy !== name) { order = 'desc' } + let projectId = dashboard ? filterProjectOption : activeProjectId loadChallengesByPage( page, - activeProjectId, + projectId, status, searchText, selfService, @@ -163,6 +251,7 @@ class ChallengeList extends Component { errorMessage, sortBy, sortOrder, + challengeProjectOption, challengeStatus, challengeType, challengeDate @@ -174,11 +263,14 @@ class ChallengeList extends Component { challenges, status, page, + projects, + dashboard, perPage, totalChallenges, partiallyUpdateChallengeDetails, deleteChallenge, isBillingAccountExpired, + setActiveProject, billingStartDate, billingEndDate, isBillingAccountLoadingFailed, @@ -190,19 +282,20 @@ class ChallengeList extends Component { return } - const statusOptions = _.map(CHALLENGE_STATUS, (item) => ( - { - label: _.capitalize(item), - value: _.capitalize(item) - } - )) + const statusOptions = _.map(CHALLENGE_STATUS, item => ({ + label: _.capitalize(item), + value: _.capitalize(item) + })) statusOptions.unshift({ label: 'All Challenge Status', value: null }) - const challengeTypesOptions = challengeTypes.map(item => ({ label: item.name, value: item.abbreviation })) + const challengeTypesOptions = challengeTypes.map(item => ({ + label: item.name, + value: item.abbreviation + })) challengeTypesOptions.unshift({ label: 'All Challenge Types', value: null @@ -229,46 +322,118 @@ class ChallengeList extends Component { let warningModal = null if (errorMessage) { - warningModal = -
    -
    Error
    - {errorMessage} -
    -
    - + warningModal = ( + +
    +
    Error
    + {errorMessage} +
    +
    + +
    -
    - + + ) + } + let projectOptions + let projectOption + if (dashboard) { + projectOptions = projects.map(p => { + return { + label: p.name, + value: p.id + } + }) + projectOptions.unshift({ + label: 'All Projects', + value: -1 + }) + + let projectId = (challengeProjectOption && challengeProjectOption.value) || -1 + projectOption = projectOptions.find(p => p.value === projectId) } return (
    -
    - {!isBillingAccountLoading && !isBillingAccountLoadingFailed && !isBillingAccountExpired && ( + {dashboard &&

    Active Challenges

    } +
    + {!dashboard && + !isBillingAccountLoading && + !isBillingAccountLoadingFailed && + !isBillingAccountExpired && ( +
    + Billing Account: + ACTIVE  {' '} + Start Date:{' '} + {billingStartDate}  {' '} + End Date: {billingEndDate} +
    + )} + {!dashboard && + !isBillingAccountLoading && + !isBillingAccountLoadingFailed && + isBillingAccountExpired && (
    - Billing Account: ACTIVE   Start Date: {billingStartDate}   End Date: {billingEndDate} + Billing Account: + INACTIVE  {' '} + Start Date:{' '} + {billingStartDate}  {' '} + End Date: {billingEndDate}
    )} - {!isBillingAccountLoading && !isBillingAccountLoadingFailed && isBillingAccountExpired && ( + {!dashboard && + !isBillingAccountLoading && + isBillingAccountLoadingFailed && (
    - Billing Account: INACTIVE   Start Date: {billingStartDate}   End Date: {billingEndDate} + + Billing Account failed to load +
    )} - {!isBillingAccountLoading && isBillingAccountLoadingFailed && ( -
    Billing Account failed to load
    + {dashboard && ( +
    +
    + +
    +
    + - -
    - ) - } - { - activeProjectId === -1 && !selfService &&
    No project selected. Select one below
    - } - { - isLoading ? : ( -
      - {projectComponents} -
    - ) - } + )} + + +
    + )} + {!dashboard && activeProjectId === -1 && !selfService && ( +
    No project selected. Select one below
    + )} + {dashboard ? null : isLoading ? ( + + ) : ( +
      {projectComponents}
    + )}
    - {(activeProjectId !== -1 || selfService) && - } + {(dashboard || activeProjectId !== -1 || selfService) && ( + + )} ) } @@ -198,8 +252,9 @@ Challenges.propTypes = { projectId: PropTypes.string, activeProjectId: PropTypes.number, warnMessage: PropTypes.string, - filterChallengeName: PropTypes.string, filterChallengeType: PropTypes.shape(), + filterChallengeName: PropTypes.string, + filterProjectOption: PropTypes.shape(), filterDate: PropTypes.shape(), filterSortBy: PropTypes.string, filterSortOrder: PropTypes.string, @@ -218,6 +273,7 @@ Challenges.propTypes = { isBillingAccountLoadingFailed: PropTypes.bool, isBillingAccountLoading: PropTypes.bool, selfService: PropTypes.bool, + dashboard: PropTypes.bool, auth: PropTypes.object.isRequired, loadChallengeTypes: PropTypes.func, metadata: PropTypes.shape({ diff --git a/src/containers/Tab/index.js b/src/containers/Tab/index.js new file mode 100644 index 00000000..03cabd5e --- /dev/null +++ b/src/containers/Tab/index.js @@ -0,0 +1,121 @@ +import React, { Component } from 'react' +import { withRouter } from 'react-router-dom' +import PropTypes from 'prop-types' +import { connect } from 'react-redux' +import Tab from '../../components/Tab' +import { + loadProjects, + setActiveProject, + resetSidebarActiveParams, + unloadProjects +} from '../../actions/sidebar' + +class TabContainer extends Component { + constructor (props) { + super(props) + this.state = { + searchProjectName: '', + currentTab: 1 + } + this.updateProjectName = this.updateProjectName.bind(this) + this.onTabChange = this.onTabChange.bind(this) + } + + componentDidMount () { + const { projectId, activeProjectId, isLoading, selfService } = this.props + if (!projectId && activeProjectId === -1 && !isLoading && !selfService) { + this.props.loadProjects() + } + + if (projectId && activeProjectId < 0) { + this.props.setActiveProject(parseInt(projectId)) + } + } + + componentWillReceiveProps (nextProps) { + const { projectId, isLoading, selfService, projects } = nextProps + + if (nextProps.history.location.pathname === '/') { + this.setState({ currentTab: 1 }) + } else if (nextProps.history.location.pathname === '/projects') { + this.setState({ currentTab: 2 }) + } else { + this.setState({ currentTab: 0 }) + } + // if we're viewing a specific project, + // or we're viewing the self serve page, + // or if the project is already loading, + // don't load the projects + if (!!projectId || selfService || isLoading) { + // if we're not in the middle of loading, + // and we have projects to unload, + // unload them + if (!isLoading && !!projects && !!projects.length) { + this.props.unloadProjects() + } + + return + } + + // if we already have projects in the list, + // don't load the projects again + if (!!projects && !!projects.length) { + return + } + + // now it's okay to load the projects + this.props.loadProjects() + } + + updateProjectName (val) { + this.setState({ searchProjectName: val }) + this.props.loadProjects(val) + } + + onTabChange (tab) { + const { history, resetSidebarActiveParams } = this.props + if (tab === 1) { + history.push('/') + this.setState({ currentTab: 1 }) + } else if (tab === 2) { + history.push('/projects') + this.setState({ currentTab: 2 }) + } + + resetSidebarActiveParams() + } + + render () { + const { currentTab } = this.state + + return + } +} + +TabContainer.propTypes = { + projects: PropTypes.arrayOf(PropTypes.shape()), + isLoading: PropTypes.bool, + loadProjects: PropTypes.func, + unloadProjects: PropTypes.func, + activeProjectId: PropTypes.number, + history: PropTypes.any.isRequired, + setActiveProject: PropTypes.func, + projectId: PropTypes.string, + resetSidebarActiveParams: PropTypes.func, + selfService: PropTypes.bool +} + +const mapStateToProps = ({ sidebar }) => ({ + ...sidebar +}) + +const mapDispatchToProps = { + loadProjects, + unloadProjects, + setActiveProject, + resetSidebarActiveParams +} + +export default withRouter( + connect(mapStateToProps, mapDispatchToProps)(TabContainer) +) diff --git a/src/reducers/challenges.js b/src/reducers/challenges.js index cdd1fb09..48057aaf 100644 --- a/src/reducers/challenges.js +++ b/src/reducers/challenges.js @@ -88,6 +88,7 @@ export default function (state = initialState, action) { projectId: action.projectId, status: action.status, filterChallengeName: action.filterChallengeName, + filterProjectOption: action.filterProjectOption, filterChallengeType: action.filterChallengeType, filterDate: action.filterDate, filterSortBy: action.filterSortBy, diff --git a/src/routes.js b/src/routes.js index fbe0be96..d599b707 100644 --- a/src/routes.js +++ b/src/routes.js @@ -8,7 +8,7 @@ import _ from 'lodash' import { BETA_MODE_COOKIE_TAG } from './config/constants' import renderApp from './components/App' import TopBarContainer from './containers/TopbarContainer' -import Sidebar from './containers/Sidebar' +import Tab from './containers/Tab' import Challenges from './containers/Challenges' import ChallengeEditor from './containers/ChallengeEditor' import { getFreshToken, decodeToken } from 'tc-auth-lib' @@ -155,7 +155,7 @@ class Routes extends React.Component { render={() => renderApp( , , - + )()} /> @@ -163,23 +163,30 @@ class Routes extends React.Component { {isAllowed && renderApp( - , + , , - + + )()} + /> + renderApp( + , + , + )()} /> renderApp( , , - + )()} /> renderApp( , , - + )()} /> renderApp( , , - + )()} /> renderApp( - , + , , - + )()} /> {/* If path is not defined redirect to landing page */} From c12cffc78872c20399e5492e3d111d79b0d25bc0 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Mon, 31 Oct 2022 08:36:47 +1100 Subject: [PATCH 03/48] Change logo background --- src/components/TopBar/Topbar.module.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/TopBar/Topbar.module.scss b/src/components/TopBar/Topbar.module.scss index 232da5c0..4ada6f4e 100644 --- a/src/components/TopBar/Topbar.module.scss +++ b/src/components/TopBar/Topbar.module.scss @@ -29,7 +29,7 @@ .icon { vertical-align: middle; - color: $blue; + color: $white; margin-left: 20px; font-size: 20px; } From 690d3fd553fa60e8963b3b40434f1ff2cc111d98 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Mon, 31 Oct 2022 08:47:55 +1100 Subject: [PATCH 04/48] Clean up table --- src/components/ChallengesComponent/ChallengeList/index.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/ChallengesComponent/ChallengeList/index.js b/src/components/ChallengesComponent/ChallengeList/index.js index 270a0d91..20991c9c 100644 --- a/src/components/ChallengesComponent/ChallengeList/index.js +++ b/src/components/ChallengesComponent/ChallengeList/index.js @@ -594,7 +594,7 @@ class ChallengeList extends Component { {challenges.length > 0 && (
    - Challenge Type + Type
    Forums
    Status
    -
    Edit
    -
    OR
    -
    CA
    +
    +
    +
    )} {challenges.length > 0 && ( From adbc3faf076af47a5799ab502395de53fd426fe6 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Mon, 31 Oct 2022 08:49:50 +1100 Subject: [PATCH 05/48] Update page title to avoid confusion --- src/components/ChallengesComponent/ChallengeList/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ChallengesComponent/ChallengeList/index.js b/src/components/ChallengesComponent/ChallengeList/index.js index 20991c9c..c992e493 100644 --- a/src/components/ChallengesComponent/ChallengeList/index.js +++ b/src/components/ChallengesComponent/ChallengeList/index.js @@ -360,7 +360,7 @@ class ChallengeList extends Component { return (
    - {dashboard &&

    Active Challenges

    } + {dashboard &&

    My Challenges

    }
    {!dashboard && !isBillingAccountLoading && From 0870553ec11d7b8a21a46f3f348167f4035b4168 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Mon, 31 Oct 2022 10:07:07 +1100 Subject: [PATCH 06/48] Revert "Change logo background" This reverts commit c12cffc78872c20399e5492e3d111d79b0d25bc0. --- src/components/TopBar/Topbar.module.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/TopBar/Topbar.module.scss b/src/components/TopBar/Topbar.module.scss index 4ada6f4e..232da5c0 100644 --- a/src/components/TopBar/Topbar.module.scss +++ b/src/components/TopBar/Topbar.module.scss @@ -29,7 +29,7 @@ .icon { vertical-align: middle; - color: $white; + color: $blue; margin-left: 20px; font-size: 20px; } From b0e870a4b578f77cb954ca56924e4edd56b11638 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Tue, 1 Nov 2022 08:49:52 +1100 Subject: [PATCH 07/48] =?UTF-8?q?Remove=20=E2=80=9CMy=20Projects=E2=80=9D?= =?UTF-8?q?=20checkbox?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ChallengesComponent/ChallengeList/index.js | 6 +++--- .../ChallengesComponent.module.scss | 2 +- src/containers/Challenges/index.js | 11 ++--------- 3 files changed, 6 insertions(+), 13 deletions(-) diff --git a/src/components/ChallengesComponent/ChallengeList/index.js b/src/components/ChallengesComponent/ChallengeList/index.js index c992e493..1d795f0b 100644 --- a/src/components/ChallengesComponent/ChallengeList/index.js +++ b/src/components/ChallengesComponent/ChallengeList/index.js @@ -657,9 +657,9 @@ class ChallengeList extends Component {
    Forums
    Status
    -
    -
    -
    +
    +
    +
    )} {challenges.length > 0 && ( diff --git a/src/components/ChallengesComponent/ChallengesComponent.module.scss b/src/components/ChallengesComponent/ChallengesComponent.module.scss index c779d15b..33da1995 100644 --- a/src/components/ChallengesComponent/ChallengesComponent.module.scss +++ b/src/components/ChallengesComponent/ChallengesComponent.module.scss @@ -3,7 +3,7 @@ .challenges { width: 100%; box-sizing: border-box; - padding: 30px; + padding: 20px 30px 30px 30px; } .title { diff --git a/src/containers/Challenges/index.js b/src/containers/Challenges/index.js index 76d2412f..ca687731 100644 --- a/src/containers/Challenges/index.js +++ b/src/containers/Challenges/index.js @@ -150,7 +150,7 @@ class Challenges extends Component { auth, metadata } = this.props - const { searchProjectName, onlyMyProjects } = this.state + const { searchProjectName } = this.state const { challengeTypes = [] } = metadata const projectInfo = _.find(projects, { id: activeProjectId }) || {} const projectComponents = @@ -167,7 +167,7 @@ class Challenges extends Component { )) return ( -
    +
    {!selfService && (
    {!dashboard && } @@ -180,13 +180,6 @@ class Challenges extends Component { value={searchProjectName} /> )} - -
    )} {!dashboard && activeProjectId === -1 && !selfService && ( From 8151422a5776472454456e5d683fe02cd0c96603 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Tue, 1 Nov 2022 08:58:39 +1100 Subject: [PATCH 08/48] Alignment fixes --- .../ChallengeList/ChallengeList.module.scss | 11 ++++++++++- .../ChallengesComponent/ChallengeList/index.js | 18 +++++++++--------- .../ChallengesComponent.module.scss | 2 +- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/components/ChallengesComponent/ChallengeList/ChallengeList.module.scss b/src/components/ChallengesComponent/ChallengeList/ChallengeList.module.scss index 597b8114..ba269410 100644 --- a/src/components/ChallengesComponent/ChallengeList/ChallengeList.module.scss +++ b/src/components/ChallengesComponent/ChallengeList/ChallengeList.module.scss @@ -7,6 +7,16 @@ justify-content: space-between; } +.col-6 { + flex: 0 0 50%; + max-width: 50%; +} + +.col-9 { + flex: 0 0 75%; + max-width: 75%; +} + .dashboardRow { align-items: flex-end; } @@ -151,7 +161,6 @@ .challengeInput { width: 94% !important; - margin-left: -6px; } @-moz-document url-prefix() { diff --git a/src/components/ChallengesComponent/ChallengeList/index.js b/src/components/ChallengesComponent/ChallengeList/index.js index 1d795f0b..65600788 100644 --- a/src/components/ChallengesComponent/ChallengeList/index.js +++ b/src/components/ChallengesComponent/ChallengeList/index.js @@ -366,7 +366,7 @@ class ChallengeList extends Component { !isBillingAccountLoading && !isBillingAccountLoadingFailed && !isBillingAccountExpired && ( -
    +
    Billing Account: ACTIVE  {' '} Start Date:{' '} @@ -378,7 +378,7 @@ class ChallengeList extends Component { !isBillingAccountLoading && !isBillingAccountLoadingFailed && isBillingAccountExpired && ( -
    +
    Billing Account: INACTIVE  {' '} Start Date:{' '} @@ -389,14 +389,14 @@ class ChallengeList extends Component { {!dashboard && !isBillingAccountLoading && isBillingAccountLoadingFailed && ( -
    +
    Billing Account failed to load
    )} {dashboard && ( -
    +
    @@ -419,7 +419,7 @@ class ChallengeList extends Component {
    )} -
    +
    -
    +
    @@ -465,7 +465,7 @@ class ChallengeList extends Component { />
    -
    +
    @@ -515,7 +515,7 @@ class ChallengeList extends Component {
    -
    +
    @@ -538,7 +538,7 @@ class ChallengeList extends Component {
    -
    +
    diff --git a/src/components/ChallengesComponent/ChallengesComponent.module.scss b/src/components/ChallengesComponent/ChallengesComponent.module.scss index 33da1995..6c735eee 100644 --- a/src/components/ChallengesComponent/ChallengesComponent.module.scss +++ b/src/components/ChallengesComponent/ChallengesComponent.module.scss @@ -22,7 +22,7 @@ display: flex; justify-content: space-between; align-items: center; - padding: 0 20px; + padding: 0 30px; } From 513b238a47a59401d72506201bc86abcb0f4a946 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Tue, 1 Nov 2022 09:30:53 +1100 Subject: [PATCH 09/48] Support new sort types --- .../ChallengeCard/index.js | 3 - .../ChallengeList/index.js | 86 +++++++++++-------- 2 files changed, 49 insertions(+), 40 deletions(-) diff --git a/src/components/ChallengesComponent/ChallengeCard/index.js b/src/components/ChallengesComponent/ChallengeCard/index.js index 6d9e5b1d..0ba62262 100644 --- a/src/components/ChallengesComponent/ChallengeCard/index.js +++ b/src/components/ChallengesComponent/ChallengeCard/index.js @@ -272,9 +272,6 @@ class ChallengeCard extends React.Component {
    {challenge.numOfSubmissions}
    -
    - 0 -
    {renderStatus(challenge.status.toUpperCase(), getStatusText)} diff --git a/src/components/ChallengesComponent/ChallengeList/index.js b/src/components/ChallengesComponent/ChallengeList/index.js index 65600788..ef1b84ef 100644 --- a/src/components/ChallengesComponent/ChallengeList/index.js +++ b/src/components/ChallengesComponent/ChallengeList/index.js @@ -245,12 +245,23 @@ class ChallengeList extends Component { }) } + renderSortIcon (currentSortBy) { + const { sortBy, sortOrder } = this.state + return sortBy === currentSortBy ? ( + + ) : null + } + render () { const { searchText, errorMessage, - sortBy, - sortOrder, challengeProjectOption, challengeStatus, challengeType, @@ -593,8 +604,14 @@ class ChallengeList extends Component { )} {challenges.length > 0 && (
    -
    - Type +
    this.updateSort('type')} + > + + Type + {this.renderSortIcon('type')} +
    Challenge Name - {sortBy === 'name' && ( - - )} + {this.renderSortIcon('name')}
    Start Date - {sortBy === 'startDate' && ( - - )} + {this.renderSortIcon('startDate')}
    End Date - {sortBy === 'endDate' && ( - - )} + {this.renderSortIcon('endDate')}
    -
    +
    this.updateSort('numOfRegistrants')} + > + {this.renderSortIcon('numOfRegistrants')}
    -
    - +
    this.updateSort('numOfSubmissions')} + > + + + {this.renderSortIcon('numOfSubmissions')} +
    -
    Forums
    -
    Status
    -
    +
    this.updateSort('status')} + > + + Status + {this.renderSortIcon('status')} + +
    +
     
    +
     
    +
     
    From f7252f47736bdcc3a2acc40e5e5a4936d2683f04 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Tue, 1 Nov 2022 10:31:45 +1100 Subject: [PATCH 10/48] Minor layout fix --- src/components/ChallengesComponent/ChallengeList/index.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/ChallengesComponent/ChallengeList/index.js b/src/components/ChallengesComponent/ChallengeList/index.js index ef1b84ef..e1ec5d11 100644 --- a/src/components/ChallengesComponent/ChallengeList/index.js +++ b/src/components/ChallengesComponent/ChallengeList/index.js @@ -670,8 +670,6 @@ class ChallengeList extends Component {
     
     
     
    -
    -
    )} {challenges.length > 0 && ( From 7ba11ac3eca0c1a0fe733e6ff3ed60d67c6d1ccc Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Fri, 4 Nov 2022 07:28:29 +1100 Subject: [PATCH 11/48] Sorting and filtering bugs --- .../ChallengeCard/index.js | 14 +++++++------ .../ChallengeList/index.js | 20 +++++++++---------- .../ChallengeStatus.module.scss | 4 ++++ .../ChallengeStatus/index.js | 3 ++- .../ChallengeTag/ChallengeTag.module.scss | 4 ++++ .../ChallengesComponent/ChallengeTag/index.js | 2 +- src/containers/Challenges/index.js | 2 +- 7 files changed, 30 insertions(+), 19 deletions(-) diff --git a/src/components/ChallengesComponent/ChallengeCard/index.js b/src/components/ChallengesComponent/ChallengeCard/index.js index 0ba62262..915a6fc7 100644 --- a/src/components/ChallengesComponent/ChallengeCard/index.js +++ b/src/components/ChallengesComponent/ChallengeCard/index.js @@ -95,16 +95,18 @@ const hoverComponents = (challenge, onUpdateLaunch, deleteModalLaunch) => { } const renderStatus = (status, getStatusText) => { - switch (status) { + const statusMessage = status.split(' ')[0] + switch (statusMessage) { case CHALLENGE_STATUS.ACTIVE: case CHALLENGE_STATUS.APPROVED: case CHALLENGE_STATUS.NEW: case CHALLENGE_STATUS.DRAFT: case CHALLENGE_STATUS.COMPLETED: - const statusText = getStatusText ? getStatusText(status) : status - return () + case CHALLENGE_STATUS.CANCELLED: + const statusText = getStatusText ? getStatusText(statusMessage) : statusMessage + return () default: - return ({statusText}) + return ({status}) } } @@ -204,7 +206,7 @@ class ChallengeCard extends React.Component { const deleteMessage = isCheckChalengePermission ? 'Checking permissions...' : `Do you want to delete "${challenge.name}"?` - const directUrl = `${DIRECT_PROJECT_URL}/contest/detail?projectId=${challenge.legacyId}` + const orUrl = `${ONLINE_REVIEW_URL}/review/actions/ViewProjectDetails?pid=${challenge.legacyId}` const communityAppUrl = `${COMMUNITY_APP_URL}/challenges/${challenge.id}` return ( @@ -279,7 +281,7 @@ class ChallengeCard extends React.Component { {(disableHover ? Edit : hoverComponents(challenge, this.onUpdateLaunch, this.deleteModalLaunch))}
    - OR + OR
    CA diff --git a/src/components/ChallengesComponent/ChallengeList/index.js b/src/components/ChallengesComponent/ChallengeList/index.js index e1ec5d11..a0cf0758 100644 --- a/src/components/ChallengesComponent/ChallengeList/index.js +++ b/src/components/ChallengesComponent/ChallengeList/index.js @@ -7,6 +7,7 @@ import PropTypes from 'prop-types' import { DebounceInput } from 'react-debounce-input' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faFile, faUser } from '@fortawesome/free-solid-svg-icons' +import isAfter from 'date-fns/isAfter' import DateTime from '@nateradebaugh/react-datetime' import Pagination from 'react-js-pagination' import cn from 'classnames' @@ -298,19 +299,10 @@ class ChallengeList extends Component { value: _.capitalize(item) })) - statusOptions.unshift({ - label: 'All Challenge Status', - value: null - }) - const challengeTypesOptions = challengeTypes.map(item => ({ label: item.name, value: item.abbreviation })) - challengeTypesOptions.unshift({ - label: 'All Challenge Types', - value: null - }) let selectedTab = 0 switch (status) { @@ -467,12 +459,13 @@ class ChallengeList extends Component { onChange={e => this.updateSearchParam( searchText, - e.value, + e ? e.value : null, challengeType, challengeDate, projectOption ) } + isClearable />
    @@ -509,6 +502,9 @@ class ChallengeList extends Component { value={ challengeDate.startDateEnd ? challengeDate.startDateEnd : null } + isValidDate={(current) => { + return isAfter(current, challengeDate.startDateStart) + }} onChange={e => this.updateSearchParam( searchText, @@ -545,6 +541,7 @@ class ChallengeList extends Component { projectOption ) } + isClearable />
    @@ -580,6 +577,9 @@ class ChallengeList extends Component { value={ challengeDate.endDateEnd ? challengeDate.endDateEnd : null } + isValidDate={(current) => { + return isAfter(current, challengeDate.endDateStart) + }} onChange={e => this.updateSearchParam( searchText, diff --git a/src/components/ChallengesComponent/ChallengeStatus/ChallengeStatus.module.scss b/src/components/ChallengesComponent/ChallengeStatus/ChallengeStatus.module.scss index 12ac0156..5d29d46c 100644 --- a/src/components/ChallengesComponent/ChallengeStatus/ChallengeStatus.module.scss +++ b/src/components/ChallengesComponent/ChallengeStatus/ChallengeStatus.module.scss @@ -37,5 +37,9 @@ &.yellow { background-color: $status-yellow; } + + &.red { + background-color: $tc-red; + } } diff --git a/src/components/ChallengesComponent/ChallengeStatus/index.js b/src/components/ChallengesComponent/ChallengeStatus/index.js index a5d074ea..4290670f 100644 --- a/src/components/ChallengesComponent/ChallengeStatus/index.js +++ b/src/components/ChallengesComponent/ChallengeStatus/index.js @@ -15,7 +15,8 @@ const statuses = { [CHALLENGE_STATUS.APPROVED]: styles.yellow, [CHALLENGE_STATUS.NEW]: styles.yellow, [CHALLENGE_STATUS.DRAFT]: styles.gray, - [CHALLENGE_STATUS.COMPLETED]: styles.blue + [CHALLENGE_STATUS.COMPLETED]: styles.blue, + [CHALLENGE_STATUS.CANCELLED]: styles.red } const ChallengeStatus = ({ status, statusText }) => { diff --git a/src/components/ChallengesComponent/ChallengeTag/ChallengeTag.module.scss b/src/components/ChallengesComponent/ChallengeTag/ChallengeTag.module.scss index 026e5fc9..80fda267 100644 --- a/src/components/ChallengesComponent/ChallengeTag/ChallengeTag.module.scss +++ b/src/components/ChallengesComponent/ChallengeTag/ChallengeTag.module.scss @@ -58,6 +58,10 @@ $corner-radius: 2px; background: $track-code-grey; } + &.MA { + background: $track-code-grey; + } + &.withTco { border-bottom-left-radius: 0; border-bottom-right-radius: 0; diff --git a/src/components/ChallengesComponent/ChallengeTag/index.js b/src/components/ChallengesComponent/ChallengeTag/index.js index 0522b98e..2bc95c8d 100644 --- a/src/components/ChallengesComponent/ChallengeTag/index.js +++ b/src/components/ChallengesComponent/ChallengeTag/index.js @@ -8,7 +8,7 @@ export default function ChallengeTag ({ type, challengeTypes }) { let abbreviation = getChallengeTypeAbbr(type, challengeTypes) - if (['CH', 'F2F', 'TSK', 'MM', 'RDM', 'SKL', 'SRM', 'PC'].indexOf(abbreviation) < 0) { + if (['CH', 'F2F', 'TSK', 'MM', 'RDM', 'SKL', 'MA', 'SRM', 'PC'].indexOf(abbreviation) < 0) { abbreviation = '' } return ( diff --git a/src/containers/Challenges/index.js b/src/containers/Challenges/index.js index ca687731..ed18b50b 100644 --- a/src/containers/Challenges/index.js +++ b/src/containers/Challenges/index.js @@ -57,7 +57,7 @@ class Challenges extends Component { window.localStorage.setItem('projectLoading', 'true') this.props.loadProject(projectId) } - this.reloadChallenges(this.props) + this.reloadChallenges(this.props, true) } } From 00c51e0f8b5bedb5fd03d676fc33ed2c63e16504 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Sat, 5 Nov 2022 09:31:36 +1100 Subject: [PATCH 12/48] Changes to sorting and filtering due to QA reports --- src/actions/challenges.js | 9 +- .../ChallengeCard/index.js | 6 +- .../ChallengeList/ChallengeList.module.scss | 22 ++- .../ChallengeList/index.js | 152 ++++++++++++++---- .../ChallengeTag/ChallengeTag.module.scss | 1 - src/config/constants.js | 9 +- 6 files changed, 159 insertions(+), 40 deletions(-) diff --git a/src/actions/challenges.js b/src/actions/challenges.js index 89753d42..f9eb9771 100644 --- a/src/actions/challenges.js +++ b/src/actions/challenges.js @@ -69,7 +69,8 @@ export function loadChallengesByPage ( filterChallengeType = {}, filterDate = {}, filterSortBy = null, - filterSortOrder = null + filterSortOrder = null, + perPage = PAGE_SIZE ) { return (dispatch, getState) => { if (_.isObject(projectId)) { @@ -83,7 +84,7 @@ export function loadChallengesByPage ( filterDate, filterSortBy, filterSortOrder, - perPage: PAGE_SIZE, + perPage, page }) } else { @@ -97,7 +98,7 @@ export function loadChallengesByPage ( filterDate, filterSortBy, filterSortOrder, - perPage: PAGE_SIZE, + perPage, page }) } @@ -152,7 +153,7 @@ export function loadChallengesByPage ( return fetchChallenges(filters, { page, - perPage: PAGE_SIZE + perPage // memberId: getState().auth.user ? getState().auth.user.userId : null }).then((res) => { dispatch({ diff --git a/src/components/ChallengesComponent/ChallengeCard/index.js b/src/components/ChallengesComponent/ChallengeCard/index.js index 915a6fc7..7458dad3 100644 --- a/src/components/ChallengesComponent/ChallengeCard/index.js +++ b/src/components/ChallengesComponent/ChallengeCard/index.js @@ -259,7 +259,7 @@ class ChallengeCard extends React.Component { setActiveProject(parseInt(challenge.projectId))}>
    - {challenge.name} + {challenge.name}
    @@ -274,9 +274,9 @@ class ChallengeCard extends React.Component {
    {challenge.numOfSubmissions}
    - +
    {renderStatus(challenge.status.toUpperCase(), getStatusText)} - +
    {(disableHover ? Edit : hoverComponents(challenge, this.onUpdateLaunch, this.deleteModalLaunch))}
    diff --git a/src/components/ChallengesComponent/ChallengeList/ChallengeList.module.scss b/src/components/ChallengesComponent/ChallengeList/ChallengeList.module.scss index ba269410..cff7fd82 100644 --- a/src/components/ChallengesComponent/ChallengeList/ChallengeList.module.scss +++ b/src/components/ChallengesComponent/ChallengeList/ChallengeList.module.scss @@ -160,7 +160,16 @@ } .challengeInput { - width: 94% !important; + width: 60% !important; + max-width: 230px !important; +} + +.searchInputWrapper { + display: flex; +} + +.resetFilter { + margin-left: 35px; } @-moz-document url-prefix() { @@ -216,12 +225,21 @@ } } -.paginationContainer { +.footer { display: flex; justify-content: flex-end; margin-top: 30px; } +.perPageContainer { + margin-right: 20px; + max-width: 150px; +} + +.paginationContainer { + display: flex; +} + .modalContainer { padding: 0; position: fixed; diff --git a/src/components/ChallengesComponent/ChallengeList/index.js b/src/components/ChallengesComponent/ChallengeList/index.js index a0cf0758..b41c9c5a 100644 --- a/src/components/ChallengesComponent/ChallengeList/index.js +++ b/src/components/ChallengesComponent/ChallengeList/index.js @@ -12,7 +12,7 @@ import DateTime from '@nateradebaugh/react-datetime' import Pagination from 'react-js-pagination' import cn from 'classnames' -import { PrimaryButton } from '../../Buttons' +import { OutlineButton, PrimaryButton } from '../../Buttons' import Modal from '../../Modal' import 'react-tabs/style/react-tabs.css' import styles from './ChallengeList.module.scss' @@ -22,7 +22,7 @@ import Message from '../Message' import SortIcon from '../../../assets/images/sort-icon.svg' import Select from '../../Select' -import { CHALLENGE_STATUS } from '../../../config/constants' +import { CHALLENGE_STATUS, PAGE_SIZE, PAGINATION_PER_PAGE_OPTIONS } from '../../../config/constants' import { checkAdmin } from '../../../util/tc' require('bootstrap/scss/bootstrap.scss') @@ -46,12 +46,14 @@ class ChallengeList extends Component { } this.directUpdateSearchParam = this.updateSearchParam.bind(this) // update search param without debounce this.handlePageChange = this.handlePageChange.bind(this) // update search param without debounce + this.handlePerPageChange = this.handlePerPageChange.bind(this) this.showError = this.showError.bind(this) this.hideError = this.hideError.bind(this) this.reloadChallengeList = this.reloadChallengeList.bind(this) this.updateSearchParam = debounce(this.updateSearchParam.bind(this), 1000) this.updateSort = this.updateSort.bind(this) this.update = debounce(this.updateSearchParam.bind(this), 1000) + this.resetFilter = this.resetFilter.bind(this) } /** @@ -111,7 +113,7 @@ class ChallengeList extends Component { /** * Update filter for getting project by pagination - * @param {Number} pageNumber page numer + * @param {Number} pageNumber page number */ handlePageChange (pageNumber) { const { searchText } = this.state @@ -142,6 +144,44 @@ class ChallengeList extends Component { } } + /** + * Update filter for getting project by pagination + * @param {Number} perPageNumber per page number + */ + handlePerPageChange (option) { + const perPageNumber = option.value + const { searchText, sortBy, sortOrder } = this.state + const { + perPage, + page, + loadChallengesByPage, + activeProjectId, + dashboard, + filterProjectOption, + status, + selfService, + filterChallengeType, + filterDate + } = this.props + + let projectId = dashboard ? filterProjectOption : activeProjectId + if (perPage !== perPageNumber) { + loadChallengesByPage( + page, + projectId, + status, + searchText, + selfService, + this.getHandle(), + filterChallengeType, + filterDate, + sortBy, + sortOrder, + perPageNumber + ) + } + } + /** * Reload challenge list */ @@ -259,6 +299,40 @@ class ChallengeList extends Component { ) : null } + resetFilter () { + const { + activeProjectId, + dashboard, + filterProjectOption, + selfService, + loadChallengesByPage + } = this.props + + this.setState({ + searchText: '', + challengeType: null, + sortBy: '', + sortOrder: 'asc', + challengeDate: {} + }) + + let projectId = dashboard ? filterProjectOption : activeProjectId + + loadChallengesByPage( + 1, + projectId, + null, + '', + selfService, + this.getHandle(), + null, + {}, + null, + null, + PAGE_SIZE + ) + } + render () { const { searchText, @@ -423,22 +497,30 @@ class ChallengeList extends Component {
    )}
    - - this.updateSearchParam( - e.target.value, - status, - challengeType, - challengeDate, - projectOption - ) - } - value={searchText} - /> +
    + +
    +
    + + this.updateSearchParam( + e.target.value, + status, + challengeType, + challengeDate, + projectOption + ) + } + value={searchText} + /> +
    + +
    +
    @@ -699,16 +781,28 @@ class ChallengeList extends Component { })} )} -
    - +
    +
    + e.target.checked && this.updatePermission(PROJECT_ROLES.READ)} + /> + +
    )} +
    +
    + {showRadioButtons && (
    + e.target.checked && this.updatePermission(PROJECT_ROLES.WRITE)} + /> + +
    )} +
    +
    + {showRadioButtons && (
    + e.target.checked && this.updatePermission(PROJECT_ROLES.MANAGER)} + /> + +
    )} +
    +
    + {showRadioButtons && (
    + e.target.checked && this.updatePermission(PROJECT_ROLES.COPILOT)} + /> + +
    )} +
    +
    + { onRemoveClick(user) }} /> +
    +
    +
    + ) + } +} + +UserCard.propTypes = { + user: PropTypes.object, + reloadProjectMembers: PropTypes.func.isRequired, + onRemoveClick: PropTypes.func.isRequired +} + +export default UserCard diff --git a/src/components/Users/Users.module.scss b/src/components/Users/Users.module.scss new file mode 100644 index 00000000..d86de92d --- /dev/null +++ b/src/components/Users/Users.module.scss @@ -0,0 +1,484 @@ +@import '../../styles/includes'; + +.list { + width: 100%; + display: flex; + flex-direction: column; + justify-content: space-between; +} + +.contentContainer { + margin: 0 20px; + padding-left: 15px; + + .col-6 { + padding: 0px; + } +} + +.userList { + display: flex; + flex-direction: column; + background-color: $white; + padding: 0; + margin: 0; +} + +.errorMesssage, .required { + color: $tc-red; +} + +.userItem { + list-style: none; + width: 100%; + border-top: 1px $gray solid; + padding: 10px 0; + font-size: 14px; + + a { + text-decoration: none; + } +} + + +.row { + display: flex; + margin-bottom: 16px; + align-items: center; + + input { + max-width: 280px; + + @include upto-sm { + display: block; + padding-bottom: 10px; + } + } + + .title { + font-weight: bold; + } + .error { + font-weight: bold; + color: #BE405E; + } + .active { + color: #008000; + } + .inactive { + color: #BE405E; + } + + .field { + @include upto-sm { + display: block; + padding-bottom: 10px; + } + + label { + @include roboto-bold(); + + font-size: 16px; + line-height: 19px; + font-weight: 500; + color: $tc-gray-80; + } + + &.input1 { + max-width: 185px; + min-width: 185px; + margin-right: 14px; + white-space: nowrap; + display: flex; + align-items: center; + flex-grow: 1; + + span { + color: $tc-red; + } + } + + &.input2.error { + color: $tc-red; + margin-top: -25px; + } + &.input2 { + align-self: flex-end; + width: 50%; + margin-bottom: auto; + margin-top: auto; + display: flex; + flex-direction: row; + max-width: 500px; + min-width: 500px; + } + + &.manageLink { + margin: 2px 12px; + text-decoration: none; + font-size: 12px; + } + } +} + +.header { + background-color: $light-bg; + height: 50px; + display: flex; + justify-content: flex-start; + padding: 0 20px; + font-size: 14px; + + gap: 30px; + + .sortable { + display: flex; + cursor: pointer; + } + + .col1 { + display: flex; + flex: 1; + flex-direction: column; + justify-content: center; + + span { + color: $tc-red; + } + } + + .col2 { + display: flex; + flex: 2; + flex-direction: column; + justify-content: center; + } + + .col3 { + display: flex; + flex: 1; + flex-direction: column; + justify-content: center; + } + + .col4 { + display: flex; + width: 30px; + flex-direction: column; + justify-content: center; + } + + .col5 { + display: flex; + width: 150px; + flex-direction: column; + justify-content: center; + } + + .col6 { + display: flex; + width: 40px; + flex-direction: column; + justify-content: center; + } +} + +.challengeInput { + width: 94% !important; + margin-left: -6px; +} + +@-moz-document url-prefix() { + .challengeInput { + &::-moz-placeholder { + /* Mozilla Firefox 19+ */ + line-height: 38px; + } + &::-webkit-input-placeholder { + /* Webkit */ + line-height: 38px; + } + &:-ms-input-placeholder { + /* IE */ + line-height: 38px; + } + } +} + +.tabsContainer { + ul { + margin: 0; + } + + :global { + .react-tabs__tab--selected { + background: $light-bg; + + &:focus::after { + background: $light-bg; + } + } + } +} + +.challengeList { + display: flex; + flex-direction: column; + background-color: $white; + padding: 0; + margin: 0; +} + +.challengeItem { + list-style: none; + width: 100%; + border-top: 1px $gray solid; + padding: 10px 0; + font-size: 14px; + + a { + text-decoration: none; + } +} + +.paginationContainer { + display: flex; + justify-content: flex-end; + margin-top: 30px; +} + +.addUserTitle { + min-width: 100px; + font-weight: 500; +} + +.modalContainer { + padding: 0; + position: fixed; + overflow: auto; + z-index: 10000; + top: 0; + right: 0; + bottom: 0; + left: 0; + box-sizing: border-box; + width: auto; + max-width: none; + transform: none; + background: transparent; + color: $text-color; + opacity: 1; + display: flex; + justify-content: center; + align-items: center; + + :global { + button.close { + margin-right: 5px; + margin-top: 5px; + } + } + + .contentContainer { + box-sizing: border-box; + background: $white; + opacity: 1; + position: relative; + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: center; + border-radius: 6px; + margin: 0 auto; + width: 852px; + padding: 30px; + + .content { + padding: 30px; + width: 100%; + height: 100%; + } + + .title { + @include roboto-bold(); + + font-size: 30px; + line-height: 36px; + margin-bottom: 30px; + margin-top: 0; + } + + span { + @include roboto; + + font-size: 22px; + font-weight: 400; + line-height: 26px; + } + + &.confirm { + width: 999px; + + .buttonGroup { + display: flex; + justify-content: space-between; + margin-top: 30px; + + .buttonSizeA { + width: 193px; + height: 40px; + margin-right: 33px; + + span { + font-size: 18px; + font-weight: 500; + } + } + + .buttonSizeB { + width: 160px; + height: 40px; + + span { + font-size: 18px; + font-weight: 500; + line-height: 22px; + } + } + } + } + + .buttonGroup { + display: flex; + justify-content: space-between; + margin-top: 30px; + + .button { + width: 135px; + height: 40px; + margin-right: 66px; + + span { + font-size: 18px; + font-weight: 500; + } + } + + .button:last-child { + margin-right: 0; + } + } + } +} + +.sortIcon { + width: 14px; + height: 12px; + margin-left: 5px; + margin-top: 2px; + height: 100%; + + &.asc { + transform: rotate(180deg); + } +} + +.filterItem { + display: flex; +} + +.to { + margin: 0 10px; + display: flex; + flex-direction: column; + justify-content: center; +} + +.addButtonContainer { + width: 110px; + height: 30px; + margin-top: 20px; + margin-bottom: 20px; +} + +.addUserContentContainer { + +} + +.tcRadioButton { + .tc-radioButton-label { + @include roboto-light(); + + line-height: 17px; + font-weight: 300; + margin-left: 21px; + user-select: none; + cursor: pointer; + width: 195px; + font-size: 14px; + color: #3d3d3d; + } + + height: 18px; + width: 210px; + margin: 0; + padding: 0; + vertical-align: bottom; + position: relative; + display: inline-block; + + input[type=radio] { + display: none; + } + + label { + @include roboto-light(); + + line-height: 17px; + font-weight: 300; + cursor: pointer; + position: absolute; + display: inline-block; + width: 16px; + height: 16px; + border-radius: 8px; + top: 0; + left: 0; + border: none; + box-shadow: none; + background: $tc-gray-30; + transition: all 0.15s ease-in-out; + + &::after { + opacity: 0; + content: ''; + position: absolute; + width: 8px; + height: 8px; + background: transparent; + top: 4px; + left: 4px; + border: 4px solid $tc-blue-20; + border-radius: 4px; + transition: all 0.15s ease-in-out; + } + + &:hover::after { + opacity: 0.3; + } + + div { + margin-left: 24px; + width: 150px; + } + } + + input[type=radio]:checked ~ label { + background: $tc-blue-20; + } + + input[type=radio]:checked + label::after { + opacity: 1; + border-color: $white; + } +} diff --git a/src/components/Users/index.js b/src/components/Users/index.js new file mode 100644 index 00000000..272a3a54 --- /dev/null +++ b/src/components/Users/index.js @@ -0,0 +1,428 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import _ from 'lodash' +import cn from 'classnames' +import styles from './Users.module.scss' +import Select from '../Select' +import UserCard from '../UserCard' +import PrimaryButton from '../Buttons/PrimaryButton' +import Modal from '../Modal' +import SelectUserAutocomplete from '../SelectUserAutocomplete' +import { PROJECT_ROLES } from '../../config/constants' +import { checkAdmin } from '../../util/tc' +import { addUserToProject, removeUserFromProject } from '../../services/projects' +import { wait } from '../../util/helper' +import ConfirmationModal from '../Modal/ConfirmationModal' + +const theme = { + container: styles.modalContainer +} + +class Users extends Component { + constructor (props) { + super(props) + this.state = { + projectOption: null, + showAddUserModal: false, + userToAdd: null, + userPermissionToAdd: PROJECT_ROLES.READ, + showSelectUserError: false, + isAdding: false, + addUserError: false, + isRemoving: false, + removeError: null, + showRemoveConfirmationModal: false, + userToRemove: null + } + this.setProjectOption = this.setProjectOption.bind(this) + this.onAddUserClick = this.onAddUserClick.bind(this) + this.resetAddUserState = this.resetAddUserState.bind(this) + this.onUpdateUserToAdd = this.onUpdateUserToAdd.bind(this) + this.onAddUserConfirmClick = this.onAddUserConfirmClick.bind(this) + this.updatePermission = this.updatePermission.bind(this) + this.onRemoveClick = this.onRemoveClick.bind(this) + this.resetRemoveUserState = this.resetRemoveUserState.bind(this) + this.onRemoveConfirmClick = this.onRemoveConfirmClick.bind(this) + } + + setProjectOption (projectOption) { + this.setState({ projectOption }) + const { loadProject } = this.props + loadProject(projectOption.value, false) + } + + updatePermission (newRole) { + this.setState({ + userPermissionToAdd: newRole + }) + } + + onAddUserClick () { + this.setState({ + showAddUserModal: true + }) + } + + resetAddUserState () { + this.setState({ + userToAdd: null, + showSelectUserError: false, + isAdding: false, + showAddUserModal: false, + userPermissionToAdd: PROJECT_ROLES.READ, + addUserError: null + }) + } + + onUpdateUserToAdd (option) { + let userToAdd = null + if (option && option.value) { + userToAdd = { + handle: option.label, + userId: parseInt(option.value, 10) + } + } + + this.setState({ + userToAdd, + showSelectUserError: !userToAdd + }) + } + + async onAddUserConfirmClick () { + console.log('in onAddUserConfirmClick') + console.log('in onAddUserConfirmClick this.state.userToAdd', this.state.userToAdd) + const { reloadProjectMembers } = this.props + if (this.state.isAdding) { return } + + this.setState({ + showSelectUserError: false, + addUserError: null + }) + + if (!this.state.userToAdd) { + console.log('in if') + this.setState({ + showSelectUserError: true + }) + return + } + + this.setState({ + isAdding: true + }) + + try { + await addUserToProject(this.state.projectOption.value, this.state.userToAdd.userId, this.state.userPermissionToAdd) + // wait for a second so that project's members are updated + await wait(1000) + if (this.state.projectOption.value) { reloadProjectMembers(this.state.projectOption.value) } + this.resetAddUserState() + } catch (e) { + const error = _.get( + e, + 'response.data.message', + `Unable to add user` + ) + this.setState({ isAdding: false, addUserError: error }) + } + } + + getHandle () { + return this.props.auth && this.props.auth.user + ? this.props.auth.user.handle + : null + } + + getMemberRole (members, handle) { + if (!handle) { return null } + + const found = _.find(members, (m) => { + return m.handle === handle + }) + + return _.get(found, 'role') + } + + onRemoveClick (user) { + if (this.state.isRemoving) { + return + } + + this.setState({ + showRemoveConfirmationModal: true, + userToRemove: user + }) + } + + resetRemoveUserState () { + this.setState({ + isRemoving: false, + showRemoveConfirmationModal: false, + userToRemove: null, + removeError: null + }) + } + + async onRemoveConfirmClick () { + if (this.state.isRemoving) { return } + + const { reloadProjectMembers } = this.props + const userToRemove = this.state.userToRemove + try { + this.setState({ isRemoving: true }) + await removeUserFromProject(userToRemove.projectId, userToRemove.id) + await wait(1000) + if (this.state.projectOption.value) { reloadProjectMembers(this.state.projectOption.value) } + + this.resetRemoveUserState() + } catch (e) { + const error = _.get( + e, + 'response.data.message', + `Unable to remove user` + ) + this.setState({ isRemoving: false, removeError: error }) + } + } + + checkIsCopilotOrManager (projectMembers, handle) { + if (projectMembers && projectMembers.length > 0) { + const role = this.getMemberRole(projectMembers, handle) + return role === PROJECT_ROLES.COPILOT || role === PROJECT_ROLES.MANAGER + } else { + return false + } + } + + render () { + const { projects, projectMembers, reloadProjectMembers } = this.props + const projectOptions = projects.map(p => { + return { + label: p.name, + value: p.id + } + }) + const loggedInHandle = this.getHandle() + const membersExist = projectMembers && projectMembers.length > 0 + const isCopilotOrManager = this.checkIsCopilotOrManager(projectMembers, loggedInHandle) + const isAdmin = checkAdmin(this.props.auth.token) + const showAddUser = this.state.projectOption && (isCopilotOrManager || isAdmin) + + return ( +
    +
    +
    +
    + +
    +
    + e.target.checked && this.updatePermission(PROJECT_ROLES.READ)} + /> + +
    +
    +
    +
    + e.target.checked && this.updatePermission(PROJECT_ROLES.WRITE)} + /> + +
    +
    +
    +
    + e.target.checked && this.updatePermission(PROJECT_ROLES.MANAGER)} + /> + +
    +
    +
    +
    + e.target.checked && this.updatePermission(PROJECT_ROLES.COPILOT)} + /> + +
    +
    +
    + { + this.state.addUserError && ( +
    + {this.state.addUserError} +
    + ) + } +
    + +
    +
    + this.resetAddUserState()} + /> +
    +
    + this.onAddUserConfirmClick()} + /> +
    +
    +
    + + ) + } + { + this.state.showRemoveConfirmationModal && ( + + ) + } + { + membersExist && ( + <> +
    +
    + User +
    +
    + Read +
    +
    + Write +
    +
    + Full Access +
    +
    + Copilot +
    +
    +
      + { + _.map(projectMembers, (member) => { + return ( +
    • + +
    • + ) + }) + } +
    + + ) + } + +
    + ) + } +} + +Users.propTypes = { + loadProject: PropTypes.func.isRequired, + reloadProjectMembers: PropTypes.func.isRequired, + auth: PropTypes.object, + projects: PropTypes.arrayOf(PropTypes.object), + projectMembers: PropTypes.arrayOf(PropTypes.object) +} + +export default Users diff --git a/src/config/constants.js b/src/config/constants.js index 2a98d365..6b6398e1 100644 --- a/src/config/constants.js +++ b/src/config/constants.js @@ -94,11 +94,14 @@ export const LOAD_PROJECT_DETAILS_SUCCESS = 'LOAD_PROJECT_DETAILS_SUCCESS' export const LOAD_PROJECT_DETAILS_PENDING = 'LOAD_PROJECT_DETAILS_PENDING' export const LOAD_PROJECT_DETAILS_FAILURE = 'LOAD_PROJECT_DETAILS_FAILURE' +export const UPDATE_PROJECT_ROLE_FOR_MEMBER_SUCCESS = 'UPDATE_PROJECT_ROLE_FOR_MEMBER_SUCCESS' + export const LOAD_CHALLENGE_SUBMISSIONS = 'LOAD_CHALLENGE_SUBMISSIONS' export const LOAD_CHALLENGE_SUBMISSIONS_SUCCESS = 'LOAD_CHALLENGE_SUBMISSIONS_SUCCESS' export const LOAD_CHALLENGE_SUBMISSIONS_PENDING = 'LOAD_CHALLENGE_SUBMISSIONS_PENDING' export const LOAD_CHALLENGE_SUBMISSIONS_FAILURE = 'LOAD_CHALLENGE_SUBMISSIONS_FAILURE' +export const LOAD_CHALLENGE_MEMBERS = 'LOAD_CHALLENGE_MEMBERS' export const LOAD_CHALLENGE_MEMBERS_SUCCESS = 'LOAD_CHALLENGE_MEMBERS_SUCCESS' export const LOAD_CHALLENGE_METADATA_SUCCESS = 'LOAD_CHALLENGE_METADATA_SUCCESS' @@ -180,6 +183,13 @@ export const MARATHON_MATCH_SUBTRACKS = [ 'DEVELOP_MARATHON_MATCH' ] +export const PROJECT_ROLES = { + READ: 'observer', + WRITE: 'customer', + MANAGER: 'manager', + COPILOT: 'copilot' +} + export const CHALLENGE_STATUS = { ACTIVE: 'ACTIVE', NEW: 'NEW', diff --git a/src/containers/Tab/index.js b/src/containers/Tab/index.js index 03cabd5e..352f54f8 100644 --- a/src/containers/Tab/index.js +++ b/src/containers/Tab/index.js @@ -39,6 +39,8 @@ class TabContainer extends Component { this.setState({ currentTab: 1 }) } else if (nextProps.history.location.pathname === '/projects') { this.setState({ currentTab: 2 }) + } else if (nextProps.history.location.pathname === '/users') { + this.setState({ currentTab: 3 }) } else { this.setState({ currentTab: 0 }) } @@ -80,6 +82,9 @@ class TabContainer extends Component { } else if (tab === 2) { history.push('/projects') this.setState({ currentTab: 2 }) + } else if (tab === 3) { + history.push('/users') + this.setState({ currentTab: 3 }) } resetSidebarActiveParams() diff --git a/src/containers/Users/index.js b/src/containers/Users/index.js new file mode 100644 index 00000000..2766d62a --- /dev/null +++ b/src/containers/Users/index.js @@ -0,0 +1,46 @@ +import React, { Component } from 'react' +import { connect } from 'react-redux' +import _ from 'lodash' +import PT from 'prop-types' +import UsersComponent from '../../components/Users' +import { loadProjects } from '../../actions/sidebar' +import { + loadProject, + reloadProjectMembers +} from '../../actions/projects' + +class Users extends Component { + componentDidMount () { + this.props.loadProjects() + } + + render () { + const { projects, loadProject, projectMembers, auth, reloadProjectMembers } = this.props + return + } +} + +const mapStateToProps = ({ sidebar, challenges, auth }) => { + return { + projects: sidebar.projects, + projectMembers: _.get(challenges, 'metadata.members'), + auth + } +} + +const mapDispatchToProps = { + loadProject, + loadProjects, + reloadProjectMembers +} + +Users.propTypes = { + loadProject: PT.func.isRequired, + loadProjects: PT.func.isRequired, + reloadProjectMembers: PT.func.isRequired, + projects: PT.arrayOf(PT.object), + projectMembers: PT.arrayOf(PT.object), + auth: PT.object +} + +export default connect(mapStateToProps, mapDispatchToProps)(Users) diff --git a/src/reducers/challenges.js b/src/reducers/challenges.js index 48057aaf..d9eedd01 100644 --- a/src/reducers/challenges.js +++ b/src/reducers/challenges.js @@ -251,7 +251,7 @@ export default function (state = initialState, action) { } } case LOAD_CHALLENGE_MEMBERS_SUCCESS: { - return { ...state, metadata: { ...state.metadata, members: action.members } } + return { ...state, metadata: { ...state.metadata, members: action.payload } } } case CREATE_ATTACHMENT_PENDING: { const attachments = [ diff --git a/src/routes.js b/src/routes.js index d599b707..50a7a947 100644 --- a/src/routes.js +++ b/src/routes.js @@ -20,6 +20,7 @@ import { setCookie, removeCookie, isBetaMode } from './util/cookie' import IdleTimer from 'react-idle-timer' import modalStyles from './styles/modal.module.scss' import ConfirmationModal from './components/Modal/ConfirmationModal' +import Users from './containers/Users' const { ACCOUNTS_APP_LOGIN_URL, IDLE_TIMEOUT_MINUTES, IDLE_TIMEOUT_GRACE_MINUTES, COMMUNITY_APP_URL } = process.env @@ -175,6 +176,13 @@ class Routes extends React.Component { )()} /> + renderApp( + , + , + + )()} + /> renderApp( , diff --git a/src/services/projects.js b/src/services/projects.js index 0279498e..50368519 100644 --- a/src/services/projects.js +++ b/src/services/projects.js @@ -57,6 +57,46 @@ export async function fetchProjectPhases (id) { return _.get(response, 'data') } +/** + * updates the role for the member for the given project id + * @param projectId project id + * @param memberRecordId the id for the member record to update + * @param newRole the new role + * @returns {Promise<*>} + */ +export async function updateProjectMemberRole (projectId, memberRecordId, newRole) { + const response = await axiosInstance.patch(`${PROJECT_API_URL}/${projectId}/members/${memberRecordId}`, { + role: newRole + }) + return _.get(response, 'data') +} + +/** + * adds the given user to the given project with the specified role + * @param projectId project id + * @param userId user id + * @param role + * @returns {Promise<*>} + */ +export async function addUserToProject (projectId, userId, role) { + const response = await axiosInstance.post(`${PROJECT_API_URL}/${projectId}/members`, { + userId, + role + }) + return _.get(response, 'data') +} + +/** + * removes the given member record from the project + * @param projectId project id + * @param memberRecordId member record id + * @returns {Promise<*>} + */ +export async function removeUserFromProject (projectId, memberRecordId) { + const response = await axiosInstance.delete(`${PROJECT_API_URL}/${projectId}/members/${memberRecordId}`) + return response +} + /** * Save challengeId as Phase product detail * @param projectId Project id diff --git a/src/util/helper.js b/src/util/helper.js new file mode 100644 index 00000000..1e3e7d16 --- /dev/null +++ b/src/util/helper.js @@ -0,0 +1,10 @@ +/** + * Returns a promise that resolves after given milliseconds + * @param ms milli seconds + * @return {Promise} + */ +export const wait = (ms) => { + return new Promise((resolve) => { + setTimeout(resolve, ms) + }) +} From 11b1900e434ac5a990e407d821c5072aaeb781c4 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Wed, 30 Nov 2022 20:24:05 +1100 Subject: [PATCH 14/48] UI Bugs https://github.com/topcoder-platform/work-manager/issues/1438 https://github.com/topcoder-platform/work-manager/issues/1439 --- src/components/ChallengeEditor/index.js | 13 +- .../ChallengeList/index.js | 208 +++++++++--------- .../ChallengesComponent.module.scss | 15 +- src/components/ChallengesComponent/index.js | 66 +++--- src/containers/Challenges/index.js | 11 +- 5 files changed, 161 insertions(+), 152 deletions(-) diff --git a/src/components/ChallengeEditor/index.js b/src/components/ChallengeEditor/index.js index e6436898..bae7f1e3 100644 --- a/src/components/ChallengeEditor/index.js +++ b/src/components/ChallengeEditor/index.js @@ -27,7 +27,8 @@ import { MILESTONE_STATUS, PHASE_PRODUCT_CHALLENGE_ID_FIELD, QA_TRACK_ID, DESIGN_CHALLENGE_TYPES, ROUND_TYPES, - MULTI_ROUND_CHALLENGE_TEMPLATE_ID, DS_TRACK_ID + MULTI_ROUND_CHALLENGE_TEMPLATE_ID, DS_TRACK_ID, + CHALLENGE_STATUS } from '../../config/constants' import { getDomainTypes, getResourceRoleByName } from '../../util/tc' import { PrimaryButton, OutlineButton } from '../Buttons' @@ -1493,7 +1494,7 @@ class ChallengeEditor extends Component { /> ) } - + const statusMessage = challenge.status && challenge.status.split(' ')[0].toUpperCase() const errorContainer =
    {error}
    const actionButtons = @@ -1534,9 +1535,11 @@ class ChallengeEditor extends Component { )}
    )} -
    - -
    + {statusMessage !== CHALLENGE_STATUS.CANCELLED && +
    + +
    + }
    } {!isLoading && isActive &&
    diff --git a/src/components/ChallengesComponent/ChallengeList/index.js b/src/components/ChallengesComponent/ChallengeList/index.js index b41c9c5a..46fe1201 100644 --- a/src/components/ChallengesComponent/ChallengeList/index.js +++ b/src/components/ChallengesComponent/ChallengeList/index.js @@ -21,6 +21,7 @@ import ChallengeCard from '../ChallengeCard' import Message from '../Message' import SortIcon from '../../../assets/images/sort-icon.svg' import Select from '../../Select' +import Loader from '../../Loader' import { CHALLENGE_STATUS, PAGE_SIZE, PAGINATION_PER_PAGE_OPTIONS } from '../../../config/constants' import { checkAdmin } from '../../../util/tc' @@ -352,6 +353,7 @@ class ChallengeList extends Component { projects, dashboard, perPage, + isLoading, totalChallenges, partiallyUpdateChallengeDetails, deleteChallenge, @@ -678,109 +680,112 @@ class ChallengeList extends Component {
    - {challenges.length === 0 && ( - - )} - {challenges.length > 0 && ( -
    -
    this.updateSort('type')} - > - - Type - {this.renderSortIcon('type')} - -
    -
    this.updateSort('name')} - > - - Challenge Name - {this.renderSortIcon('name')} - -
    -
    this.updateSort('startDate')} - > - - Start Date - {this.renderSortIcon('startDate')} - -
    -
    this.updateSort('endDate')} - > - - End Date - {this.renderSortIcon('endDate')} - -
    -
    this.updateSort('numOfRegistrants')} - > - - - {this.renderSortIcon('numOfRegistrants')} - -
    -
    this.updateSort('numOfSubmissions')} - > - - - {this.renderSortIcon('numOfSubmissions')} - -
    -
    this.updateSort('status')} - > - - Status - {this.renderSortIcon('status')} - -
    -
     
    -
     
    -
     
    +
    +
    this.updateSort('type')} + > + + Type + {this.renderSortIcon('type')} +
    - )} - {challenges.length > 0 && ( -
      - {map(challenges, c => { - return ( -
    • - -
    • +
      this.updateSort('name')} + > + + Challenge Name + {this.renderSortIcon('name')} + +
      +
      this.updateSort('startDate')} + > + + Start Date + {this.renderSortIcon('startDate')} + +
      +
      this.updateSort('endDate')} + > + + End Date + {this.renderSortIcon('endDate')} + +
      +
      this.updateSort('numOfRegistrants')} + > + + + {this.renderSortIcon('numOfRegistrants')} + +
      +
      this.updateSort('numOfSubmissions')} + > + + + {this.renderSortIcon('numOfSubmissions')} + +
      +
      this.updateSort('status')} + > + + Status + {this.renderSortIcon('status')} + +
      +
       
      +
       
      +
       
      +
    + + {isLoading + ? + : <> + {challenges.length > 0 + ? <> +
      + {map(challenges, c => { + return ( +
    • + +
    • + ) + })} +
    + : ( + ) - })} - - )} + } + + } +