Skip to content

Commit 06ab559

Browse files
committed
PM-973 - invite by email
1 parent 81a1173 commit 06ab559

File tree

5 files changed

+154
-91
lines changed

5 files changed

+154
-91
lines changed

src/components/UserCard/index.js

Lines changed: 87 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import _ from 'lodash'
2+
import moment from 'moment'
13
import React, { Component } from 'react'
24
import PropTypes from 'prop-types'
35
import cn from 'classnames'
@@ -6,7 +8,6 @@ import { PROJECT_ROLES } from '../../config/constants'
68
import PrimaryButton from '../Buttons/PrimaryButton'
79
import AlertModal from '../Modal/AlertModal'
810
import { updateProjectMemberRole } from '../../services/projects'
9-
import _ from 'lodash'
1011

1112
const theme = {
1213
container: styles.modalContainer
@@ -58,7 +59,7 @@ class UserCard extends Component {
5859
}
5960

6061
render () {
61-
const { user, onRemoveClick, isEditable } = this.props
62+
const { isInvite, user, onRemoveClick, isEditable } = this.props
6263
const showRadioButtons = _.includes(_.values(PROJECT_ROLES), user.role)
6364
return (
6465
<div>
@@ -90,76 +91,90 @@ class UserCard extends Component {
9091
)}
9192
<div className={styles.item}>
9293
<div className={cn(styles.col5)}>
93-
{user.handle}
94-
</div>
95-
<div className={cn(styles.col5)}>
96-
{showRadioButtons && (<div className={styles.tcRadioButton}>
97-
<input
98-
name={`user-${user.id}`}
99-
type='radio'
100-
id={`read-${user.id}`}
101-
checked={user.role === PROJECT_ROLES.READ}
102-
onChange={(e) => e.target.checked && this.updatePermission(PROJECT_ROLES.READ)}
103-
/>
104-
<label className={cn({ [styles.isDisabled]: !isEditable })} htmlFor={`read-${user.id}`}>
105-
<div>
106-
Read
107-
</div>
108-
<input type='hidden' />
109-
</label>
110-
</div>)}
111-
</div>
112-
<div className={cn(styles.col5)}>
113-
{showRadioButtons && (<div className={styles.tcRadioButton}>
114-
<input
115-
name={`user-${user.id}`}
116-
type='radio'
117-
id={`write-${user.id}`}
118-
checked={user.role === PROJECT_ROLES.WRITE}
119-
onChange={(e) => e.target.checked && this.updatePermission(PROJECT_ROLES.WRITE)}
120-
/>
121-
<label className={cn({ [styles.isDisabled]: !isEditable })} htmlFor={`write-${user.id}`}>
122-
<div>
123-
Write
124-
</div>
125-
<input type='hidden' />
126-
</label>
127-
</div>)}
128-
</div>
129-
<div className={cn(styles.col5)}>
130-
{showRadioButtons && (<div className={styles.tcRadioButton}>
131-
<input
132-
name={`user-${user.id}`}
133-
type='radio'
134-
id={`full-access-${user.id}`}
135-
checked={user.role === PROJECT_ROLES.MANAGER}
136-
onChange={(e) => e.target.checked && this.updatePermission(PROJECT_ROLES.MANAGER)}
137-
/>
138-
<label className={cn({ [styles.isDisabled]: !isEditable })} htmlFor={`full-access-${user.id}`}>
139-
<div>
140-
Full Access
141-
</div>
142-
<input type='hidden' />
143-
</label>
144-
</div>)}
145-
</div>
146-
<div className={cn(styles.col5)}>
147-
{showRadioButtons && (<div className={styles.tcRadioButton}>
148-
<input
149-
name={`user-${user.id}`}
150-
type='radio'
151-
id={`copilot-${user.id}`}
152-
checked={user.role === PROJECT_ROLES.COPILOT}
153-
onChange={(e) => e.target.checked && this.updatePermission(PROJECT_ROLES.COPILOT)}
154-
/>
155-
<label className={cn({ [styles.isDisabled]: !isEditable })} htmlFor={`copilot-${user.id}`}>
156-
<div>
157-
Copilot
158-
</div>
159-
<input type='hidden' />
160-
</label>
161-
</div>)}
94+
{isInvite ? user.email : user.handle}
16295
</div>
96+
{!isInvite && (
97+
<>
98+
<div className={cn(styles.col5)}>
99+
{showRadioButtons && (<div className={styles.tcRadioButton}>
100+
<input
101+
name={`user-${user.id}`}
102+
type='radio'
103+
id={`read-${user.id}`}
104+
checked={user.role === PROJECT_ROLES.READ}
105+
onChange={(e) => e.target.checked && this.updatePermission(PROJECT_ROLES.READ)}
106+
/>
107+
<label className={cn({ [styles.isDisabled]: !isEditable })} htmlFor={`read-${user.id}`}>
108+
<div>
109+
Read
110+
</div>
111+
<input type='hidden' />
112+
</label>
113+
</div>)}
114+
</div>
115+
<div className={cn(styles.col5)}>
116+
{showRadioButtons && (<div className={styles.tcRadioButton}>
117+
<input
118+
name={`user-${user.id}`}
119+
type='radio'
120+
id={`write-${user.id}`}
121+
checked={user.role === PROJECT_ROLES.WRITE}
122+
onChange={(e) => e.target.checked && this.updatePermission(PROJECT_ROLES.WRITE)}
123+
/>
124+
<label className={cn({ [styles.isDisabled]: !isEditable })} htmlFor={`write-${user.id}`}>
125+
<div>
126+
Write
127+
</div>
128+
<input type='hidden' />
129+
</label>
130+
</div>)}
131+
</div>
132+
<div className={cn(styles.col5)}>
133+
{showRadioButtons && (<div className={styles.tcRadioButton}>
134+
<input
135+
name={`user-${user.id}`}
136+
type='radio'
137+
id={`full-access-${user.id}`}
138+
checked={user.role === PROJECT_ROLES.MANAGER}
139+
onChange={(e) => e.target.checked && this.updatePermission(PROJECT_ROLES.MANAGER)}
140+
/>
141+
<label className={cn({ [styles.isDisabled]: !isEditable })} htmlFor={`full-access-${user.id}`}>
142+
<div>
143+
Full Access
144+
</div>
145+
<input type='hidden' />
146+
</label>
147+
</div>)}
148+
</div>
149+
<div className={cn(styles.col5)}>
150+
{showRadioButtons && (<div className={styles.tcRadioButton}>
151+
<input
152+
name={`user-${user.id}`}
153+
type='radio'
154+
id={`copilot-${user.id}`}
155+
checked={user.role === PROJECT_ROLES.COPILOT}
156+
onChange={(e) => e.target.checked && this.updatePermission(PROJECT_ROLES.COPILOT)}
157+
/>
158+
<label className={cn({ [styles.isDisabled]: !isEditable })} htmlFor={`copilot-${user.id}`}>
159+
<div>
160+
Copilot
161+
</div>
162+
<input type='hidden' />
163+
</label>
164+
</div>)}
165+
</div>
166+
</>
167+
)}
168+
{isInvite && (
169+
<>
170+
<div className={cn(styles.col5)} />
171+
<div className={cn(styles.col5)}>
172+
Invited {moment(user.createdAt).format('MMM D, YY')}
173+
</div>
174+
<div className={cn(styles.col5)} />
175+
<div className={cn(styles.col5)} />
176+
</>
177+
)}
163178
{isEditable ? (<div className={cn(styles.col5)}>
164179
<PrimaryButton
165180
text={'Remove'}
@@ -173,6 +188,7 @@ class UserCard extends Component {
173188
}
174189

175190
UserCard.propTypes = {
191+
isInvite: PropTypes.bool,
176192
user: PropTypes.object,
177193
updateProjectNember: PropTypes.func.isRequired,
178194
onRemoveClick: PropTypes.func.isRequired,

src/components/Users/index.js

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import PrimaryButton from '../Buttons/PrimaryButton'
99
import { PROJECT_ROLES, AUTOCOMPLETE_DEBOUNCE_TIME_MS } from '../../config/constants'
1010
import { checkAdmin } from '../../util/tc'
1111
import { removeUserFromProject } from '../../services/projects'
12+
import { deleteProjectMemberInvite } from '../../services/projectMemberInvites'
1213
import ConfirmationModal from '../Modal/ConfirmationModal'
1314
import UserAddModalContent from './user-add.modal'
1415
import InviteUserModalContent from './invite-user.modal' // Import the new component
@@ -108,11 +109,14 @@ class Users extends Component {
108109
async onRemoveConfirmClick () {
109110
if (this.state.isRemoving) { return }
110111

111-
const { removeProjectNember } = this.props
112+
const { removeProjectNember, invitedMembers } = this.props
112113
const userToRemove = this.state.userToRemove
114+
const isInvite = !!_.find(invitedMembers, { email: userToRemove.email })
113115
try {
114116
this.setState({ isRemoving: true })
115-
await removeUserFromProject(userToRemove.projectId, userToRemove.id)
117+
await (
118+
isInvite ? deleteProjectMemberInvite(userToRemove.projectId, userToRemove.id) : removeUserFromProject(userToRemove.projectId, userToRemove.id)
119+
)
116120
removeProjectNember(userToRemove)
117121

118122
this.resetRemoveUserState()
@@ -151,6 +155,7 @@ class Users extends Component {
151155
const {
152156
projects,
153157
projectMembers,
158+
invitedMembers,
154159
updateProjectNember,
155160
isEditable,
156161
isSearchingUserProjects,
@@ -166,7 +171,7 @@ class Users extends Component {
166171
}
167172
})
168173
const loggedInHandle = this.getHandle()
169-
const membersExist = projectMembers && projectMembers.length > 0
174+
const membersExist = (projectMembers && projectMembers.length > 0) || (invitedMembers && invitedMembers.length > 0)
170175
const isCopilotOrManager = this.checkIsCopilotOrManager(projectMembers, loggedInHandle)
171176
const isAdmin = checkAdmin(this.props.auth.token)
172177
const showAddUser = isEditable && this.state.projectOption && (isCopilotOrManager || isAdmin)
@@ -221,6 +226,8 @@ class Users extends Component {
221226
this.state.showInviteUserModal && (
222227
<InviteUserModalContent
223228
projectId={this.state.projectOption.value}
229+
projectMembers={projectMembers}
230+
invitedMembers={invitedMembers}
224231
onClose={this.resetInviteUserState}
225232
/>
226233
)
@@ -229,7 +236,7 @@ class Users extends Component {
229236
this.state.showRemoveConfirmationModal && (
230237
<ConfirmationModal
231238
title='Confirm Removal'
232-
message={`Are you sure you want to remove ${this.state.userToRemove.handle} from this project?`}
239+
message={`Are you sure you want to remove ${this.state.userToRemove.handle || this.state.userToRemove.email} from this project?`}
233240
theme={theme}
234241
isProcessing={this.state.isRemoving}
235242
errorMessage={this.state.removeError}
@@ -273,6 +280,22 @@ class Users extends Component {
273280
})
274281
}
275282
</ul>
283+
<ul className={styles.userList}>
284+
{
285+
_.map(invitedMembers, (member) => {
286+
return (
287+
<li className={styles.userItem} key={`user-card-${member.id}`}>
288+
<UserCard
289+
isInvite
290+
user={member}
291+
onRemoveClick={this.onRemoveClick}
292+
updateProjectNember={updateProjectNember}
293+
isEditable={isEditable} />
294+
</li>
295+
)
296+
})
297+
}
298+
</ul>
276299
</>
277300
)
278301
}
@@ -292,6 +315,7 @@ Users.propTypes = {
292315
isSearchingUserProjects: PropTypes.bool,
293316
projects: PropTypes.arrayOf(PropTypes.object),
294317
projectMembers: PropTypes.arrayOf(PropTypes.object),
318+
invitedMembers: PropTypes.arrayOf(PropTypes.object),
295319
searchUserProjects: PropTypes.func.isRequired,
296320
resultSearchUserProjects: PropTypes.arrayOf(PropTypes.object)
297321
}

src/components/Users/invite-user.modal.js

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
/* eslint-disable no-unused-vars */
21
import React, { useState } from 'react'
32
import PropTypes from 'prop-types'
43
import cn from 'classnames'
5-
import { get } from 'lodash'
4+
import { find, get } from 'lodash'
65
import Modal from '../Modal'
76
import PrimaryButton from '../Buttons/PrimaryButton'
87
import { inviteUserToProject } from '../../services/projects'
@@ -19,23 +18,35 @@ const validateEmail = (email) => {
1918
return emailRegex.test(email)
2019
}
2120

22-
const InviteUserModalContent = ({ projectId, onClose }) => {
21+
const InviteUserModalContent = ({ projectId, onClose, projectMembers, invitedMembers }) => {
2322
const [emailToInvite, setEmailToInvite] = useState('')
2423
const [showEmailError, setShowEmailError] = useState(false)
2524
const [inviteUserError, setInviteUserError] = useState(null)
2625
const [isInviting, setIsInviting] = useState(false)
2726

28-
const handleEmailBlur = () => {
27+
const checkEmail = () => {
2928
if (!validateEmail(emailToInvite)) {
3029
setShowEmailError(true)
30+
return false
3131
}
32+
33+
if (find(invitedMembers, { email: emailToInvite })) {
34+
setInviteUserError('Email is already invited!')
35+
return false
36+
}
37+
38+
if (find(projectMembers, { email: emailToInvite })) {
39+
setInviteUserError('Member already part of the project!')
40+
return false
41+
}
42+
43+
return true
3244
}
3345

3446
const onInviteUserConfirmClick = async () => {
3547
if (isInviting) return
3648

37-
if (!emailToInvite || !validateEmail(emailToInvite)) {
38-
setShowEmailError(true)
49+
if (!checkEmail()) {
3950
return
4051
}
4152

@@ -70,8 +81,9 @@ const InviteUserModalContent = ({ projectId, onClose }) => {
7081
onChange={(e) => {
7182
setEmailToInvite(e.target.value)
7283
setShowEmailError(false)
84+
setInviteUserError(null)
7385
}}
74-
onBlur={handleEmailBlur}
86+
onBlur={checkEmail}
7587
/>
7688
</div>
7789
</div>
@@ -81,7 +93,9 @@ const InviteUserModalContent = ({ projectId, onClose }) => {
8193
</div>
8294
)}
8395
{inviteUserError && (
84-
<div className={styles.errorMesssage}>{inviteUserError}</div>
96+
<div className={styles.row}>
97+
<div className={styles.errorMesssage}>{inviteUserError}</div>
98+
</div>
8599
)}
86100
</div>
87101
<div className={styles.buttonGroup}>
@@ -107,7 +121,9 @@ const InviteUserModalContent = ({ projectId, onClose }) => {
107121

108122
InviteUserModalContent.propTypes = {
109123
projectId: PropTypes.number.isRequired,
110-
onClose: PropTypes.func.isRequired
124+
onClose: PropTypes.func.isRequired,
125+
projectMembers: PropTypes.arrayOf(PropTypes.object),
126+
invitedMembers: PropTypes.arrayOf(PropTypes.object)
111127
}
112128

113129
export default InviteUserModalContent

0 commit comments

Comments
 (0)