diff --git a/src/components/TeamManagement/CopilotManagementDialog.js b/src/components/TeamManagement/CopilotManagementDialog.js
new file mode 100644
index 000000000..b76cb11a6
--- /dev/null
+++ b/src/components/TeamManagement/CopilotManagementDialog.js
@@ -0,0 +1,238 @@
+import _ from 'lodash'
+import React from 'react'
+import PT from 'prop-types'
+import moment from 'moment'
+import Modal from 'react-modal'
+import XMarkIcon from '../../assets/icons/icon-x-mark.svg'
+import Avatar from 'appirio-tech-react-components/components/Avatar/Avatar'
+import PERMISSIONS from '../../config/permissions'
+
+import { hasPermission } from '../../helpers/permissions'
+import {getAvatarResized, getFullNameWithFallback} from '../../helpers/tcHelpers'
+import { compareEmail, compareHandles } from '../../helpers/utils'
+import AutocompleteInputContainer from './AutocompleteInputContainer'
+
+class ProjectManagementDialog extends React.Component {
+ constructor(props) {
+ super(props)
+ this.state = {
+ showAlreadyMemberError: false,
+ errorMessage: null
+ }
+ this.onChange = this.onChange.bind(this)
+ this.showIndividualErrors = this.showIndividualErrors.bind(this)
+ }
+
+ componentWillReceiveProps(nextProps) {
+ const { processingInvites, selectedMembers } = this.props
+
+ if (processingInvites && !nextProps.processingInvites ) {
+ const notInvitedSelectedMembers = _.reject(selectedMembers, (selectedMember) => (
+ this.isSelectedMemberAlreadyInvited(nextProps.copilotTeamInvites, selectedMember)
+ ))
+
+ this.props.onSelectedMembersUpdate(notInvitedSelectedMembers)
+
+ if (nextProps.error) {
+ this.showIndividualErrors(nextProps.error, notInvitedSelectedMembers)
+ }
+ }
+ }
+
+ onChange(selectedMembers) {
+ const { projectTeamInvites, members, topcoderTeamInvites, copilotTeamInvites } = this.props
+
+ const present = _.some(selectedMembers, (selectedMember) => (
+ this.isSelectedMemberAlreadyInvited(members, selectedMember)
+ || this.isSelectedMemberAlreadyInvited(topcoderTeamInvites, selectedMember)
+ || this.isSelectedMemberAlreadyInvited(projectTeamInvites, selectedMember)
+ || this.isSelectedMemberAlreadyInvited(copilotTeamInvites, selectedMember)
+ ))
+
+ this.setState({
+ validUserText: !present,
+ showAlreadyMemberError: present,
+ errorMessage: null,
+ })
+
+ this.props.onSelectedMembersUpdate(selectedMembers)
+ }
+
+ isSelectedMemberAlreadyInvited(copilotTeamInvites = [], selectedMember) {
+ return !!copilotTeamInvites.find((invite) => (
+ (invite.email && compareEmail(invite.email, selectedMember.label)) ||
+ (invite.userId && compareHandles(invite.handle, selectedMember.label))
+ ))
+ }
+
+ showIndividualErrors(error) {
+ const uniqueMessages = _.groupBy(error.failed, 'message')
+
+ const msgs = _.keys(uniqueMessages).map((message) => {
+ const users = uniqueMessages[message].map((failed) => (
+ failed.email ? failed.email : failed.handle
+ ))
+
+ return ({
+ message,
+ users,
+ })
+ })
+
+ const listMessages = msgs.map((m) => `${m.users.join(', ')}: ${m.message}`)
+
+ this.setState({
+ errorMessage: listMessages.length > 0 ? listMessages.join('\n') : null
+ })
+ }
+
+ render() {
+ const {
+ members, currentUser, isMember, removeMember, removeInvite,
+ onCancel, copilotTeamInvites = [], selectedMembers, processingInvites,
+ } = this.props
+ const showRemove = currentUser.isAdmin || (!currentUser.isCopilot && isMember)
+ const showSuggestions = hasPermission(PERMISSIONS.SEE_MEMBER_SUGGESTIONS)
+ let i = 0
+ return (
+
+
+
+
+ Copilots
+
+
+
+
+ {(members.map((member) => {
+ if (!member.isCopilot) {
+ return null
+ }
+ i++
+ const remove = () => {
+ removeMember(member)
+ }
+ const userFullName = getFullNameWithFallback(member)
+ return (
+
+
+
+
+
+ {userFullName}
+
+ @{member.handle || 'ConnectUser'}
+
+
+
+ {showRemove &&
+ {(currentUser.userId === member.userId) ? 'Leave' : 'Remove'}
+
}
+
+ )
+ }))}
+ {(copilotTeamInvites.map((invite) => {
+ const remove = () => {
+ removeInvite(invite)
+ }
+ i++
+ const hasUserId = !_.isNil(invite.userId)
+ const handle = invite.handle
+ const userFullName = getFullNameWithFallback(invite)
+ return (
+
+
+
+ {hasUserId && {userFullName}}
+
+ {hasUserId && handle && @{handle}}
+ { (!hasUserId) && {invite.email}}
+
+
+ {showRemove &&
+ Remove
+
+ Invited {moment(invite.createdAt).format('MMM D, YY')}
+
+
}
+
+ )
+ }))}
+
+
+
+
invite more copilots
+
+ {this.state.showAlreadyMemberError &&
+ Project Member(s) can't be invited again. Please remove them from list.
+
}
+ { this.state.errorMessage &&
+ {this.state.errorMessage}
+
}
+
+
+
+
+
+ )
+ }
+}
+
+ProjectManagementDialog.defaultProps = {
+ projectTeamInvites: [],
+ topcoderTeamInvites: [],
+ members: []
+}
+
+ProjectManagementDialog.propTypes = {
+ error: PT.oneOfType([PT.object, PT.bool]),
+ currentUser: PT.object.isRequired,
+ members: PT.arrayOf(PT.object).isRequired,
+ allMembers: PT.arrayOf(PT.object).isRequired,
+ isMember: PT.bool.isRequired,
+ onCancel: PT.func.isRequired,
+ removeMember: PT.func.isRequired,
+ projectTeamInvites: PT.arrayOf(PT.object),
+ topcoderTeamInvites: PT.arrayOf(PT.object),
+ sendInvite: PT.func.isRequired,
+ removeInvite: PT.func.isRequired,
+ onSelectedMembersUpdate: PT.func.isRequired,
+ selectedMembers: PT.arrayOf(PT.object),
+ processingInvites: PT.bool.isRequired,
+}
+
+export default ProjectManagementDialog
diff --git a/src/components/TeamManagement/ProjectManagementDialog.js b/src/components/TeamManagement/ProjectManagementDialog.js
index 6cc49abea..537c1038f 100644
--- a/src/components/TeamManagement/ProjectManagementDialog.js
+++ b/src/components/TeamManagement/ProjectManagementDialog.js
@@ -40,12 +40,13 @@ class ProjectManagementDialog extends React.Component {
}
onChange(selectedMembers) {
- const { projectTeamInvites, members, topcoderTeamInvites } = this.props
+ const { projectTeamInvites, members, topcoderTeamInvites, copilotTeamInvites } = this.props
const present = _.some(selectedMembers, (selectedMember) => (
this.isSelectedMemberAlreadyInvited(members, selectedMember)
|| this.isSelectedMemberAlreadyInvited(topcoderTeamInvites, selectedMember)
|| this.isSelectedMemberAlreadyInvited(projectTeamInvites, selectedMember)
+ || this.isSelectedMemberAlreadyInvited(copilotTeamInvites, selectedMember)
))
this.setState({
diff --git a/src/components/TeamManagement/TeamManagement.jsx b/src/components/TeamManagement/TeamManagement.jsx
index 31ca1b9e0..0697d360e 100644
--- a/src/components/TeamManagement/TeamManagement.jsx
+++ b/src/components/TeamManagement/TeamManagement.jsx
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types'
import uncontrollable from 'uncontrollable'
import './TeamManagement.scss'
import ProjectDialog from './ProjectManagementDialog'
+import CopilotDialog from './CopilotManagementDialog'
import TopcoderDialog from './TopcoderManagementDialog'
import MemberItem from './MemberItem'
import AddIcon from '../../assets/icons/icon-ui-bold-add.svg'
@@ -46,9 +47,11 @@ class TeamManagement extends React.Component {
this.state = {
topcoderTeamInviteButtonExpanded: false,
projectTeamInviteButtonExpanded: false,
+ copilotTeamInviteButtonExpanded: false,
}
this.projectTeamInviteButtonClick = this.projectTeamInviteButtonClick.bind(this)
this.topcoderTeamInviteButtonClick = this.topcoderTeamInviteButtonClick.bind(this)
+ this.copilotTeamInviteButtonClick = this.copilotTeamInviteButtonClick.bind(this)
}
topcoderTeamInviteButtonClick() {
@@ -61,6 +64,11 @@ class TeamManagement extends React.Component {
this.setState({projectTeamInviteButtonExpanded: !this.state.projectTeamInviteButtonExpanded})
}
+ copilotTeamInviteButtonClick() {
+ this.refreshStickyComp()
+ this.setState({copilotTeamInviteButtonExpanded: !this.state.copilotTeamInviteButtonExpanded})
+ }
+
refreshStickyComp() {
const event = document.createEvent('Event')
event.initEvent('refreshsticky', true, true)
@@ -84,19 +92,22 @@ class TeamManagement extends React.Component {
projectTeamInvites, onProjectInviteDeleteConfirm, onProjectInviteSend, deletingInvite, changeRole,
onDeleteInvite, isShowTopcoderDialog, onShowTopcoderDialog, processingInvites, processingMembers,
onTopcoderInviteSend, onTopcoderInviteDeleteConfirm, topcoderTeamInvites, onAcceptOrRefuse, error,
- onSelectedMembersUpdate, selectedMembers, allMembers, updatingMemberIds
+ onSelectedMembersUpdate, selectedMembers, allMembers, updatingMemberIds, onShowCopilotDialog, copilotTeamInvites,
+ isShowCopilotDialog, onCopilotInviteSend,
} = this.props
-
const {
projectTeamInviteButtonExpanded,
topcoderTeamInviteButtonExpanded,
+ copilotTeamInviteButtonExpanded,
} = this.state
const currentMember = members.filter((member) => member.userId === currentUser.userId)[0]
const modalActive = isAddingTeamMember || deletingMember || isShowJoin || showNewMemberConfirmation || deletingInvite
const customerTeamManageAction = (currentUser.isAdmin || currentUser.isManager) && !currentMember
const topcoderTeamManageAction = hasPermission(PERMISSIONS.MANAGE_TOPCODER_TEAM)
+ const copilotTeamManageAction = hasPermission(PERMISSIONS.MANAGE_COPILOTS)
+ const canRequestCopilot = hasPermission(PERMISSIONS.REQUEST_COPILOTS)
const canJoinAsCopilot = !currentMember && currentUser.isCopilot
const canJoinAsManager = !currentMember && (currentUser.isManager || currentUser.isAccountManager)
const canShowInvite = currentMember && (currentMember.isCustomer || currentMember.isCopilot || currentMember.isManager)
@@ -104,6 +115,7 @@ class TeamManagement extends React.Component {
const sortedMembers = members
let projectTeamInviteCount = 0
let topcoderTeamInviteCount = 0
+ let copilotTeamInviteCount = 0
return (
@@ -173,6 +185,67 @@ class TeamManagement extends React.Component {
+
+
+
+ Copilot
+ {copilotTeamManageAction &&
+ onShowCopilotDialog(true)}>
+ Manage
+
+ }
+
+
+ {sortedMembers.map((member, i) => {
+ if (!member.isCopilot) {
+ return
+ }
+
+ copilotTeamInviteCount++
+ if (!copilotTeamInviteButtonExpanded && copilotTeamInviteCount > 3) {
+ return null
+ }
+
+ return (
+
+ )
+ })}
+ {copilotTeamInvites.map((invite, i) => {
+ copilotTeamInviteCount++
+ if(!copilotTeamInviteButtonExpanded && copilotTeamInviteCount > 3) {
+ return null
+ }
+
+ return (
+
+ )
+ })}
+ {copilotTeamInviteCount > 3 &&
+
+
+ {!copilotTeamInviteButtonExpanded ? 'Show All': 'Show Less'}
+
+
+ }
+ {canRequestCopilot &&
+
+ }
+
+
+
@@ -183,7 +256,7 @@ class TeamManagement extends React.Component {
{sortedMembers.map((member, i) => {
- if (member.isCustomer) {
+ if (member.isCustomer || member.isCopilot) {
return
}
@@ -197,9 +270,6 @@ class TeamManagement extends React.Component {
)
})}
{topcoderTeamInvites.map((invite, i) => {
- if (invite.isCustomer) {
- return
- }
topcoderTeamInviteCount++
if(!topcoderTeamInviteButtonExpanded &&topcoderTeamInviteCount > 3) {
return null
@@ -284,6 +354,35 @@ class TeamManagement extends React.Component {
/>
)
})())}
+ {(!modalActive && isShowCopilotDialog) && ((() => {
+ const onClickCancel = () => onShowCopilotDialog(false)
+ const removeMember = (member) => {
+ onMemberDelete(member)
+ }
+ const removeInvite = (item) => {
+ onDeleteInvite({item, type: 'copilot'})
+ }
+ return (
+
+ )
+ })())}
{(!modalActive && (isShowTopcoderDialog || this.props.history.location.hash === '#manageTopcoderTeam')) && ((() => {
const onClickCancel = () => {
this.props.history.push(this.props.history.location.pathname)
@@ -538,6 +637,7 @@ export default uncontrollable(TeamManagement, {
deletingMember: 'onMemberDelete',
isShowJoin: 'onJoin',
isShowProjectDialog: 'onShowProjectDialog',
+ isShowCopilotDialog: 'onShowCopilotDialog',
isShowTopcoderDialog: 'onShowTopcoderDialog',
deletingInvite: 'onDeleteInvite',
isInvited: 'onInviteAcceptShow'
diff --git a/src/components/TeamManagement/TeamManagement.scss b/src/components/TeamManagement/TeamManagement.scss
index e1f48debb..ec80e7b4f 100644
--- a/src/components/TeamManagement/TeamManagement.scss
+++ b/src/components/TeamManagement/TeamManagement.scss
@@ -504,7 +504,8 @@
}
}
- .join-btn {
+ .join-btn,
+ a.join-btn {
@include roboto;
cursor: pointer;
padding: $base-unit*2 $base-unit*3;
diff --git a/src/components/TeamManagement/TopcoderManagementDialog.js b/src/components/TeamManagement/TopcoderManagementDialog.js
index 03ae312a8..2c06521e8 100644
--- a/src/components/TeamManagement/TopcoderManagementDialog.js
+++ b/src/components/TeamManagement/TopcoderManagementDialog.js
@@ -42,10 +42,6 @@ class TopcoderManagementDialog extends React.Component {
}, {
title: 'Observer',
value: 'observer',
- }, {
- title: 'Copilot',
- value: 'copilot',
- canAddDirectly: true,
}, {
title: 'Account Manager',
value: 'account_manager',
@@ -82,12 +78,13 @@ class TopcoderManagementDialog extends React.Component {
}
onChange(selectedMembers) {
- const { projectTeamInvites, members, topcoderTeamInvites } = this.props
+ const { projectTeamInvites, members, topcoderTeamInvites, copilotTeamInvites } = this.props
const present = _.some(selectedMembers, (selectedMember) => (
this.isSelectedMemberAlreadyInvited(members, selectedMember)
|| this.isSelectedMemberAlreadyInvited(topcoderTeamInvites, selectedMember)
|| this.isSelectedMemberAlreadyInvited(projectTeamInvites, selectedMember)
+ || this.isSelectedMemberAlreadyInvited(copilotTeamInvites, selectedMember)
))
this.setState({
@@ -170,7 +167,7 @@ class TopcoderManagementDialog extends React.Component {
{(members.map((member) => {
- if (member.isCustomer) {
+ if (member.isCustomer || member.isCopilot) {
return null
}
i++
@@ -216,9 +213,9 @@ class TopcoderManagementDialog extends React.Component {
)
}
- let types = ['Copilot', 'Manager', 'Account Manager', 'Account Executive', 'Program Manager', 'Solution Architect', 'Project Manager']
+ let types = ['Manager', 'Account Manager', 'Account Executive', 'Program Manager', 'Solution Architect', 'Project Manager']
const currentType = role
- types = currentType === 'Observer'? ['Observer', ...types] : [...types]
+ types = currentType === 'Observer'? ['Observer', ...types] : [...types]
const onClick = (type) => {
this.onUserRoleChange(member.userId, member.id, type)
}
@@ -379,9 +376,7 @@ class TopcoderManagementDialog extends React.Component {
disabled={processingInvites || this.state.showAlreadyMemberError || selectedMembers.length === 0}
onClick={this.addUsers}
>
- {_.find(this.roles, {value:this.state.userRole}).canAddDirectly && !showApproveDecline
- ?'Request invite'
- :'Invite users'}
+ Invite users
}
diff --git a/src/config/permissions.js b/src/config/permissions.js
index cf5c0ff54..1840289a1 100644
--- a/src/config/permissions.js
+++ b/src/config/permissions.js
@@ -196,6 +196,33 @@ export default {
]
},
+ MANAGE_COPILOTS: {
+ _meta: {
+ group: 'Project Members',
+ title: 'Manage copilots',
+ description: 'Directly invite copilots to the project.',
+ },
+ topcoderRoles: [
+ ...TOPCODER_ADMINS,
+ ROLE_CONNECT_COPILOT_MANAGER
+ ]
+ },
+
+ REQUEST_COPILOTS: {
+ _meta: {
+ group: 'Project Members',
+ title: 'Request copilots',
+ description: 'Request copilots to the project.',
+ },
+ projectRoles: [
+ ..._.difference(PROJECT_ALL, [PROJECT_ROLE_COPILOT, PROJECT_ROLE_CUSTOMER])
+ ],
+ topcoderRoles: [
+ ...TOPCODER_ADMINS,
+ ROLE_CONNECT_COPILOT_MANAGER
+ ]
+ },
+
ACCESS_PRIVATE_POST: {
_meta: {
group: 'Topics & Posts',
diff --git a/src/projects/detail/containers/TeamManagementContainer.jsx b/src/projects/detail/containers/TeamManagementContainer.jsx
index 88b2805bb..7a3e20035 100644
--- a/src/projects/detail/containers/TeamManagementContainer.jsx
+++ b/src/projects/detail/containers/TeamManagementContainer.jsx
@@ -47,6 +47,7 @@ class TeamManagementContainer extends Component {
}
this.onProjectInviteSend = this.onProjectInviteSend.bind(this)
+ this.onCopilotInviteSend = this.onCopilotInviteSend.bind(this)
this.onProjectInviteDelete = this.onProjectInviteDelete.bind(this)
this.onTopcoderInviteDelete = this.onTopcoderInviteDelete.bind(this)
this.onTopcoderInviteSend = this.onTopcoderInviteSend.bind(this)
@@ -116,6 +117,11 @@ class TeamManagementContainer extends Component {
this.props.inviteProjectMembers(this.props.projectId, emails, handles)
}
+ onCopilotInviteSend() {
+ const {handles, emails} = this.getEmailsAndHandles()
+ this.props.inviteTopcoderMembers(this.props.projectId, { role: 'copilot', handles, emails })
+ }
+
onAcceptOrRefuse(invite) {
return this.props.acceptOrRefuseInvite(this.props.projectId, invite)
}
@@ -182,7 +188,7 @@ class TeamManagementContainer extends Component {
render() {
const projectMembers = this.anontateMemberProps()
- const {projectTeamInvites, topcoderTeamInvites } = this.props
+ const {projectTeamInvites, topcoderTeamInvites, copilotTeamInvites } = this.props
return (
{
processingMembers: projectState.processingMembers,
updatingMemberIds: projectState.updatingMemberIds,
error: projectState.error,
- topcoderTeamInvites: _.filter(projectState.project.invites, i => i.role !== 'customer'),
+ topcoderTeamInvites: _.filter(projectState.project.invites, i => i.role !== 'customer' && i.role !== 'copilot'),
+ copilotTeamInvites: _.filter(projectState.project.invites, i => i.role === 'copilot'),
projectTeamInvites: _.filter(projectState.project.invites, i => i.role === 'customer')
}
}