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
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ workflows:
context: org-global
filters: &filters-dev
branches:
only: ["develop", "pm-2917", "points", "pm-3270", "projects-api-v6"]
only: ["develop", "pm-2917", "points", "pm-3270", "permissions-hotfix"]

# Production builds are exectuted only on tagged commits to the
# master branch.
Expand Down
1 change: 0 additions & 1 deletion config/constants/local.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,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`,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ maintainability]
The PROJECT_API_URL is set to use the development API host (DEV_API_HOSTNAME). Consider using the local endpoint LOCAL_PROJECTS_API for consistency with other local services, unless there's a specific reason to keep it on the dev environment.


// 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(),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[❗❗ correctness]
The function checkCanManageProject is replacing checkAdminOrCopilotOrManager. Ensure that the new function provides equivalent or improved functionality, as this change may affect permission checks.

checkCanViewProjectAssets: jest.fn(),
checkManager: jest.fn(),
getProjectMemberByUserId: jest.fn((project, userId) => {
getProjectMemberRole: jest.fn((project, userId) => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[❗❗ correctness]
The function getProjectMemberRole now returns the role directly instead of the member object. Verify that all usages of this function are updated accordingly, as this change alters the expected return value.

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')
})
})
26 changes: 26 additions & 0 deletions src/containers/Challenges/helper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* Returns project details only when they match the current project context.
*
* This prevents stale project data from rendering on another project page while
* still handling APIs that may return ids as strings.
*
* @param {Object} projectDetail Loaded project details from redux.
* @param {string|number} projectId Project id from the current route.
* @param {number} activeProjectId Active project id stored in the sidebar state.
* @returns {Object} The matching project detail object, or an empty object.
*/
export function getActiveProject (projectDetail, projectId, activeProjectId) {
if (!projectDetail) {
return {}
}

const scopedProjectId = projectId != null
? `${projectId}`
: (activeProjectId != null && activeProjectId !== -1 ? `${activeProjectId}` : '')

if (!scopedProjectId || `${projectDetail.id}` !== scopedProjectId) {
return {}
}

return projectDetail
}
23 changes: 23 additions & 0 deletions src/containers/Challenges/helper.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/* global describe, it, expect */

import { getActiveProject } from './helper'

describe('getActiveProject', () => {
it('returns the project detail when the route project id matches a string project id', () => {
const projectDetail = {
id: '100566',
name: 'Project Phoenix'
}

expect(getActiveProject(projectDetail, 100566, 100566)).toEqual(projectDetail)
})

it('returns an empty object when the loaded project does not match the current project context', () => {
const projectDetail = {
id: '100566',
name: 'Project Phoenix'
}

expect(getActiveProject(projectDetail, 100567, 100567)).toEqual({})
})
})
13 changes: 8 additions & 5 deletions src/containers/Challenges/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
} from '../../actions/sidebar'
import { checkAdmin, checkIsUserInvitedToProject } from '../../util/tc'
import { withRouter } from 'react-router-dom'
import { getActiveProject } from './helper'

class Challenges extends Component {
constructor (props) {
Expand Down Expand Up @@ -134,6 +135,7 @@ class Challenges extends Component {
setActiveProject,
partiallyUpdateChallengeDetails,
deleteChallenge,
projectId,
isBillingAccountExpired,
billingStartDate,
billingEndDate,
Expand All @@ -150,16 +152,17 @@ class Challenges extends Component {
fetchNextProjects
} = this.props
const { challengeTypes = [] } = metadata
const isActiveProjectLoaded =
reduxProjectInfo && `${reduxProjectInfo.id}` === `${activeProjectId}`
const activeProject = getActiveProject(
reduxProjectInfo,
projectId,
activeProjectId
)

return (
<Fragment>
{(dashboard || activeProjectId !== -1 || selfService) && (
<ChallengesComponent
activeProject={{
...(isActiveProjectLoaded ? reduxProjectInfo : {})
}}
activeProject={activeProject}
fetchNextProjects={fetchNextProjects}
warnMessage={warnMessage}
setActiveProject={setActiveProject}
Expand Down
Loading
Loading