Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion config/constants/local.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ module.exports = {
// Copilots and other apps remain on dev
COPILOTS_URL: 'https://copilots.topcoder-dev.com',

// Projects API v6: keep dev default (switch to LOCAL_PROJECTS_API when needed)
PROJECT_API_URL: `${DEV_API_HOSTNAME}/v6/projects`,

// Local groups/resources/review services
Expand Down
14 changes: 6 additions & 8 deletions src/components/ChallengesComponent/ChallengeList/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import Loader from '../../Loader'
import UpdateBillingAccount from '../../UpdateBillingAccount'

import { CHALLENGE_STATUS, PAGE_SIZE, PAGINATION_PER_PAGE_OPTIONS, PROJECT_ROLES } from '../../../config/constants'
import { checkAdmin, checkManager, checkReadOnlyRoles } from '../../../util/tc'
import { checkAdmin, checkCanManageProjectBillingAccount, checkReadOnlyRoles } from '../../../util/tc'

require('bootstrap/scss/bootstrap.scss')

Expand Down Expand Up @@ -405,10 +405,10 @@ class ChallengeList extends Component {
fetchNextProjects
} = this.props
const isReadOnly = checkReadOnlyRoles(this.props.auth.token) || loginUserRoleInProject === PROJECT_ROLES.READ
const isAdmin = checkAdmin(this.props.auth.token)
const isManager = checkManager(this.props.auth.token)
const loginUserId = this.props.auth.user.userId
const isMemberOfActiveProject = activeProject && activeProject.members && activeProject.members.some(member => member.userId === loginUserId)
const canManageBillingAccount = checkCanManageProjectBillingAccount(
this.props.auth.token,
activeProject
)

if (warnMessage) {
return <Message warnMessage={warnMessage} />
Expand Down Expand Up @@ -495,12 +495,10 @@ class ChallengeList extends Component {
billingStartDate={billingStartDate}
billingEndDate={billingEndDate}
isBillingAccountExpired={isBillingAccountExpired}
isAdmin={isAdmin}
canManageBillingAccount={canManageBillingAccount}
currentBillingAccount={currentBillingAccount}
updateProject={updateProject}
projectId={activeProject.id}
isMemberOfActiveProject={isMemberOfActiveProject}
isManager={isManager}
/>
</div>
) : (
Expand Down
20 changes: 6 additions & 14 deletions src/components/ChallengesComponent/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
* Component to render Challenges page
*/
import React, { useState, useEffect } from 'react'
import _ from 'lodash'
import PropTypes from 'prop-types'
import { Helmet } from 'react-helmet'
import { Link } from 'react-router-dom'
Expand All @@ -11,14 +10,7 @@ import { PROJECT_ROLES, PROJECT_STATUS, COPILOTS_URL, CHALLENGE_STATUS } from '.
import { PrimaryButton, OutlineButton } from '../Buttons'
import ChallengeList from './ChallengeList'
import styles from './ChallengesComponent.module.scss'
import {
checkAdmin,
checkReadOnlyRoles,
checkAdminOrCopilotOrManager,
checkCanViewProjectAssets,
checkManager,
getProjectMemberByUserId
} from '../../util/tc'
import { checkAdmin, checkCanManageProject, checkCanViewProjectAssets, checkReadOnlyRoles, checkManager, getProjectMemberRole } from '../../util/tc'

const ChallengesComponent = ({
challenges,
Expand Down Expand Up @@ -58,8 +50,8 @@ const ChallengesComponent = ({
}) => {
const [loginUserRoleInProject, setLoginUserRoleInProject] = useState('')
const isReadOnly = checkReadOnlyRoles(auth.token) || loginUserRoleInProject === PROJECT_ROLES.READ
const canManageProject = checkCanManageProject(auth.token, activeProject)
const canViewAssets = checkCanViewProjectAssets(auth.token, activeProject)
const canEditProject = checkAdminOrCopilotOrManager(auth.token, activeProject)

const projectStatus = activeProject && activeProject.status
? activeProject.status.toUpperCase()
Expand All @@ -69,9 +61,9 @@ const ChallengesComponent = ({

useEffect(() => {
const loggedInUser = auth.user
const loginUserProjectInfo = getProjectMemberByUserId(activeProject, _.get(loggedInUser, 'userId'))
if (loginUserProjectInfo && loginUserRoleInProject !== loginUserProjectInfo.role) {
setLoginUserRoleInProject(loginUserProjectInfo.role)
const loginUserProjectRole = getProjectMemberRole(activeProject, loggedInUser.userId)
if (loginUserProjectRole && loginUserRoleInProject !== loginUserProjectRole) {
setLoginUserRoleInProject(loginUserProjectRole)
}
}, [activeProject, auth, loginUserRoleInProject])

Expand All @@ -84,7 +76,7 @@ const ChallengesComponent = ({
{activeProject ? activeProject.name : ''}
{activeProject && activeProject.status && <ProjectStatus className={styles.status} status={activeProject.status} />}
</div>
{activeProject && activeProject.id && canEditProject && (
{activeProject && activeProject.id && canManageProject && (
<span>
(
<Link
Expand Down
9 changes: 5 additions & 4 deletions src/components/ChallengesComponent/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,13 @@ jest.mock('react-helmet', () => ({
jest.mock('../../util/tc', () => ({
checkAdmin: jest.fn(),
checkReadOnlyRoles: jest.fn(),
checkAdminOrCopilotOrManager: jest.fn(),
checkCanManageProject: jest.fn(),
checkCanViewProjectAssets: jest.fn(),
checkManager: jest.fn(),
getProjectMemberByUserId: jest.fn((project, userId) => {
getProjectMemberRole: jest.fn((project, userId) => {
const members = (project && project.members) || []
return members.find(member => `${member.userId}` === `${userId}`) || null
const member = members.find(candidate => `${candidate.userId}` === `${userId}`) || null
return member ? member.role : null
})
}))

Expand Down Expand Up @@ -111,7 +112,7 @@ describe('ChallengesComponent', () => {

tcUtils.checkAdmin.mockReturnValue(false)
tcUtils.checkReadOnlyRoles.mockReturnValue(false)
tcUtils.checkAdminOrCopilotOrManager.mockReturnValue(false)
tcUtils.checkCanManageProject.mockReturnValue(false)
tcUtils.checkCanViewProjectAssets.mockReturnValue(true)
tcUtils.checkManager.mockReturnValue(false)
})
Expand Down
16 changes: 6 additions & 10 deletions src/components/UpdateBillingAccount/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,10 @@ const UpdateBillingAccount = ({
billingStartDate,
billingEndDate,
isBillingAccountExpired,
isAdmin,
canManageBillingAccount,
currentBillingAccount,
projectId,
updateProject,
isMemberOfActiveProject,
isManager
updateProject
}) => {
const [isEditing, setIsEditing] = useState(false)
const [selectedBillingAccount, setSelectedBillingAccount] = useState(null)
Expand Down Expand Up @@ -131,7 +129,7 @@ const UpdateBillingAccount = ({
!currentBillingAccount && (
<Fragment>
<span className={styles.error}>No Billing Account set</span>
{(isAdmin || (isManager && isMemberOfActiveProject)) && (
{canManageBillingAccount && (
<span>
{' '}
({' '}
Expand All @@ -155,7 +153,7 @@ const UpdateBillingAccount = ({
>
{isBillingAccountExpired ? 'INACTIVE' : 'ACTIVE'}
</span>{' '}
{(isAdmin || (isManager && isMemberOfActiveProject)) && (
{canManageBillingAccount && (
<span>
{' '}
({' '}
Expand Down Expand Up @@ -187,11 +185,9 @@ UpdateBillingAccount.propTypes = {
billingEndDate: PropTypes.string,
currentBillingAccount: PropTypes.number,
isBillingAccountExpired: PropTypes.bool,
isAdmin: PropTypes.bool,
canManageBillingAccount: PropTypes.bool,
projectId: PropTypes.number,
updateProject: PropTypes.func.isRequired,
isMemberOfActiveProject: PropTypes.bool.isRequired,
isManager: PropTypes.bool.isRequired
updateProject: PropTypes.func.isRequired
}

export default UpdateBillingAccount
90 changes: 90 additions & 0 deletions src/components/UpdateBillingAccount/index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/* global describe, it, expect, beforeEach, afterEach, jest */

import React from 'react'
import ReactDOM from 'react-dom'
import { act } from 'react-dom/test-utils'
import UpdateBillingAccount from './index'

jest.mock('../Select', () => () => null)
jest.mock('../Buttons', () => {
const React = require('react')

const renderButton = (text) => React.createElement(
'button',
{ type: 'button' },
text
)

return {
PrimaryButton: ({ text }) => renderButton(text),
OutlineButton: ({ text }) => renderButton(text)
}
})

describe('UpdateBillingAccount', () => {
let container

const defaultProps = {
billingAccounts: [],
isBillingAccountsLoading: false,
isBillingAccountLoading: false,
isBillingAccountLoadingFailed: false,
billingStartDate: 'Jan 01, 2026',
billingEndDate: 'Dec 31, 2026',
isBillingAccountExpired: false,
canManageBillingAccount: false,
currentBillingAccount: null,
projectId: 1001,
updateProject: () => {}
}

const renderComponent = (props = {}) => {
act(() => {
ReactDOM.render(
<UpdateBillingAccount {...defaultProps} {...props} />,
container
)
})
}

beforeEach(() => {
container = document.createElement('div')
document.body.appendChild(container)
})

afterEach(() => {
ReactDOM.unmountComponentAtNode(container)
container.remove()
container = null
})

it('shows the select action when the user can manage billing accounts and none is assigned', () => {
renderComponent({
canManageBillingAccount: true,
isBillingAccountLoadingFailed: true
})

expect(container.textContent).toContain('No Billing Account set')
expect(container.textContent).toContain('Select Billing Account')
})

it('shows the edit action when the user can manage an assigned billing account', () => {
renderComponent({
canManageBillingAccount: true,
currentBillingAccount: 12345
})

expect(container.textContent).toContain('Billing Account:')
expect(container.textContent).toContain('Edit Billing Account')
})

it('hides management actions when the user cannot manage billing accounts', () => {
renderComponent({
currentBillingAccount: 12345
})

expect(container.textContent).toContain('Billing Account:')
expect(container.textContent).not.toContain('Edit Billing Account')
expect(container.textContent).not.toContain('Select Billing Account')
})
})
46 changes: 39 additions & 7 deletions src/components/UserCard/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { PROJECT_ROLES } from '../../config/constants'
import PrimaryButton from '../Buttons/PrimaryButton'
import AlertModal from '../Modal/AlertModal'
import { updateProjectMemberRole } from '../../services/projects'
import { checkCanSelfAssignProjectRole } from '../../util/tc'

const theme = {
container: styles.modalContainer
Expand All @@ -23,11 +24,23 @@ function normalizeDisplayValue (value) {
return normalizedValue || null
}

function normalizeUserId (userId) {
if (_.isNil(userId)) {
return null
}

const normalizedUserId = String(userId).trim()

return normalizedUserId || null
}

/**
* Renders one project member or invite card with role controls.
*
* `user.handle` may be null/empty for some members; this component falls back
* to `user.userId` and then `"(unknown user)"` when rendering labels/messages.
* Members may lower their own role, but this control blocks self-promotion to
* higher project privileges.
*/
class UserCard extends Component {
constructor (props) {
Expand All @@ -54,12 +67,21 @@ class UserCard extends Component {
async updatePermission (newRole) {
if (this.state.isUpdatingPermission) { return }

const { user, updateProjectMember, currentUserId } = this.props
const isCurrentUser = normalizeUserId(user.userId) === normalizeUserId(currentUserId)

if (isCurrentUser && !checkCanSelfAssignProjectRole(user.role, newRole)) {
this.setState({
showWarningModal: true,
permissionUpdateError: 'You cannot give yourself higher privileges in this project.'
})
return
}

this.setState({
isUpdatingPermission: true
})

const { user, updateProjectMember } = this.props

try {
const newUserInfoRole = await updateProjectMemberRole(user.projectId, user.id, newRole)
updateProjectMember(newUserInfoRole)
Expand All @@ -75,8 +97,13 @@ class UserCard extends Component {
}

render () {
const { isInvite, user, onRemoveClick, isEditable } = this.props
const { isInvite, user, onRemoveClick, isEditable, currentUserId } = this.props
const showRadioButtons = _.includes(_.values(PROJECT_ROLES), user.role)
const isCurrentUser = normalizeUserId(user.userId) === normalizeUserId(currentUserId)
const canAssignRole = role => (
isEditable &&
(!isCurrentUser || checkCanSelfAssignProjectRole(user.role, role))
)
const userDisplayName = normalizeDisplayValue(user.handle) ||
normalizeDisplayValue(user.userId) ||
'(unknown user)'
Expand Down Expand Up @@ -125,9 +152,10 @@ class UserCard extends Component {
type='radio'
id={`read-${user.id}`}
checked={user.role === PROJECT_ROLES.READ}
disabled={!canAssignRole(PROJECT_ROLES.READ)}
onChange={(e) => e.target.checked && this.updatePermission(PROJECT_ROLES.READ)}
/>
<label className={cn({ [styles.isDisabled]: !isEditable })} htmlFor={`read-${user.id}`}>
<label className={cn({ [styles.isDisabled]: !canAssignRole(PROJECT_ROLES.READ) })} htmlFor={`read-${user.id}`}>
<div>
Read
</div>
Expand All @@ -142,9 +170,10 @@ class UserCard extends Component {
type='radio'
id={`write-${user.id}`}
checked={user.role === PROJECT_ROLES.WRITE}
disabled={!canAssignRole(PROJECT_ROLES.WRITE)}
onChange={(e) => e.target.checked && this.updatePermission(PROJECT_ROLES.WRITE)}
/>
<label className={cn({ [styles.isDisabled]: !isEditable })} htmlFor={`write-${user.id}`}>
<label className={cn({ [styles.isDisabled]: !canAssignRole(PROJECT_ROLES.WRITE) })} htmlFor={`write-${user.id}`}>
<div>
Write
</div>
Expand All @@ -159,9 +188,10 @@ class UserCard extends Component {
type='radio'
id={`full-access-${user.id}`}
checked={user.role === PROJECT_ROLES.MANAGER}
disabled={!canAssignRole(PROJECT_ROLES.MANAGER)}
onChange={(e) => e.target.checked && this.updatePermission(PROJECT_ROLES.MANAGER)}
/>
<label className={cn({ [styles.isDisabled]: !isEditable })} htmlFor={`full-access-${user.id}`}>
<label className={cn({ [styles.isDisabled]: !canAssignRole(PROJECT_ROLES.MANAGER) })} htmlFor={`full-access-${user.id}`}>
<div>
Full Access
</div>
Expand All @@ -176,9 +206,10 @@ class UserCard extends Component {
type='radio'
id={`copilot-${user.id}`}
checked={user.role === PROJECT_ROLES.COPILOT}
disabled={!canAssignRole(PROJECT_ROLES.COPILOT)}
onChange={(e) => e.target.checked && this.updatePermission(PROJECT_ROLES.COPILOT)}
/>
<label className={cn({ [styles.isDisabled]: !isEditable })} htmlFor={`copilot-${user.id}`}>
<label className={cn({ [styles.isDisabled]: !canAssignRole(PROJECT_ROLES.COPILOT) })} htmlFor={`copilot-${user.id}`}>
<div>
Copilot
</div>
Expand Down Expand Up @@ -213,6 +244,7 @@ class UserCard extends Component {
UserCard.propTypes = {
isInvite: PropTypes.bool,
user: PropTypes.object,
currentUserId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
updateProjectMember: PropTypes.func.isRequired,
onRemoveClick: PropTypes.func.isRequired,
isEditable: PropTypes.bool
Expand Down
Loading
Loading