Skip to content

Commit 592b787

Browse files
committed
PM-973 - add invitation dialog after user accepts invitation through email
1 parent 06ab559 commit 592b787

File tree

10 files changed

+310
-6
lines changed

10 files changed

+310
-6
lines changed

src/components/Users/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ class Users extends Component {
228228
projectId={this.state.projectOption.value}
229229
projectMembers={projectMembers}
230230
invitedMembers={invitedMembers}
231+
onMemberInvited={this.props.addNewProjectInvite}
231232
onClose={this.resetInviteUserState}
232233
/>
233234
)
@@ -309,6 +310,7 @@ Users.propTypes = {
309310
loadProject: PropTypes.func.isRequired,
310311
updateProjectNember: PropTypes.func.isRequired,
311312
removeProjectNember: PropTypes.func.isRequired,
313+
addNewProjectInvite: PropTypes.func.isRequired,
312314
addNewProjectMember: PropTypes.func.isRequired,
313315
auth: PropTypes.object,
314316
isEditable: PropTypes.bool,

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

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const validateEmail = (email) => {
1818
return emailRegex.test(email)
1919
}
2020

21-
const InviteUserModalContent = ({ projectId, onClose, projectMembers, invitedMembers }) => {
21+
const InviteUserModalContent = ({ projectId, onClose, onMemberInvited, projectMembers, invitedMembers }) => {
2222
const [emailToInvite, setEmailToInvite] = useState('')
2323
const [showEmailError, setShowEmailError] = useState(false)
2424
const [inviteUserError, setInviteUserError] = useState(null)
@@ -55,8 +55,16 @@ const InviteUserModalContent = ({ projectId, onClose, projectMembers, invitedMem
5555

5656
try {
5757
// api restriction: ONLY "customer" role can be invited via email
58-
await inviteUserToProject(projectId, emailToInvite, PROJECT_ROLES.CUSTOMER)
59-
onClose()
58+
const { success: invitations = [], failed } = await inviteUserToProject(projectId, emailToInvite, PROJECT_ROLES.CUSTOMER)
59+
60+
if (failed) {
61+
const error = get(failed, '0.message', 'Unable to invite user')
62+
setInviteUserError(error)
63+
setIsInviting(false)
64+
} else {
65+
onMemberInvited(invitations[0] || {})
66+
onClose()
67+
}
6068
} catch (e) {
6169
const error = get(e, 'response.data.message', 'Unable to invite user')
6270
setInviteUserError(error)
@@ -122,6 +130,7 @@ const InviteUserModalContent = ({ projectId, onClose, projectMembers, invitedMem
122130
InviteUserModalContent.propTypes = {
123131
projectId: PropTypes.number.isRequired,
124132
onClose: PropTypes.func.isRequired,
133+
onMemberInvited: PropTypes.func.isRequired,
125134
projectMembers: PropTypes.arrayOf(PropTypes.object),
126135
invitedMembers: PropTypes.arrayOf(PropTypes.object)
127136
}

src/config/constants.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,14 @@ export const UPDATE_PROJECT_PENDING = 'UPDATE_PROJECT_PENDING'
195195
export const UPDATE_PROJECT_SUCCESS = 'UPDATE_PROJECT_SUCCESS'
196196
export const UPDATE_PROJECT_FAILURE = 'UPDATE_PROJECT_FAILURE'
197197

198+
export const PROJECT_MEMBER_INVITE_STATUS_ACCEPTED = 'accepted'
199+
export const PROJECT_MEMBER_INVITE_STATUS_REFUSED = 'refused'
200+
export const PROJECT_MEMBER_INVITE_STATUS_CANCELED = 'canceled'
201+
export const PROJECT_MEMBER_INVITE_STATUS_PENDING = 'pending'
202+
export const PROJECT_MEMBER_INVITE_STATUS_REQUESTED = 'requested'
203+
export const PROJECT_MEMBER_INVITE_STATUS_REQUEST_APPROVED = 'request_approved'
204+
export const PROJECT_MEMBER_INVITE_STATUS_REQUEST_REJECTED = 'request_rejected'
205+
198206
// Name of challenge tracks
199207
export const CHALLENGE_TRACKS = {
200208
DESIGN: DES_TRACK_ID,

src/containers/Challenges/index.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ import {
2020
setActiveProject,
2121
resetSidebarActiveParams
2222
} from '../../actions/sidebar'
23-
import { checkAdmin } from '../../util/tc'
23+
import { checkAdmin, checkIsUserInvited } from '../../util/tc'
24+
import { withRouter } from 'react-router-dom'
2425

2526
class Challenges extends Component {
2627
constructor (props) {
@@ -55,6 +56,14 @@ class Challenges extends Component {
5556
}
5657
}
5758

59+
componentDidUpdate () {
60+
const { auth } = this.props
61+
62+
if (checkIsUserInvited(auth.token, this.props.projectDetail)) {
63+
this.props.history.push(`/projects/${this.props.projectDetail.id}/invitation`)
64+
}
65+
}
66+
5867
componentWillReceiveProps (nextProps) {
5968
if (
6069
(nextProps.dashboard && this.props.dashboard !== nextProps.dashboard) ||
@@ -194,6 +203,7 @@ Challenges.defaultProps = {
194203
}
195204

196205
Challenges.propTypes = {
206+
history: PropTypes.object,
197207
projects: PropTypes.arrayOf(PropTypes.shape()),
198208
menu: PropTypes.string,
199209
challenges: PropTypes.arrayOf(PropTypes.object),
@@ -268,4 +278,6 @@ const mapDispatchToProps = {
268278
deleteChallenge
269279
}
270280

271-
export default connect(mapStateToProps, mapDispatchToProps)(Challenges)
281+
export default withRouter(
282+
connect(mapStateToProps, mapDispatchToProps)(Challenges)
283+
)

src/containers/ProjectEditor/index.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
updateProject
1616
} from '../../actions/projects'
1717
import { setActiveProject } from '../../actions/sidebar'
18-
import { checkAdminOrCopilot, checkAdmin } from '../../util/tc'
18+
import { checkAdminOrCopilot, checkAdmin, checkIsUserInvited } from '../../util/tc'
1919
import { PROJECT_ROLES } from '../../config/constants'
2020
import Loader from '../../components/Loader'
2121

@@ -37,6 +37,11 @@ class ProjectEditor extends Component {
3737

3838
componentDidUpdate () {
3939
const { auth } = this.props
40+
41+
if (checkIsUserInvited(auth.token, this.props.projectDetail)) {
42+
this.props.history.push(`/projects/${this.props.projectDetail.id}/invitation`)
43+
}
44+
4045
if (!checkAdminOrCopilot(auth.token, this.props.projectDetail)) {
4146
this.props.history.push('/projects')
4247
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
@import '../../styles/includes';
2+
3+
.modalContainer {
4+
padding: 0;
5+
position: fixed;
6+
overflow: auto;
7+
z-index: 10000;
8+
top: 0;
9+
right: 0;
10+
bottom: 0;
11+
left: 0;
12+
box-sizing: border-box;
13+
width: auto;
14+
max-width: none;
15+
transform: none;
16+
background: transparent;
17+
color: $text-color;
18+
opacity: 1;
19+
display: flex;
20+
justify-content: center;
21+
align-items: center;
22+
23+
:global {
24+
button.close {
25+
margin-right: 5px;
26+
margin-top: 5px;
27+
}
28+
}
29+
30+
.contentContainer {
31+
box-sizing: border-box;
32+
background: $white;
33+
opacity: 1;
34+
position: relative;
35+
display: flex;
36+
flex-direction: column;
37+
justify-content: flex-start;
38+
align-items: center;
39+
border-radius: 6px;
40+
margin: 0 auto;
41+
width: 852px;
42+
padding: 30px;
43+
44+
.content {
45+
padding: 30px;
46+
width: 100%;
47+
height: 100%;
48+
}
49+
50+
.title {
51+
@include roboto-bold();
52+
53+
font-size: 30px;
54+
line-height: 36px;
55+
margin-bottom: 30px;
56+
margin-top: 0;
57+
}
58+
59+
span {
60+
@include roboto;
61+
62+
font-size: 22px;
63+
font-weight: 400;
64+
line-height: 26px;
65+
}
66+
67+
&.confirm {
68+
width: 999px;
69+
70+
.buttonGroup {
71+
display: flex;
72+
justify-content: space-between;
73+
margin-top: 30px;
74+
75+
.buttonSizeA {
76+
width: 193px;
77+
height: 40px;
78+
margin-right: 33px;
79+
80+
span {
81+
font-size: 18px;
82+
font-weight: 500;
83+
}
84+
}
85+
86+
.buttonSizeB {
87+
width: 160px;
88+
height: 40px;
89+
90+
span {
91+
font-size: 18px;
92+
font-weight: 500;
93+
line-height: 22px;
94+
}
95+
}
96+
}
97+
}
98+
99+
.buttonGroup {
100+
display: flex;
101+
justify-content: space-between;
102+
margin-top: 30px;
103+
104+
.button {
105+
width: 135px;
106+
height: 40px;
107+
margin-right: 66px;
108+
109+
span {
110+
font-size: 18px;
111+
font-weight: 500;
112+
}
113+
}
114+
115+
.button:last-child {
116+
margin-right: 0;
117+
}
118+
}
119+
}
120+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import PropTypes from 'prop-types'
2+
import React, { useCallback, useEffect, useMemo, useState } from 'react'
3+
import { connect } from 'react-redux'
4+
import { withRouter } from 'react-router-dom'
5+
import { toastr } from 'react-redux-toastr'
6+
import { checkIsUserInvited } from '../../util/tc'
7+
import { isEmpty } from 'lodash'
8+
import { loadProject } from '../../actions/projects'
9+
import ConfirmationModal from '../../components/Modal/ConfirmationModal'
10+
11+
import styles from './ProjectInvitations.module.scss'
12+
import { updateProjectMemberInvite } from '../../services/projectMemberInvites'
13+
import { PROJECT_MEMBER_INVITE_STATUS_ACCEPTED, PROJECT_MEMBER_INVITE_STATUS_REFUSED } from '../../config/constants'
14+
15+
const theme = {
16+
container: styles.modalContainer
17+
}
18+
19+
const ProjectInvitations = ({ match, auth, isProjectLoading, history, projectDetail, loadProject }) => {
20+
const automaticAction = useMemo(() => [PROJECT_MEMBER_INVITE_STATUS_ACCEPTED, PROJECT_MEMBER_INVITE_STATUS_REFUSED].includes(match.params.action) ? match.params.action : undefined, [match.params])
21+
const projectId = useMemo(() => parseInt(match.params.projectId), [match.params])
22+
const invitation = useMemo(() => checkIsUserInvited(auth.token, projectDetail), [auth.token, projectDetail])
23+
const [isUpdating, setIsUpdating] = useState(automaticAction || false)
24+
const isAccepting = isUpdating === PROJECT_MEMBER_INVITE_STATUS_ACCEPTED
25+
const isDeclining = isUpdating === PROJECT_MEMBER_INVITE_STATUS_REFUSED
26+
27+
useEffect(() => {
28+
if (!projectId) {
29+
return
30+
}
31+
32+
if (isProjectLoading || isEmpty(projectDetail)) {
33+
if (!isProjectLoading) {
34+
loadProject(projectId)
35+
}
36+
return
37+
}
38+
39+
if (!invitation) {
40+
history.push(`/projects`)
41+
}
42+
}, [projectId, auth, projectDetail, isProjectLoading, history])
43+
44+
const updateInvite = useCallback(async (status) => {
45+
setIsUpdating(status)
46+
await updateProjectMemberInvite(projectId, invitation.id, status)
47+
toastr.success('Success', `Successfully ${status} the invitation.`)
48+
history.push(status === PROJECT_MEMBER_INVITE_STATUS_ACCEPTED ? `/projects/${projectId}/challenges` : '/projects')
49+
}, [invitation])
50+
51+
const acceptInvite = useCallback(() => updateInvite(PROJECT_MEMBER_INVITE_STATUS_ACCEPTED), [updateInvite])
52+
const declineInvite = useCallback(() => updateInvite(PROJECT_MEMBER_INVITE_STATUS_REFUSED), [updateInvite])
53+
54+
useEffect(() => {
55+
if (!invitation || !automaticAction) {
56+
return
57+
}
58+
59+
setTimeout(() => {
60+
if (automaticAction === PROJECT_MEMBER_INVITE_STATUS_ACCEPTED) {
61+
acceptInvite()
62+
} else if (automaticAction === PROJECT_MEMBER_INVITE_STATUS_REFUSED) {
63+
declineInvite()
64+
}
65+
}, [1500])
66+
}, [invitation, automaticAction])
67+
68+
return (
69+
<>
70+
{invitation && (
71+
<ConfirmationModal
72+
title={
73+
isUpdating ? (
74+
isAccepting ? 'Adding you to the project....' : 'Declining invitation...'
75+
) : "You're invited to join this project"
76+
}
77+
message={isDeclining ? '' : `
78+
Once you join the team you will be able to see the project details, collaborate on project specification and monitor the progress of all deliverables
79+
`}
80+
theme={theme}
81+
cancelText='Cancel'
82+
confirmText='Join project'
83+
onCancel={declineInvite}
84+
onConfirm={acceptInvite}
85+
isProcessing={isUpdating}
86+
/>
87+
)}
88+
</>
89+
)
90+
}
91+
92+
ProjectInvitations.propTypes = {
93+
match: PropTypes.shape({
94+
params: PropTypes.shape({
95+
projectId: PropTypes.string
96+
})
97+
}).isRequired,
98+
auth: PropTypes.object.isRequired,
99+
isProjectLoading: PropTypes.bool,
100+
history: PropTypes.object,
101+
loadProject: PropTypes.func.isRequired,
102+
projectDetail: PropTypes.object
103+
}
104+
105+
const mapStateToProps = ({ projects, auth }) => {
106+
return {
107+
projectDetail: projects.projectDetail,
108+
isProjectLoading: projects.isLoading,
109+
auth
110+
}
111+
}
112+
113+
const mapDispatchToProps = {
114+
loadProject
115+
}
116+
117+
export default withRouter(
118+
connect(mapStateToProps, mapDispatchToProps)(ProjectInvitations)
119+
)

0 commit comments

Comments
 (0)