Skip to content

feat(PM-972): copilot invitation with email #1638

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 14, 2025
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: 1 addition & 0 deletions config/constants/development.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ module.exports = {
PROJECT_API_URL: `${DEV_API_HOSTNAME}/v5/projects`,
GROUPS_API_URL: `${DEV_API_HOSTNAME}/v5/groups`,
TERMS_API_URL: `${DEV_API_HOSTNAME}/v5/terms`,
MEMBERS_API_URL: `${DEV_API_HOSTNAME}/v5/members`,
RESOURCES_API_URL: `${DEV_API_HOSTNAME}/v5/resources`,
RESOURCE_ROLES_API_URL: `${DEV_API_HOSTNAME}/v5/resource-roles`,
SUBMISSIONS_API_URL: `${DEV_API_HOSTNAME}/v5/submissions`,
Expand Down
1 change: 1 addition & 0 deletions config/constants/production.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ module.exports = {
PROJECT_API_URL: `${PROD_API_HOSTNAME}/v5/projects`,
GROUPS_API_URL: `${PROD_API_HOSTNAME}/v5/groups`,
TERMS_API_URL: `${PROD_API_HOSTNAME}/v5/terms`,
MEMBERS_API_URL: `${PROD_API_HOSTNAME}/v5/members`,
RESOURCES_API_URL: `${PROD_API_HOSTNAME}/v5/resources`,
RESOURCE_ROLES_API_URL: `${PROD_API_HOSTNAME}/v5/resource-roles`,
SUBMISSIONS_API_URL: `${PROD_API_HOSTNAME}/v5/submissions`,
Expand Down
1 change: 1 addition & 0 deletions src/components/Users/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ class Users extends Component {
<UserAddModalContent
projectId={this.state.projectOption.value}
addNewProjectMember={this.props.addNewProjectMember}
onMemberInvited={this.props.addNewProjectInvite}

Choose a reason for hiding this comment

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

Consider renaming the onMemberInvited prop to something more descriptive, such as onInviteMember or handleMemberInvitation, to clearly convey its purpose and maintain consistency with other handler props.

onClose={this.resetAddUserState}
/>
)
Expand Down
5 changes: 4 additions & 1 deletion src/components/Users/invite-user.modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,10 @@ const InviteUserModalContent = ({ projectId, onClose, onMemberInvited, projectMe

try {
// api restriction: ONLY "customer" role can be invited via email
const { success: invitations = [], failed } = await inviteUserToProject(projectId, emailToInvite, PROJECT_ROLES.CUSTOMER)
const { success: invitations = [], failed } = await inviteUserToProject(projectId, {

Choose a reason for hiding this comment

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

Consider updating the function inviteUserToProject to accept an object as its second parameter, as this change modifies the function call to pass an object instead of separate arguments. Ensure that the function definition and any other calls to it are updated accordingly.

emails: [emailToInvite],
role: PROJECT_ROLES.CUSTOMER
})

if (failed) {
const error = get(failed, '0.message', 'Unable to invite user')
Expand Down
28 changes: 22 additions & 6 deletions src/components/Users/user-add.modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ import Modal from '../Modal'
import SelectUserAutocomplete from '../SelectUserAutocomplete'
import { PROJECT_ROLES } from '../../config/constants'
import PrimaryButton from '../Buttons/PrimaryButton'
import { addUserToProject } from '../../services/projects'
import { addUserToProject, inviteUserToProject } from '../../services/projects'

import styles from './Users.module.scss'

const theme = {
container: styles.modalContainer
}

const UserAddModalContent = ({ projectId, addNewProjectMember, onClose }) => {
const UserAddModalContent = ({ projectId, addNewProjectMember, onMemberInvited, onClose }) => {

Choose a reason for hiding this comment

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

The onMemberInvited prop has been added to the UserAddModalContent component. Ensure that this prop is being used appropriately within the component to handle the copilot invitation flow. If not already implemented, consider adding logic to trigger this callback when a member is successfully invited.

const [userToAdd, setUserToAdd] = useState(null)
const [userPermissionToAdd, setUserPermissionToAdd] = useState(PROJECT_ROLES.READ)
const [showSelectUserError, setShowSelectUserError] = useState(false)
Expand Down Expand Up @@ -45,10 +45,25 @@ const UserAddModalContent = ({ projectId, addNewProjectMember, onClose }) => {
setAddUserError(null)

try {
const newUserInfo = await addUserToProject(projectId, userToAdd.userId, userPermissionToAdd)
newUserInfo.handle = userToAdd.handle
addNewProjectMember(newUserInfo)
onClose()
if (userPermissionToAdd === PROJECT_ROLES.COPILOT) {
const { success: invitations = [], failed } = await inviteUserToProject(projectId, {
handles: [userToAdd.handle],
role: userPermissionToAdd
})
if (failed) {
const error = get(failed, '0.message', 'Unable to invite user')

Choose a reason for hiding this comment

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

Consider checking if failed is an array before attempting to access its elements with get(failed, '0.message', 'Unable to invite user'). This will prevent potential runtime errors if failed is not an array.

setAddUserError(error)
setIsAdding(false)
} else {
onMemberInvited(invitations[0] || {})

Choose a reason for hiding this comment

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

Ensure that invitations is an array and has elements before accessing invitations[0]. This will prevent potential runtime errors if invitations is not an array or is empty.

onClose()
}
} else {
const newUserInfo = await addUserToProject(projectId, userToAdd.userId, userPermissionToAdd)
newUserInfo.handle = userToAdd.handle
addNewProjectMember(newUserInfo)
onClose()
}
} catch (e) {
const error = get(e, 'response.data.message', 'Unable to add user')
setAddUserError(error)
Expand Down Expand Up @@ -169,6 +184,7 @@ const UserAddModalContent = ({ projectId, addNewProjectMember, onClose }) => {
UserAddModalContent.propTypes = {
projectId: PropTypes.number.isRequired,
addNewProjectMember: PropTypes.func.isRequired,
onMemberInvited: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired
}

Expand Down
1 change: 1 addition & 0 deletions src/config/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export const FILE_PICKER_PROGRESS_INTERVAL = 100
export const FILE_PICKER_UPLOAD_RETRY = 2
export const FILE_PICKER_UPLOAD_TIMEOUT = 30 * 60 * 1000 // 30 minutes
export const SPECIFICATION_ATTACHMENTS_FOLDER = 'SPECIFICATION_ATTACHMENTS'
export const MEMBERS_API_URL = process.env.MEMBERS_API_URL

Choose a reason for hiding this comment

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

Ensure that process.env.MEMBERS_API_URL is defined and has a valid value before using it. Consider adding validation logic to handle cases where the environment variable might be undefined or incorrectly set.

Copy link
Contributor

Choose a reason for hiding this comment

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

@hentrymartin can we reuse MEMBERS_API_URL as set via https://github.com/topcoder-platform/work-manager/pull/1638/files#diff-eb618fb9d5e5863df30212207469a147b32305b06fc3606b6a1aaf1fe4f136c4R24?
I want to avoid need to set MEMBERS_API_URL as it can be compiled.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@kkartunov afaik, we don't have to set the MEMBERS_API_URL, we are loading the env variables into process.env(

'process.env': _.mapValues(constants, (value) => JSON.stringify(value))
) from the production.js/development.js file based on the environment. so in a nutshell we don't have to set environment variable anywhere.


export const getAWSContainerFileURL = (key) => `https://${FILE_PICKER_CONTAINER_NAME}.s3.amazonaws.com/${key}`

Expand Down
12 changes: 9 additions & 3 deletions src/containers/Users/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import _ from 'lodash'
import PT from 'prop-types'
import UsersComponent from '../../components/Users'
import { PROJECT_ROLES } from '../../config/constants'
import { fetchProjectById } from '../../services/projects'
import { fetchInviteMembers, fetchProjectById } from '../../services/projects'
import { checkAdmin, checkManager } from '../../util/tc'

import {
Expand Down Expand Up @@ -80,12 +80,18 @@ class Users extends Component {
}

loadProject (projectId) {
fetchProjectById(projectId).then((project) => {
fetchProjectById(projectId).then(async (project) => {
const projectMembers = _.get(project, 'members')
const invitedMembers = _.get(project, 'invites')
const invitedUserIds = _.filter(_.map(invitedMembers, 'userId'))

Choose a reason for hiding this comment

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

Consider checking if invitedUserIds is not empty before calling fetchInviteMembers to avoid unnecessary API calls.

const invitedUsers = await fetchInviteMembers(invitedUserIds)

this.setState({
projectMembers,
invitedMembers
invitedMembers: invitedMembers.map(m => ({
...m,
email: m.email || invitedUsers[m.userId].email

Choose a reason for hiding this comment

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

Ensure that invitedUsers[m.userId] is defined before accessing email to prevent potential runtime errors.

}))
})
const { loggedInUser } = this.props
this.updateLoginUserRoleInProject(projectMembers, loggedInUser)
Expand Down
23 changes: 17 additions & 6 deletions src/services/projects.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import {
GENERIC_PROJECT_MILESTONE_PRODUCT_TYPE,
PHASE_PRODUCT_CHALLENGE_ID_FIELD,
PHASE_PRODUCT_TEMPLATE_ID,
PROJECTS_API_URL
PROJECTS_API_URL,
MEMBERS_API_URL
} from '../config/constants'
import { paginationHeaders } from '../util/pagination'
import { createProjectMemberInvite } from './projectMemberInvites'
Expand Down Expand Up @@ -68,6 +69,19 @@ export async function fetchProjectById (id) {
return _.get(response, 'data')
}

/**
* This fetches the user corresponding to the given userIds
* @param {*} userIds

Choose a reason for hiding this comment

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

Consider specifying the type of userIds in the JSDoc comment. For example, if userIds is expected to be an array of strings, you could write @param {Array<string>} userIds.

*/
export async function fetchInviteMembers (userIds) {
const url = `${MEMBERS_API_URL}?${userIds.map(id => `userIds[]=${id}`).join('&')}`

Choose a reason for hiding this comment

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

Ensure that MEMBERS_API_URL is defined and correctly imported or declared in this file. If it's not, this could lead to runtime errors.

const { data = [] } = await axiosInstance.get(url)

Choose a reason for hiding this comment

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

Consider adding error handling for the API request. This will help manage scenarios where the request fails or returns an unexpected response.

return data.reduce((acc, member) => {
acc[member.userId] = member
return acc
}, {})
}

/**
* Api request for fetching project phases
* @param id Project id
Expand Down Expand Up @@ -118,11 +132,8 @@ export async function addUserToProject (projectId, userId, role) {
* @param role
* @returns {Promise<*>}
*/
export async function inviteUserToProject (projectId, email, role) {
return createProjectMemberInvite(projectId, {
emails: [email],
role: role
})
export async function inviteUserToProject (projectId, params) {

Choose a reason for hiding this comment

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

The function inviteUserToProject now takes a params object instead of separate email and role parameters. Ensure that the params object is validated to contain the necessary properties (emails and role) before passing it to createProjectMemberInvite. This will prevent potential runtime errors if the params object is malformed.

return createProjectMemberInvite(projectId, params)
}

/**
Expand Down