-
-
-
-
- {activeProject && activeProject.id && (
-
- (View Project)
-
- )}
-
- {(activeProject && activeProject.id) ? (
-
-
-
- ) : (
-
- )}
-
-
- {isLoading ? (
-
- ) : (
-
+
+
+ {!dashboard &&
+
+ {activeProject && activeProject.id && !isReadOnly ? (
+
+
+
+ ) : (
+
+ )}
+
}
+
+
-
+
)
}
ChallengesComponent.propTypes = {
challenges: PropTypes.arrayOf(PropTypes.object),
+ projects: PropTypes.arrayOf(PropTypes.object),
activeProject: PropTypes.shape({
id: PropTypes.number,
name: PropTypes.string
@@ -105,6 +140,11 @@ ChallengesComponent.propTypes = {
isLoading: PropTypes.bool,
warnMessage: PropTypes.string,
filterChallengeName: PropTypes.string,
+ filterChallengeType: PropTypes.shape(),
+ filterProjectOption: PropTypes.shape(),
+ filterDate: PropTypes.shape(),
+ filterSortBy: PropTypes.string,
+ filterSortOrder: PropTypes.string,
status: PropTypes.string,
activeProjectId: PropTypes.number,
loadChallengesByPage: PropTypes.func.isRequired,
@@ -113,18 +153,22 @@ ChallengesComponent.propTypes = {
totalChallenges: PropTypes.number.isRequired,
partiallyUpdateChallengeDetails: PropTypes.func.isRequired,
deleteChallenge: PropTypes.func.isRequired,
+ setActiveProject: PropTypes.func.isRequired,
isBillingAccountExpired: PropTypes.bool,
+ dashboard: PropTypes.bool,
billingStartDate: PropTypes.string,
billingEndDate: PropTypes.string,
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/components/Handle/Handle.module.scss b/src/components/Handle/Handle.module.scss
index 6441d9c7..39870917 100644
--- a/src/components/Handle/Handle.module.scss
+++ b/src/components/Handle/Handle.module.scss
@@ -3,6 +3,6 @@
.handle {
@include roboto-medium;
display: inline-block;
- color: $text-color;
text-decoration: none;
+ color: $white;
}
diff --git a/src/components/Modal/AlertModal.js b/src/components/Modal/AlertModal.js
index a603a51f..084263c2 100644
--- a/src/components/Modal/AlertModal.js
+++ b/src/components/Modal/AlertModal.js
@@ -6,7 +6,7 @@ import styles from './ConfirmationModal.module.scss'
import OutlineButton from '../Buttons/OutlineButton'
import PrimaryButton from '../Buttons/PrimaryButton'
-const AlertModal = ({ title, message, theme, onClose, closeLink, okLink, closeText, okText }) => (
+const AlertModal = ({ title, message, theme, onClose, closeLink, okLink, closeText, okText, onOk }) => (
{title}
@@ -28,6 +28,7 @@ const AlertModal = ({ title, message, theme, onClose, closeLink, okLink, closeTe
text={okText}
type={'success'}
link={okLink}
+ onClick={okLink ? () => {} : onOk}
/>
)}
@@ -41,6 +42,7 @@ AlertModal.propTypes = {
message: PropTypes.string,
theme: PropTypes.shape(),
onClose: PropTypes.func,
+ onOk: PropTypes.func,
closeText: PropTypes.string,
closeLink: PropTypes.string,
okText: PropTypes.string,
diff --git a/src/components/Modal/ConfirmationModal.module.scss b/src/components/Modal/ConfirmationModal.module.scss
index 38d46ea2..0fc48a82 100644
--- a/src/components/Modal/ConfirmationModal.module.scss
+++ b/src/components/Modal/ConfirmationModal.module.scss
@@ -58,9 +58,10 @@
width: 193px;
height: 40px;
margin-right: 33px;
-
+
span {
- font-size: 18px;
+ font-size: 16px;
+ line-height: 1;
font-weight: 500;
}
}
diff --git a/src/components/PhaseInput/index.js b/src/components/PhaseInput/index.js
index aeaa8619..7eb3c48b 100644
--- a/src/components/PhaseInput/index.js
+++ b/src/components/PhaseInput/index.js
@@ -1,5 +1,5 @@
import moment from 'moment'
-import React from 'react'
+import React, { useEffect } from 'react'
import PropTypes from 'prop-types'
import styles from './PhaseInput.module.scss'
import cn from 'classnames'
@@ -31,6 +31,18 @@ const PhaseInput = ({ onUpdatePhase, phase, readOnly, phaseIndex }) => {
})
}
+ useEffect(() => {
+ if (!startDate && onUpdatePhase) {
+ let startDate = moment().format(dateFormat)
+ let endDate = getEndDate(startDate, duration)
+ onUpdatePhase({
+ startDate,
+ endDate,
+ duration
+ })
+ }
+ }, [startDate])
+
const onDurationChange = (e, isBlur = false) => {
if (e.length > MAX_LENGTH) return null
diff --git a/src/components/Tab/Tab.module.scss b/src/components/Tab/Tab.module.scss
new file mode 100644
index 00000000..f8af09a0
--- /dev/null
+++ b/src/components/Tab/Tab.module.scss
@@ -0,0 +1,145 @@
+@import "../../styles/includes";
+
+.tabs {
+ margin: 0 20px;
+
+ > h1 {
+ @include roboto-bold();
+
+ font-weight: 400;
+ color: #2a2a2a;
+ font-size: 32px;
+ margin: 32px 0 24px;
+ line-height: 32px;
+ font-weight: 600;
+ text-transform: uppercase;
+ }
+
+ hr {
+ border: none;
+ height: 1px;
+ background-color: #e5e5e5;
+ margin: 0;
+ }
+}
+
+.mobileTabExpanded {
+ margin: 0 16px;
+ background-color: #eaf6fd;
+
+ .item {
+ height: 40px;
+ margin: 0;
+
+ p {
+ // @include barlow-bold;
+
+ font-weight: 600;
+ color: #555;
+ font-size: 14px;
+ line-height: 20px;
+ text-transform: uppercase;
+ padding-left: 16px;
+ padding-top: 10px;
+ }
+ }
+
+ .active {
+ background-color: #bae1f9;
+
+ p {
+ color: #2a2a2a;
+ font-weight: 700;
+ }
+ }
+}
+
+.challengeTab {
+ display: flex;
+ height: 42px;
+ padding: 0px;
+ margin-top: 24px;
+ background-color: #eaf6fd;
+ border-radius: 4px 4px 0 0;
+ border-bottom: 1px solid #d4d4d4;
+ position: relative;
+
+ .item {
+ // @include barlow;
+
+ font-weight: 600;
+ font-size: 14px;
+ color: #555;
+ line-height: 20px;
+ padding: 12px 16px 10px 16px;
+ cursor: pointer;
+ display: flex;
+ justify-content: center;
+
+ &:not(.active):hover {
+ background-color: #bae1f9;
+ }
+ }
+
+ .active {
+ color: #0c0c0c;
+ font-weight: 700;
+
+ &::after {
+ content: "";
+ background-image: url(../../assets/images/nav-active-item.svg);
+ height: 10px;
+ width: 40px;
+ margin-top: 10px;
+ justify-content: center;
+ z-index: 100;
+ display: block;
+ position: absolute;
+ top: 31px;
+ }
+ }
+}
+
+.mobileTabContainer {
+ background-color: #eaf6fd;
+ height: 40px;
+ margin-top: 32px;
+ display: flex;
+ justify-content: space-between;
+ border-radius: 4px 4px 0 0;
+ border-bottom: 1px solid #d4d4d4;
+
+ .title {
+ // @include barlow-bold;
+
+ font-weight: 700;
+ color: #0c0c0c;
+ font-size: 14px;
+ line-height: 20px;
+ text-transform: uppercase;
+ padding-left: 16px;
+ padding-top: 10px;
+ }
+
+ .icon {
+ width: 16px;
+ height: 9px;
+ margin-right: 15px;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-self: center;
+ cursor: pointer;
+ transform: scale(1.5);
+ }
+
+ .down {
+ transform: scale(1.5) rotate(180deg);
+ margin-right: 20px !important;
+ }
+
+
+ @media (max-width: 1024px) {
+ margin: 0 16px;
+ }
+}
diff --git a/src/components/Tab/index.js b/src/components/Tab/index.js
new file mode 100644
index 00000000..01649259
--- /dev/null
+++ b/src/components/Tab/index.js
@@ -0,0 +1,94 @@
+import React from 'react'
+import cn from 'classnames'
+import PT from 'prop-types'
+import styles from './Tab.module.scss'
+
+const Tab = ({
+ currentTab,
+ selectTab
+}) => {
+ const onActiveClick = () => {
+ if (currentTab === 1) {
+ return
+ }
+ selectTab(1)
+ }
+
+ const onPastChallengesClick = () => {
+ if (currentTab === 2) {
+ return
+ }
+ selectTab(2)
+ }
+
+ const onUsersClick = () => {
+ if (currentTab === 3) {
+ return
+ }
+ selectTab(3)
+ }
+
+ const tabComponent = (
+
+ - {
+ if (e.key !== 'Enter') {
+ return
+ }
+ onActiveClick()
+ }}
+ role='presentation'
+ >
+ All Work
+
+ - {
+ if (e.key !== 'Enter') {
+ return
+ }
+ onPastChallengesClick()
+ }}
+ role='presentation'
+ >
+ Projects
+
+ - {
+ if (e.key !== 'Enter') {
+ return
+ }
+ onUsersClick()
+ }}
+ role='presentation'
+ >
+ Users
+
+
+ )
+
+ return (
+
+ {tabComponent}
+
+ )
+}
+
+Tab.defaultProps = {
+ selectTab: () => {}
+}
+
+Tab.propTypes = {
+ selectTab: PT.func.isRequired,
+ currentTab: PT.number.isRequired
+}
+
+export default Tab
diff --git a/src/components/TopBar/Topbar.module.scss b/src/components/TopBar/Topbar.module.scss
deleted file mode 100644
index 369066d5..00000000
--- a/src/components/TopBar/Topbar.module.scss
+++ /dev/null
@@ -1,27 +0,0 @@
-@import "../../styles/includes";
-.topbar {
- height: 60px;
- background-color: $white;
- padding: 0 30px 0 30px;
- border-bottom: 1px solid $gray;
-}
-
-.hide-line {
- border-bottom: none;
-}
-
-.details {
- display: block;
- margin-left: auto;
- text-align: right;
- font-size: 16px;
- line-height: 60px;
-
-}
-
-.icon {
- vertical-align: middle;
- color: $blue;
- margin-left: 20px;
- font-size: 20px;
-}
diff --git a/src/components/TopBar/index.js b/src/components/TopBar/index.js
index fda398f7..ccf6137e 100644
--- a/src/components/TopBar/index.js
+++ b/src/components/TopBar/index.js
@@ -1,40 +1,67 @@
-/**
- * Component to render top bar of app
- */
-import React from 'react'
+/* global tcUniNav */
+import React, { useRef, useState, useEffect } from 'react'
import PropTypes from 'prop-types'
-import cn from 'classnames'
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
-import { faSignInAlt } from '@fortawesome/free-solid-svg-icons'
-import { get } from 'lodash'
-import styles from './Topbar.module.scss'
-import Handle from '../Handle'
-import { COMMUNITY_APP_URL } from '../../config/constants'
-
-const TopBar = ({ user, hideBottomLine }) => {
- return (
-
- {user && (
-
- Welcome,{' '}
-
-
-
-
-
- )}
-
- )
+import { COMMUNITY_APP_URL, HEADER_AUTH_URLS_HREF, HEADER_AUTH_URLS_LOCATION } from '../../config/constants'
+
+let uniqueId = 0
+
+const HEADER_AUTH_URLS = {
+ href: HEADER_AUTH_URLS_HREF,
+ location: HEADER_AUTH_URLS_LOCATION
+}
+const BASE = COMMUNITY_APP_URL
+
+const TopBar = ({ auth }) => {
+ const uniNavInitialized = useRef(false)
+ const toolNameRef = useRef('Work Manager')
+ const authURLs = HEADER_AUTH_URLS
+ const headerRef = useRef()
+ const [headerId, setHeaderId] = useState(0)
+
+ useEffect(() => {
+ uniqueId += 1
+ setHeaderId(uniqueId)
+ }, [])
+
+ useEffect(() => {
+ if (uniNavInitialized.current || !headerId) {
+ return
+ }
+
+ uniNavInitialized.current = true
+
+ const regSource = window.location.pathname.split('/')[1]
+ const retUrl = encodeURIComponent(window.location.href)
+ tcUniNav('init', `headerNav-${headerId}`, {
+ type: 'tool',
+ toolName: toolNameRef.current,
+ toolRoot: '/',
+ user: 'auto',
+ signOut: () => {
+ window.location = `${BASE}/logout?ref=nav`
+ },
+ signIn: () => {
+ window.location = `${authURLs.location
+ .replace('%S', retUrl)
+ .replace('member?', '#!/member?')}®Source=${regSource}`
+ },
+ signUp: () => {
+ window.location = `${authURLs.location
+ .replace('%S', retUrl)
+ .replace('member?', '#!/member?')}&mode=signUp®Source=${regSource}`
+ }
+ })
+ }, [headerId])
+
+ return
+}
+
+TopBar.defaultProps = {
+ auth: {}
}
TopBar.propTypes = {
- user: PropTypes.object,
- hideBottomLine: PropTypes.bool
+ auth: PropTypes.shape()
}
export default TopBar
diff --git a/src/components/TopcoderFooter/index.js b/src/components/TopcoderFooter/index.js
new file mode 100644
index 00000000..27f6c502
--- /dev/null
+++ b/src/components/TopcoderFooter/index.js
@@ -0,0 +1,36 @@
+/* global tcUniNav */
+import React, { useEffect, useRef, useState } from 'react'
+import styles from './styles.module.scss'
+
+let uniqueId = 0
+
+export default function TopcoderFooter () {
+ const footerRef = useRef()
+ const footerInitialized = useRef(false)
+ const [footerId, setFooterId] = useState(0)
+
+ useEffect(() => {
+ uniqueId += 1
+ setFooterId(uniqueId)
+ }, [])
+
+ useEffect(() => {
+ if (footerInitialized.current || !footerId) {
+ return
+ }
+
+ footerInitialized.current = true
+
+ tcUniNav('init', `footerNav-${footerId}`, {
+ type: 'footer'
+ })
+ }, [footerId])
+
+ return (
+
+ )
+}
diff --git a/src/components/TopcoderFooter/styles.module.scss b/src/components/TopcoderFooter/styles.module.scss
new file mode 100644
index 00000000..d6960ed7
--- /dev/null
+++ b/src/components/TopcoderFooter/styles.module.scss
@@ -0,0 +1,5 @@
+@import '../../styles/includes';
+
+.container {
+ margin-top: auto;
+}
diff --git a/src/components/TwoColsLayout/TwoColsLayout.module.scss b/src/components/TwoRowsLayout/TwoRowsLayout.module.scss
similarity index 53%
rename from src/components/TwoColsLayout/TwoColsLayout.module.scss
rename to src/components/TwoRowsLayout/TwoRowsLayout.module.scss
index 21fd886c..a85ead3b 100644
--- a/src/components/TwoColsLayout/TwoColsLayout.module.scss
+++ b/src/components/TwoRowsLayout/TwoRowsLayout.module.scss
@@ -1,33 +1,19 @@
.container {
display: flex;
+ flex-direction: column;
&.scrollIndependent {
- .sidebar,
.content {
- min-height: 0;
- height: 100vh;
- overflow: auto;
+ // min-height: 0;
+ // height: 100vh;
+ // overflow: auto;
}
}
}
-.sidebar {
- height: 100vh;
- position: relative;
- width: 280px;
-
- @media screen and (max-width: 300px * 4) {
- width: auto;
- flex: 1;
- }
-
-}
-
.content {
- /* minus sidebar width and minus sidebar border width */
- width: calc(100% - 280px);
- max-height: 100vh;
+ height: 100vh;
display: flex;
position: relative;
flex-direction: column;
diff --git a/src/components/TwoColsLayout/index.js b/src/components/TwoRowsLayout/index.js
similarity index 51%
rename from src/components/TwoColsLayout/index.js
rename to src/components/TwoRowsLayout/index.js
index 2c740b6d..e1df88a1 100644
--- a/src/components/TwoColsLayout/index.js
+++ b/src/components/TwoRowsLayout/index.js
@@ -6,9 +6,9 @@ import React from 'react'
import PropTypes from 'prop-types'
import cn from 'classnames'
-import styles from './TwoColsLayout.module.scss'
+import styles from './TwoRowsLayout.module.scss'
-const TwoColsLayout = ({
+const TwoRowsLayout = ({
children,
scrollIndependent
}) => (
@@ -17,40 +17,28 @@ const TwoColsLayout = ({
)
-TwoColsLayout.Sidebar = ({ children }) => (
-
-)
-
-TwoColsLayout.Sidebar.defaultProps = {
- children: null
-}
-
-TwoColsLayout.Sidebar.propTypes = {
- children: PropTypes.node
-}
-
-TwoColsLayout.Content = ({ children }) => (
+TwoRowsLayout.Content = ({ children }) => (
{children}
)
-TwoColsLayout.Content.defaultProps = {
+TwoRowsLayout.Content.defaultProps = {
children: null
}
-TwoColsLayout.Content.propTypes = {
+TwoRowsLayout.Content.propTypes = {
children: PropTypes.node
}
-TwoColsLayout.defaultProps = {
+TwoRowsLayout.defaultProps = {
children: null,
scrollIndependent: false
}
-TwoColsLayout.propTypes = {
+TwoRowsLayout.propTypes = {
children: PropTypes.node,
scrollIndependent: PropTypes.bool
}
-export default TwoColsLayout
+export default TwoRowsLayout
diff --git a/src/components/UserCard/UserCard.module.scss b/src/components/UserCard/UserCard.module.scss
new file mode 100644
index 00000000..da47949c
--- /dev/null
+++ b/src/components/UserCard/UserCard.module.scss
@@ -0,0 +1,216 @@
+@import "../../styles/includes";
+
+.item {
+ display: flex;
+ font-size: 14px;
+ gap: 30px;
+ height: 30px;
+ padding: 0 20px;
+
+ .col5 {
+ display: flex;
+ width: 150px;
+ flex-direction: column;
+ justify-content: center;
+ }
+}
+
+.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;
+ margin: 0;
+ padding: 0;
+ vertical-align: bottom;
+ position: relative;
+ display: inline-block;
+
+ input[type=radio] {
+ display: none;
+ }
+
+ .isDisabled {
+ pointer-events: 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;
+ }
+}
+
+.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;
+ }
+ }
+ }
+}
diff --git a/src/components/UserCard/index.js b/src/components/UserCard/index.js
new file mode 100644
index 00000000..318ce474
--- /dev/null
+++ b/src/components/UserCard/index.js
@@ -0,0 +1,184 @@
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import cn from 'classnames'
+import styles from './UserCard.module.scss'
+import { PROJECT_ROLES } from '../../config/constants'
+import PrimaryButton from '../Buttons/PrimaryButton'
+import AlertModal from '../Modal/AlertModal'
+import { updateProjectMemberRole } from '../../services/projects'
+import { wait } from '../../util/helper'
+import _ from 'lodash'
+
+const theme = {
+ container: styles.modalContainer
+}
+
+class UserCard extends Component {
+ constructor (props) {
+ super(props)
+ this.state = {
+ isUpdatingPermission: false,
+ showWarningModal: false,
+ permissionUpdateError: null,
+ showSuccessModal: false
+ }
+ this.updatePermission = this.updatePermission.bind(this)
+ this.resetPermState = this.resetPermState.bind(this)
+ }
+
+ resetPermState () {
+ this.setState({
+ isUpdatingPermission: false,
+ showWarningModal: false,
+ permissionUpdateError: null,
+ showSuccessModal: false
+ })
+ }
+
+ async updatePermission (newRole) {
+ if (this.state.isUpdatingPermission) { return }
+
+ this.setState({
+ isUpdatingPermission: true
+ })
+
+ const { user, reloadProjectMembers } = this.props
+
+ try {
+ await updateProjectMemberRole(user.projectId, user.id, newRole)
+ await wait(1000)
+ reloadProjectMembers(user.projectId)
+ this.setState({ showSuccessModal: true })
+ } catch (e) {
+ const error = _.get(
+ e,
+ 'response.data.message',
+ `Unable to update permission`
+ )
+ this.setState({ showWarningModal: true, permissionUpdateError: error })
+ }
+ }
+
+ render () {
+ const { user, onRemoveClick, isEditable } = this.props
+ const showRadioButtons = _.includes(_.values(PROJECT_ROLES), user.role)
+ return (
+
+ {
+ this.state.isUpdatingPermission && (
+
+ )
+ }
+ {this.state.showWarningModal && (
+
+ )}
+ {this.state.showSuccessModal && (
+
+ )}
+
+
+ {user.handle}
+
+
+ {showRadioButtons && (
+
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)}
+ />
+
+
)}
+
+ {isEditable ? (
+
{ onRemoveClick(user) }} />
+ ) : null}
+
+
+ )
+ }
+}
+
+UserCard.propTypes = {
+ user: PropTypes.object,
+ reloadProjectMembers: PropTypes.func.isRequired,
+ onRemoveClick: PropTypes.func.isRequired,
+ isEditable: PropTypes.bool
+}
+
+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..b0a95933
--- /dev/null
+++ b/src/components/Users/index.js
@@ -0,0 +1,429 @@
+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, isEditable } = 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 = isEditable && this.state.projectOption && (isCopilotOrManager || isAdmin)
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ {
+ showAddUser && (
+
+
this.onAddUserClick()} />
+
+ )
+ }
+ {
+ this.state.showAddUserModal && (
+
this.resetAddUserState()}>
+
+
Add User
+
+
+ {
+ this.state.showSelectUserError && (
+
+
Please select a member.
+
+ )
+ }
+
+
+
+
+
+
+
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,
+ isEditable: PropTypes.bool,
+ 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 a37f2768..e25915cd 100644
--- a/src/config/constants.js
+++ b/src/config/constants.js
@@ -20,7 +20,10 @@ export const {
CHALLENGE_TYPE_ID,
MARATHON_TYPE_ID,
SEGMENT_API_KEY,
- MULTI_ROUND_CHALLENGE_TEMPLATE_ID
+ MULTI_ROUND_CHALLENGE_TEMPLATE_ID,
+ UNIVERSAL_NAV_URL,
+ HEADER_AUTH_URLS_HREF,
+ HEADER_AUTH_URLS_LOCATION
} = process.env
export const CREATE_FORUM_TYPE_IDS = typeof process.env.CREATE_FORUM_TYPE_IDS === 'string' ? process.env.CREATE_FORUM_TYPE_IDS.split(',') : process.env.CREATE_FORUM_TYPE_IDS
@@ -94,11 +97,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 +186,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',
@@ -211,7 +224,12 @@ export const ALLOWED_USER_ROLES = [
'administrator',
'connect admin',
'connect manager',
- 'connect copilot'
+ 'connect copilot',
+ 'topcoder user'
+]
+
+export const READ_ONLY_ROLES = [
+ 'topcoder user'
]
export const ADMIN_ROLES = [
@@ -222,7 +240,7 @@ export const ADMIN_ROLES = [
export const downloadAttachmentURL = (challengeId, attachmentId, token) =>
`${CHALLENGE_API_URL}/${challengeId}/attachments/${attachmentId}/download?token=${token}`
-export const PAGE_SIZE = 50
+export const PAGE_SIZE = 10
/**
* The minimal number of characters to enter before starting showing autocomplete suggestions
@@ -301,3 +319,10 @@ export const MULTI_ROUND_CHALLENGE_DESC_TEMPLATE = '\n\n### ROUND 1\n' +
export const MAX_CHECKPOINT_PRIZE_COUNT = 8
export const DEFAULT_CHECKPOINT_PRIZE = 50
export const DEFAULT_CHECKPOINT_PRIZE_COUNT = 5
+
+export const PAGINATION_PER_PAGE_OPTIONS = [
+ { label: '5', value: '5' },
+ { label: '10', value: '10' },
+ { label: '25', value: '25' },
+ { label: '50', value: '50' }
+]
diff --git a/src/containers/ChallengeEditor/index.js b/src/containers/ChallengeEditor/index.js
index 66843d05..361dd337 100644
--- a/src/containers/ChallengeEditor/index.js
+++ b/src/containers/ChallengeEditor/index.js
@@ -36,7 +36,7 @@ import { loadSubmissions } from '../../actions/challengeSubmissions'
import { loadProject } from '../../actions/projects'
import { connect } from 'react-redux'
-import { SUBMITTER_ROLE_UUID, MESSAGE } from '../../config/constants'
+import { SUBMITTER_ROLE_UUID, MESSAGE, PROJECT_ROLES } from '../../config/constants'
import { patchChallenge } from '../../services/challenges'
import ConfirmationModal from '../../components/Modal/ConfirmationModal'
import AlertModal from '../../components/Modal/AlertModal'
@@ -59,7 +59,8 @@ class ChallengeEditor extends Component {
showSuccessModal: false,
showLaunchModal: false,
showRejectModal: false,
- cancelReason: null
+ cancelReason: null,
+ loginUserRoleInProject: ''
}
this.onLaunchChallenge = this.onLaunchChallenge.bind(this)
@@ -125,7 +126,7 @@ class ChallengeEditor extends Component {
componentWillReceiveProps (nextProps) {
const { match } = this.props
- const { match: newMatch, loadChallengeDetails, loadResources, loadSubmissions } = nextProps
+ const { match: newMatch, loadChallengeDetails, loadResources, loadSubmissions, projectDetail, loggedInUser } = nextProps
const projectId = _.get(newMatch.params, 'projectId', null)
const challengeId = _.get(newMatch.params, 'challengeId', null)
if (
@@ -136,6 +137,15 @@ class ChallengeEditor extends Component {
} else {
this.setState({ challengeDetails: nextProps.challengeDetails })
}
+ if (projectDetail && loggedInUser) {
+ const projectMembers = projectDetail.members
+ const loginUserProjectInfo = _.find(projectMembers, { userId: loggedInUser.userId })
+ if (loginUserProjectInfo && this.state.loginUserRoleInProject !== loginUserProjectInfo.role) {
+ this.setState({
+ loginUserRoleInProject: loginUserProjectInfo.role
+ })
+ }
+ }
}
async fetchProjectDetails (newMatch) {
@@ -175,6 +185,10 @@ class ChallengeEditor extends Component {
if (isAdmin) {
return true
}
+ const { loginUserRoleInProject } = this.state
+ if (loginUserRoleInProject === PROJECT_ROLES.READ) {
+ return false
+ }
const userRoles = _.filter(
challengeResources,
cr => cr.memberId === `${loggedInUser.userId}`
@@ -532,6 +546,7 @@ class ChallengeEditor extends Component {
rejectChallenge={this.rejectChallenge}
showRejectChallengeModal={showRejectChallengeModal}
loggedInUser={loggedInUser}
+ enableEdit={enableEdit}
/>
)}
/>
@@ -571,6 +586,7 @@ class ChallengeEditor extends Component {
loggedInUser={loggedInUser}
projectPhases={projectPhases}
assignYourselfCopilot={this.assignYourselfCopilot}
+ enableEdit={enableEdit}
/>
)}
/>
diff --git a/src/containers/Challenges/index.js b/src/containers/Challenges/index.js
index 6a12f204..89dee8db 100644
--- a/src/containers/Challenges/index.js
+++ b/src/containers/Challenges/index.js
@@ -6,14 +6,21 @@ import React, { Component, Fragment } from 'react'
// import { Redirect } from 'react-router-dom'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
-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 Loader from '../../components/Loader'
+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 {
+ loadProjects,
+ setActiveProject,
+ resetSidebarActiveParams
+} from '../../actions/sidebar'
import styles from './Challenges.module.scss'
import { checkAdmin } from '../../util/tc'
@@ -21,39 +28,70 @@ class Challenges extends Component {
constructor (props) {
super(props)
this.state = {
- searchProjectName: '',
onlyMyProjects: true
}
- this.updateProjectName = this.updateProjectName.bind(this)
- this.toggleMyProjects = this.toggleMyProjects.bind(this)
}
componentDidMount () {
- const { activeProjectId, resetSidebarActiveParams, menu, projectId, selfService } = this.props
+ const {
+ dashboard,
+ activeProjectId,
+ resetSidebarActiveParams,
+ menu,
+ projectId,
+ selfService,
+ loadChallengeTypes
+ } = this.props
+ loadChallengeTypes()
+ if (dashboard) {
+ this.reloadChallenges(this.props, true)
+ }
if (menu === 'NULL' && activeProjectId !== -1) {
resetSidebarActiveParams()
} else if (projectId || selfService) {
- if (projectId) {
+ if (projectId && projectId !== -1) {
window.localStorage.setItem('projectLoading', 'true')
this.props.loadProject(projectId)
}
- this.reloadChallenges(this.props)
+ this.reloadChallenges(this.props, true)
}
}
componentWillReceiveProps (nextProps) {
- if (this.props.activeProjectId !== nextProps.activeProjectId) {
+ if (
+ (nextProps.dashboard && this.props.dashboard !== nextProps.dashboard) ||
+ this.props.activeProjectId !== nextProps.activeProjectId
+ ) {
this.reloadChallenges(nextProps)
}
}
- reloadChallenges (props) {
- const { activeProjectId, projectDetail: reduxProjectInfo, projectId, challengeProjectId, loadProject, selfService } = props
- if (activeProjectId !== challengeProjectId || selfService) {
+ reloadChallenges (props, forceLoad) {
+ const {
+ activeProjectId,
+ projectDetail: reduxProjectInfo,
+ projectId,
+ dashboard,
+ challengeProjectId,
+ loadProject,
+ selfService
+ } = props
+ if (activeProjectId !== challengeProjectId || selfService || forceLoad) {
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)
- const projectLoading = window.localStorage.getItem('projectLoading') !== null
- if (!selfService && (!reduxProjectInfo || `${reduxProjectInfo.id}` !== projectId) && !projectLoading
+ this.props.loadChallengesByPage(
+ 1,
+ projectId ? parseInt(projectId) : -1,
+ dashboard ? 'all' : '',
+ '',
+ selfService,
+ isAdmin ? null : this.props.auth.user.handle
+ )
+ const projectLoading =
+ window.localStorage.getItem('projectLoading') !== null
+ if (
+ !selfService &&
+ (!reduxProjectInfo || `${reduxProjectInfo.id}` !== projectId) &&
+ !projectLoading
) {
loadProject(projectId)
} else {
@@ -62,23 +100,17 @@ class Challenges extends Component {
}
}
- updateProjectName (val) {
- this.setState({ searchProjectName: val })
- this.props.loadProjects(val, this.state.onlyMyProjects)
- }
-
- toggleMyProjects (evt) {
- this.setState({ onlyMyProjects: evt.target.checked }, () => {
- this.props.loadProjects(this.state.searchProjectName, this.state.onlyMyProjects)
- })
- }
-
render () {
const {
challenges,
isLoading,
warnMessage,
filterChallengeName,
+ filterChallengeType,
+ filterDate,
+ filterSortBy,
+ filterSortOrder,
+ filterProjectOption,
projects,
activeProjectId,
status,
@@ -95,87 +127,84 @@ class Challenges extends Component {
billingEndDate,
isBillingAccountLoadingFailed,
isBillingAccountLoading,
+ dashboard,
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 => (
-
-
-
- ))
+ const projectComponents =
+ !dashboard &&
+ projects.map(p => (
+
+
+
+ ))
return (
-
- {
- !selfService && (
-
-
- this.updateProjectName(e.target.value)}
- value={searchProjectName}
- />
-
-
-
- )
- }
- {
- activeProjectId === -1 && !selfService &&
No project selected. Select one below
- }
- {
- isLoading ?
: (
-
- )
- }
-
- {(activeProjectId !== -1 || selfService) &&
- }
+ {!dashboard &&
+ (!!projectComponents.length ||
+ (activeProjectId === -1 && !selfService)) ? (
+
+ {activeProjectId === -1 && !selfService && (
+
No project selected. Select one below
+ )}
+
+
+ ) : null}
+ {(dashboard || activeProjectId !== -1 || selfService) && (
+
+ )}
)
}
}
+Challenges.defaultProps = {
+ isLoading: false
+}
+
Challenges.propTypes = {
projects: PropTypes.arrayOf(PropTypes.shape()),
menu: PropTypes.string,
@@ -187,13 +216,17 @@ Challenges.propTypes = {
projectId: PropTypes.string,
activeProjectId: PropTypes.number,
warnMessage: PropTypes.string,
+ filterChallengeType: PropTypes.shape(),
filterChallengeName: PropTypes.string,
+ filterProjectOption: PropTypes.shape(),
+ filterDate: PropTypes.shape(),
+ filterSortBy: PropTypes.string,
+ filterSortOrder: PropTypes.string,
status: PropTypes.string,
resetSidebarActiveParams: PropTypes.func,
page: PropTypes.number.isRequired,
perPage: PropTypes.number.isRequired,
totalChallenges: PropTypes.number.isRequired,
- loadProjects: PropTypes.func.isRequired,
setActiveProject: PropTypes.func.isRequired,
partiallyUpdateChallengeDetails: PropTypes.func.isRequired,
deleteChallenge: PropTypes.func.isRequired,
@@ -203,7 +236,12 @@ Challenges.propTypes = {
isBillingAccountLoadingFailed: PropTypes.bool,
isBillingAccountLoading: PropTypes.bool,
selfService: PropTypes.bool,
- auth: PropTypes.object.isRequired
+ dashboard: PropTypes.bool,
+ auth: PropTypes.object.isRequired,
+ loadChallengeTypes: PropTypes.func,
+ metadata: PropTypes.shape({
+ challengeTypes: PropTypes.array
+ })
}
const mapStateToProps = ({ challenges, sidebar, projects, auth }) => ({
@@ -217,7 +255,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 +264,7 @@ const mapDispatchToProps = {
resetSidebarActiveParams,
loadProject,
loadProjects,
+ loadChallengeTypes,
setActiveProject,
partiallyUpdateChallengeDetails,
deleteChallenge
diff --git a/src/containers/FooterContainer/index.js b/src/containers/FooterContainer/index.js
new file mode 100644
index 00000000..190061b6
--- /dev/null
+++ b/src/containers/FooterContainer/index.js
@@ -0,0 +1,16 @@
+/**
+ * Container to provide user info to TopBar component
+ */
+import React, { Component } from 'react'
+import TopcoderFooter from '../../components/TopcoderFooter'
+
+class FooterContainer extends Component {
+ render () {
+ return
+ }
+}
+
+FooterContainer.propTypes = {
+}
+
+export default FooterContainer
diff --git a/src/containers/Tab/index.js b/src/containers/Tab/index.js
new file mode 100644
index 00000000..553de850
--- /dev/null
+++ b/src/containers/Tab/index.js
@@ -0,0 +1,127 @@
+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, isLoadProjectsSuccess } = nextProps
+
+ if (nextProps.history.location.pathname === '/') {
+ 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 })
+ }
+ // 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) || isLoadProjectsSuccess) {
+ 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 })
+ } else if (tab === 3) {
+ history.push('/users')
+ this.setState({ currentTab: 3 })
+ }
+
+ resetSidebarActiveParams()
+ }
+
+ render () {
+ const { currentTab } = this.state
+
+ return
+ }
+}
+
+TabContainer.propTypes = {
+ projects: PropTypes.arrayOf(PropTypes.shape()),
+ isLoading: PropTypes.bool,
+ isLoadProjectsSuccess: 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/containers/TopbarContainer/index.js b/src/containers/TopbarContainer/index.js
index 90a4d646..20df77c0 100644
--- a/src/containers/TopbarContainer/index.js
+++ b/src/containers/TopbarContainer/index.js
@@ -21,17 +21,13 @@ class TopbarContainer extends Component {
}
render () {
- const { match } = this.props
- const isChalengeViewPage = match.path === '/projects/:projectId/challenges/:challengeId'
- const { user } = this.props.auth
- return
+ return
}
}
TopbarContainer.propTypes = {
loadUser: PropTypes.func.isRequired,
setActiveProject: PropTypes.func.isRequired,
- match: PropTypes.any.isRequired,
auth: PropTypes.object.isRequired,
activeProjectId: PropTypes.number,
projectId: PropTypes.string
diff --git a/src/containers/Users/index.js b/src/containers/Users/index.js
new file mode 100644
index 00000000..78f2c141
--- /dev/null
+++ b/src/containers/Users/index.js
@@ -0,0 +1,94 @@
+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'
+import { PROJECT_ROLES } from '../../config/constants'
+
+class Users extends Component {
+ constructor (props) {
+ super(props)
+
+ this.state = {
+ loginUserRoleInProject: ''
+ }
+ }
+
+ componentDidMount () {
+ this.props.loadProjects()
+ }
+
+ isEditable () {
+ const { loginUserRoleInProject } = this.state
+ if (loginUserRoleInProject === PROJECT_ROLES.READ) {
+ return false
+ }
+ return true
+ }
+
+ componentWillReceiveProps (nextProps) {
+ const { projectDetail, loggedInUser } = nextProps
+ if (projectDetail && loggedInUser) {
+ const projectMembers = projectDetail.members
+ const loginUserProjectInfo = _.find(projectMembers, { userId: loggedInUser.userId })
+ if (loginUserProjectInfo && this.state.loginUserRoleInProject !== loginUserProjectInfo.role) {
+ this.setState({
+ loginUserRoleInProject: loginUserProjectInfo.role
+ })
+ }
+ }
+ }
+
+ render () {
+ const {
+ projects,
+ loadProject,
+ projectMembers,
+ auth,
+ reloadProjectMembers,
+ projectDetail
+ } = this.props
+ return (
+
+ )
+ }
+}
+
+const mapStateToProps = ({ sidebar, challenges, auth, projects }) => {
+ return {
+ projects: sidebar.projects,
+ projectMembers: _.get(challenges, 'metadata.members'),
+ projectDetail: projects.projectDetail,
+ auth,
+ loggedInUser: auth.user
+ }
+}
+
+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,
+ projectDetail: PT.object,
+ loggedInUser: PT.object
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(Users)
diff --git a/src/index.js b/src/index.js
index def73426..82fb6376 100644
--- a/src/index.js
+++ b/src/index.js
@@ -6,7 +6,7 @@ import ReactDOM from 'react-dom'
import './styles/main.scss'
import 'react-redux-toastr/lib/css/react-redux-toastr.min.css'
import App from './App'
-import { SEGMENT_API_KEY } from './config/constants'
+import { SEGMENT_API_KEY, UNIVERSAL_NAV_URL } from './config/constants'
ReactDOM.render(
, document.getElementById('root'))
@@ -18,3 +18,29 @@ if (!_.isEmpty(SEGMENT_API_KEY)) {
}}();
}
/* eslint-enable */
+
+//
+// eslint-disable-next-line no-unused-expressions
+!(function (n, t, e, a, c, i, o) {
+// eslint-disable-next-line no-unused-expressions, no-sequences
+ ;(n['TcUnivNavConfig'] = c),
+ (n[c] =
+ n[c] ||
+ function () {
+ ;(n[c].q = n[c].q || []).push(arguments)
+ }),
+ (n[c].l = 1 * new Date())
+ // eslint-disable-next-line no-unused-expressions, no-sequences
+ ;(i = t.createElement(e)), (o = t.getElementsByTagName(e)[0])
+ i.async = 1
+ i.type = 'module'
+ i.src = a
+ o.parentNode.insertBefore(i, o)
+})(
+ window,
+ document,
+ 'script',
+ UNIVERSAL_NAV_URL,
+ 'tcUniNav'
+)
+//
diff --git a/src/reducers/challenges.js b/src/reducers/challenges.js
index 71f85fda..56b816dd 100644
--- a/src/reducers/challenges.js
+++ b/src/reducers/challenges.js
@@ -32,7 +32,8 @@ import {
CREATE_CHALLENGE_RESOURCE_FAILURE,
DELETE_CHALLENGE_SUCCESS,
DELETE_CHALLENGE_FAILURE,
- DELETE_CHALLENGE_PENDING
+ DELETE_CHALLENGE_PENDING,
+ MULTI_ROUND_CHALLENGE_TEMPLATE_ID
} from '../config/constants'
const initialState = {
@@ -47,6 +48,10 @@ const initialState = {
attachments: [],
challenge: null,
filterChallengeName: '',
+ filterChallengeType: {},
+ filterDate: {},
+ filterSortBy: 'startDate',
+ filterSortOrder: 'desc',
failedToDelete: false,
status: '',
perPage: 0,
@@ -84,6 +89,11 @@ 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,
+ filterSortOrder: action.filterSortOrder,
perPage: action.perPage,
page: action.page
}
@@ -97,7 +107,18 @@ export default function (state = initialState, action) {
case LOAD_CHALLENGE_DETAILS_SUCCESS: {
return {
...state,
- challengeDetails: action.payload,
+ challengeDetails: { ...action.payload,
+ // change the phase order for the design challenge with multiple phases
+ phases: (action.payload.timelineTemplateId === MULTI_ROUND_CHALLENGE_TEMPLATE_ID && action.payload.phases.length === 8) ? [
+ action.payload.phases.find(x => x.name === 'Registration'),
+ action.payload.phases.find(x => x.name === 'Checkpoint Submission'),
+ action.payload.phases.find(x => x.name === 'Checkpoint Screening'),
+ action.payload.phases.find(x => x.name === 'Checkpoint Review'),
+ action.payload.phases.find(x => x.name === 'Submission'),
+ action.payload.phases.find(x => x.name === 'Screening'),
+ action.payload.phases.find(x => x.name === 'Review'),
+ action.payload.phases.find(x => x.name === 'Approval')] : action.payload.phases
+ },
isLoading: false,
attachments: _.has(action.payload, 'attachments') ? action.payload.attachments : [],
failedToLoad: false
@@ -141,7 +162,18 @@ export default function (state = initialState, action) {
return {
...state,
challenges: updatedChallenges,
- challengeDetails: action.challengeDetails,
+ challengeDetails: { ...action.challengeDetails,
+ // change the phase order for the design challenge with multiple phases
+ phases: (action.challengeDetails.timelineTemplateId === MULTI_ROUND_CHALLENGE_TEMPLATE_ID && action.challengeDetails.phases.length === 8) ? [
+ action.challengeDetails.phases.find(x => x.name === 'Registration'),
+ action.challengeDetails.phases.find(x => x.name === 'Checkpoint Submission'),
+ action.challengeDetails.phases.find(x => x.name === 'Checkpoint Screening'),
+ action.challengeDetails.phases.find(x => x.name === 'Checkpoint Review'),
+ action.challengeDetails.phases.find(x => x.name === 'Submission'),
+ action.challengeDetails.phases.find(x => x.name === 'Screening'),
+ action.challengeDetails.phases.find(x => x.name === 'Review'),
+ action.challengeDetails.phases.find(x => x.name === 'Approval')] : action.challengeDetails.phases
+ },
isLoading: false,
attachments: _.has(action.challengeDetails, 'attachments') ? action.challengeDetails.attachments : [],
failedToLoad: false
@@ -242,7 +274,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/reducers/sidebar.js b/src/reducers/sidebar.js
index 54e2cf45..6bf04611 100644
--- a/src/reducers/sidebar.js
+++ b/src/reducers/sidebar.js
@@ -12,7 +12,8 @@ import {
const initialState = {
activeProjectId: -1,
isLoading: false,
- projects: []
+ projects: [],
+ isLoadProjectsSuccess: false
}
export default function (state = initialState, action) {
@@ -20,7 +21,7 @@ export default function (state = initialState, action) {
case SET_ACTIVE_PROJECT:
return { ...state, activeProjectId: action.projectId, projects: [], isLoading: false }
case LOAD_PROJECTS_SUCCESS:
- return { ...state, projects: action.projects, isLoading: false, isLoggedIn: true }
+ return { ...state, projects: action.projects, isLoading: false, isLoggedIn: true, isLoadProjectsSuccess: true }
case LOAD_PROJECTS_PENDING:
return { ...state, isLoading: true }
case LOAD_PROJECTS_FAILURE:
diff --git a/src/routes.js b/src/routes.js
index fbe0be96..df3d102c 100644
--- a/src/routes.js
+++ b/src/routes.js
@@ -8,18 +8,20 @@ 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 FooterContainer from './containers/FooterContainer'
+import Tab from './containers/Tab'
import Challenges from './containers/Challenges'
import ChallengeEditor from './containers/ChallengeEditor'
import { getFreshToken, decodeToken } from 'tc-auth-lib'
import { saveToken } from './actions/auth'
import { loadChallengeDetails } from './actions/challenges'
import { connect } from 'react-redux'
-import { checkAllowedRoles } from './util/tc'
+import { checkAllowedRoles, checkReadOnlyRoles } from './util/tc'
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
@@ -35,9 +37,11 @@ class RedirectToChallenge extends React.Component {
}
componentWillReceiveProps (nextProps) {
+ const { token } = nextProps
+ const isReadOnly = checkReadOnlyRoles(token)
const projectId = _.get(nextProps.challengeDetails, 'projectId')
const challengeId = _.get(nextProps.challengeDetails, 'id')
- if (projectId && challengeId) {
+ if (projectId && challengeId && isReadOnly) {
console.log('Redircting to full URL')
this.props.history.replace(`/projects/${projectId}/challenges/${challengeId}/view`)
}
@@ -60,7 +64,8 @@ RedirectToChallenge.propTypes = {
loadChallengeDetails: PropTypes.func,
challengeDetails: PropTypes.object,
match: PropTypes.object,
- history: PropTypes.object
+ history: PropTypes.object,
+ token: PropTypes.string
}
const ConnectRedirectToChallenge = connect(mapStateToProps, mapDispatchToProps)(RedirectToChallenge)
@@ -127,6 +132,7 @@ class Routes extends React.Component {
}
const isAllowed = checkAllowedRoles(_.get(decodeToken(this.props.token), 'roles'))
+ const isReadOnly = checkReadOnlyRoles(this.props.token)
const modal = (
renderApp(
,
,
-
+ ,
+
)()}
/>
@@ -163,37 +170,66 @@ class Routes extends React.Component {
{isAllowed &&
renderApp(
- ,
+ ,
+ ,
+ ,
+
+ )()}
+ />
+ renderApp(
+ ,
,
-
+ ,
+
)()}
/>
+ {
+ !isReadOnly && (
+ renderApp(
+ ,
+ ,
+ ,
+
+ )()}
+ />
+ )
+ }
renderApp(
,
,
-
+ ,
+
)()}
/>
- renderApp(
- ,
- ,
-
- )()} />
+ {
+ !isReadOnly && (
+ renderApp(
+ ,
+ ,
+ ,
+
+ )()} />
+ )
+ }
renderApp(
,
,
-
+ ,
+
)()} />
renderApp(
- ,
+ ,
,
-
+ ,
+
)()} />
{/* If path is not defined redirect to landing page */}
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/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/date.js b/src/util/date.js
index 32e2733c..915c3a98 100644
--- a/src/util/date.js
+++ b/src/util/date.js
@@ -116,7 +116,8 @@ export const updateChallengePhaseBeforeSendRequest = (challengeDetail) => {
// challengeDetailTmp.submissionEndDate = moment(challengeDetail.phases[1].scheduledEndDate)
challengeDetailTmp.phases = challengeDetailTmp.phases.map((p) => ({
duration: p.duration * hourToSecond,
- phaseId: p.phaseId
+ phaseId: p.phaseId,
+ scheduledStartDate: p.scheduledStartDate
}))
return challengeDetailTmp
}
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)
+ })
+}
diff --git a/src/util/tc.js b/src/util/tc.js
index 9760a5de..c011f3af 100644
--- a/src/util/tc.js
+++ b/src/util/tc.js
@@ -6,7 +6,8 @@ import {
CHALLENGE_TRACKS,
ALLOWED_USER_ROLES,
ADMIN_ROLES,
- SUBMITTER_ROLE_UUID
+ SUBMITTER_ROLE_UUID,
+ READ_ONLY_ROLES
} from '../config/constants'
import _ from 'lodash'
import { decodeToken } from 'tc-auth-lib'
@@ -149,6 +150,18 @@ export const getDomainTypes = (trackId) => {
export const checkAllowedRoles = roles =>
roles.some(val => ALLOWED_USER_ROLES.indexOf(val.toLowerCase()) > -1)
+/**
+ * Checks if read only role is present in allowed roles
+ * @param token
+ */
+export const checkReadOnlyRoles = token => {
+ const roles = _.get(decodeToken(token), 'roles')
+ if (checkAllowedRoles(roles)) {
+ return false
+ }
+ return roles.some(val => READ_ONLY_ROLES.indexOf(val.toLowerCase()) > -1)
+}
+
/**
* Checks if token has any of the admin roles
* @param token
@@ -244,3 +257,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
+}
diff --git a/src/util/url.js b/src/util/url.js
new file mode 100644
index 00000000..c8f14a9e
--- /dev/null
+++ b/src/util/url.js
@@ -0,0 +1,9 @@
+/**
+ * Get initials from user profile
+ * @param {String} firstName first name
+ * @param {String} lastName last name
+ * @returns {String}
+ */
+export function getInitials (firstName = '', lastName = '') {
+ return `${firstName.slice(0, 1)}${lastName.slice(0, 1)}`
+}