Skip to content

Commit 805f754

Browse files
authored
Merge pull request #3807 from appirio-tech/feature/restful-invites-and-masked-email
Restful Invites + PII
2 parents cad79f7 + ebf6691 commit 805f754

File tree

9 files changed

+84
-95
lines changed

9 files changed

+84
-95
lines changed

src/api/projectMemberInvites.js

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,27 @@ import { PROJECTS_API_URL } from '../config/constants'
44
/**
55
* Update project member invite based on project's id & given member
66
* @param {integer} projectId unique identifier of the project
7-
* @param {object} member invite to update
7+
* @param {integer} inviteId unique identifier of the invite
8+
* @param {string} status the new status for invitation
89
* @return {object} project member invite returned by api
910
*/
10-
export function updateProjectMemberInvite(projectId, member) {
11-
const url = `${PROJECTS_API_URL}/v5/projects/${projectId}/members/invite/`
12-
return axios.put(url, member)
11+
export function updateProjectMemberInvite(projectId, inviteId, status) {
12+
const url = `${PROJECTS_API_URL}/v5/projects/${projectId}/invites/${inviteId}`
13+
return axios.patch(url, { status })
1314
.then(resp => resp.data)
1415
}
1516

17+
/**
18+
* Delete project member invite based on project's id & given invite's id
19+
* @param {integer} projectId unique identifier of the project
20+
* @param {integer} inviteId unique identifier of the invite
21+
* @return {object} project member invite returned by api
22+
*/
23+
export function deleteProjectMemberInvite(projectId, inviteId) {
24+
const url = `${PROJECTS_API_URL}/v5/projects/${projectId}/invites/${inviteId}`
25+
return axios.delete(url)
26+
}
27+
1628
/**
1729
* Create a project member invite based on project's id & given member
1830
* @param {integer} projectId unique identifier of the project
@@ -21,7 +33,7 @@ export function updateProjectMemberInvite(projectId, member) {
2133
*/
2234
export function createProjectMemberInvite(projectId, member) {
2335
const fields = 'id,projectId,userId,email,role,status,createdAt,updatedAt,createdBy,updatedBy,handle'
24-
const url = `${PROJECTS_API_URL}/v5/projects/${projectId}/members/invite/?fields=` + encodeURIComponent(fields)
36+
const url = `${PROJECTS_API_URL}/v5/projects/${projectId}/invites/?fields=` + encodeURIComponent(fields)
2537
return axios({
2638
method: 'post',
2739
url,
@@ -35,7 +47,7 @@ export function createProjectMemberInvite(projectId, member) {
3547

3648
export function getProjectMemberInvites(projectId) {
3749
const fields = 'id,projectId,userId,email,role,status,createdAt,updatedAt,createdBy,updatedBy,handle'
38-
const url = `${PROJECTS_API_URL}/v5/projects/${projectId}/members/invites/?fields=`
50+
const url = `${PROJECTS_API_URL}/v5/projects/${projectId}/invites/?fields=`
3951
+ encodeURIComponent(fields)
4052
return axios.get(url)
4153
.then( resp => {
@@ -49,6 +61,6 @@ export function getProjectMemberInvites(projectId) {
4961
* @return {object} project member invite returned by api
5062
*/
5163
export function getProjectInviteById(projectId) {
52-
return axios.get(`${PROJECTS_API_URL}/v5/projects/${projectId}/members/invite/`)
64+
return axios.get(`${PROJECTS_API_URL}/v5/projects/${projectId}/invites`)
5365
.then(resp => resp.data)
5466
}

src/components/TeamManagement/ProjectManagementDialog.js

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -57,28 +57,16 @@ class ProjectManagementDialog extends React.Component {
5757
isSelectedMemberAlreadyInvited(projectTeamInvites = [], selectedMember) {
5858
return !!projectTeamInvites.find((invite) => (
5959
(invite.email && compareEmail(invite.email, selectedMember.label)) ||
60-
(invite.userId && compareHandles(this.resolveUserHandle(invite.userId), selectedMember.label))
60+
(invite.userId && compareHandles(invite.handle, selectedMember.label))
6161
))
6262
}
6363

64-
/**
65-
* Get user handle using `allMembers` which comes from props and contains all the users
66-
* which are loaded to `members.members` in the Redux store
67-
*
68-
* @param {Number} userId user id
69-
*/
70-
resolveUserHandle(userId) {
71-
const { allMembers } = this.props
72-
73-
return _.get(_.find(allMembers, { userId }), 'handle')
74-
}
75-
7664
showIndividualErrors(error) {
7765
const uniqueMessages = _.groupBy(error.failed, 'message')
7866

7967
const msgs = _.keys(uniqueMessages).map((message) => {
8068
const users = uniqueMessages[message].map((failed) => (
81-
failed.email ? failed.email : this.resolveUserHandle(failed.userId)
69+
failed.email ? failed.email : failed.handle
8270
))
8371

8472
return ({

src/components/TeamManagement/TeamManagement.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@
175175
@include roboto-bold;
176176
color: $tc-black;
177177
font-size: $tc-label-lg;
178+
line-height: 20px;
178179
max-width: calc(100% - 60px);
179180
flex: 1;
180181
overflow: hidden;

src/components/TeamManagement/TopcoderManagementDialog.js

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -114,28 +114,16 @@ class TopcoderManagementDialog extends React.Component {
114114
isSelectedMemberAlreadyInvited(topcoderTeamInvites = [], selectedMember) {
115115
return !!topcoderTeamInvites.find((invite) => (
116116
(invite.email && compareEmail(invite.email, selectedMember.label)) ||
117-
(invite.userId && compareHandles(this.resolveUserHandle(invite.userId), selectedMember.label))
117+
(invite.userId && compareHandles(invite.handle, selectedMember.label))
118118
))
119119
}
120120

121-
/**
122-
* Get user handle using `allMembers` which comes from props and contains all the users
123-
* which are loaded to `members.members` in the Redux store
124-
*
125-
* @param {Number} userId user id
126-
*/
127-
resolveUserHandle(userId) {
128-
const { allMembers } = this.props
129-
130-
return _.get(_.find(allMembers, { userId }), 'handle')
131-
}
132-
133121
showIndividualErrors(error) {
134122
const uniqueMessages = _.groupBy(error.failed, 'message')
135123

136124
const msgs = _.keys(uniqueMessages).map((message) => {
137125
const users = uniqueMessages[message].map((failed) => (
138-
failed.email ? failed.email : this.resolveUserHandle(failed.userId)
126+
failed.email ? failed.email : failed.handle
139127
))
140128

141129
return ({
@@ -276,7 +264,7 @@ class TopcoderManagementDialog extends React.Component {
276264
const approve = () => {
277265
this.setState(prevState => ({ processingInviteRequestIds: [ ...prevState.processingInviteRequestIds, invite.id ] }))
278266
approveOrDecline({
279-
userId: invite.userId,
267+
id: invite.id,
280268
status: 'request_approved'
281269
}).then(() => {
282270
this.setState(prevState => ({ processingInviteRequestIds: _.xor(prevState.processingInviteRequestIds, [invite.id]) }))
@@ -285,7 +273,7 @@ class TopcoderManagementDialog extends React.Component {
285273
const decline = () => {
286274
this.setState(prevState => ({ processingInviteRequestIds: [ ...prevState.processingInviteRequestIds, invite.id ] }))
287275
approveOrDecline({
288-
userId: invite.userId,
276+
id: invite.id,
289277
status: 'request_rejected'
290278
}).then(() => {
291279
this.setState(prevState => ({ processingInviteRequestIds: _.xor(prevState.processingInviteRequestIds, [invite.id]) }))

src/projects/actions/project.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,10 +133,19 @@ export function loadProject(projectId) {
133133
}
134134

135135
export function loadProjectInvite(projectId) {
136-
return (dispatch) => {
136+
return (dispatch, getState) => {
137+
const loadUserState = getState().loadUser
137138
return dispatch({
138139
type: LOAD_PROJECT_MEMBER_INVITE,
139140
payload: getProjectInviteById(projectId)
141+
.then((invites) => {
142+
if (loadUserState.isLoggedIn && loadUserState.user) {
143+
const user = loadUserState.user
144+
return Promise.resolve({ invites, currentUserId: user.userId, currentUserEmail: user.email })
145+
} else {
146+
return Promise.resolve(invites)
147+
}
148+
})
140149
})
141150
}
142151

src/projects/actions/projectMember.js

Lines changed: 21 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ import { addProjectMember as addMember,
44
loadMemberSuggestions as loadMemberSuggestionsAPI
55
} from '../../api/projectMembers'
66
import { createProjectMemberInvite as createProjectMemberInvite,
7-
updateProjectMemberInvite as updateProjectMemberInvite
7+
updateProjectMemberInvite as updateProjectMemberInvite,
8+
deleteProjectMemberInvite as deleteProjectMemberInvite,
89
} from '../../api/projectMemberInvites'
910
import { loadProjectMember, loadProjectMembers, loadProjectMemberInvites } from './project'
10-
import { loadMembersByHandle } from '../../actions/members'
1111

1212
import {ADD_PROJECT_MEMBER, REMOVE_PROJECT_MEMBER, UPDATE_PROJECT_MEMBER,
1313
LOAD_MEMBER_SUGGESTIONS,
@@ -17,7 +17,6 @@ import {ADD_PROJECT_MEMBER, REMOVE_PROJECT_MEMBER, UPDATE_PROJECT_MEMBER,
1717
INVITE_CUSTOMER,
1818
ACCEPT_OR_REFUSE_INVITE,
1919
PROJECT_ROLE_CUSTOMER,
20-
PROJECT_MEMBER_INVITE_STATUS_CANCELED,
2120
CLEAR_MEMBER_SUGGESTIONS,
2221
ACCEPT_OR_REFUSE_INVITE_FAILURE,
2322
ES_REINDEX_DELAY,
@@ -95,20 +94,17 @@ function inviteMembersWithData(dispatch, projectId, emailIds, handles, role) {
9594
return new Promise((resolve, reject) => {
9695
// remove `@` from handles before making a request to the server as server may not support format with `@`
9796
const clearedHandles = handles ? handles.map(handle => handle.replace(/^@/, '')) : []
98-
return dispatch(loadMembersByHandle(clearedHandles))
99-
.then(({ value }) => {
100-
const req = {}
101-
if(value && value.length > 0) {
102-
req.userIds = value.map(member => member.userId)
103-
}
104-
if(emailIds && emailIds.length > 0) {
105-
req.emails = emailIds
106-
}
107-
req.role = role
108-
createProjectMemberInvite(projectId, req)
109-
.then((res) => resolve(res))
110-
.catch(err => reject(err))
111-
})
97+
const req = {}
98+
if(clearedHandles && clearedHandles.length > 0) {
99+
req.handles = clearedHandles
100+
}
101+
if(emailIds && emailIds.length > 0) {
102+
req.emails = emailIds
103+
}
104+
req.role = role
105+
createProjectMemberInvite(projectId, req)
106+
.then((res) => resolve(res))
107+
.catch(err => reject(err))
112108
})
113109
}
114110

@@ -121,26 +117,12 @@ export function inviteTopcoderMembers(projectId, items) {
121117
}
122118
}
123119

124-
function deleteTopcoderMemberInviteWithData(projectId, invite) {
125-
return new Promise((resolve, reject) => {
126-
const req = {}
127-
if(invite.item.userId) {
128-
req.userId = invite.item.userId
129-
} else {
130-
req.email = invite.item.email
131-
}
132-
req.status = PROJECT_MEMBER_INVITE_STATUS_CANCELED
133-
updateProjectMemberInvite(projectId, req)
134-
.then((res) => resolve(res))
135-
.catch(err => reject(err))
136-
})
137-
}
138-
139120
export function deleteTopcoderMemberInvite(projectId, invite) {
140121
return (dispatch) => {
141122
dispatch({
142123
type: REMOVE_TOPCODER_MEMBER_INVITE,
143-
payload: deleteTopcoderMemberInviteWithData(projectId, invite)
124+
payload: deleteProjectMemberInvite(projectId, invite.item.id),
125+
meta: { inviteId: invite.item.id }
144126
})
145127
}
146128
}
@@ -149,7 +131,8 @@ export function deleteProjectInvite(projectId, invite) {
149131
return (dispatch) => {
150132
dispatch({
151133
type: REMOVE_CUSTOMER_INVITE,
152-
payload: deleteTopcoderMemberInviteWithData(projectId, invite)
134+
payload: deleteProjectMemberInvite(projectId, invite.item.id),
135+
meta: { inviteId: invite.item.id }
153136
})
154137
}
155138
}
@@ -170,10 +153,12 @@ export function inviteProjectMembers(projectId, emailIds, handles) {
170153
* @param {Object} currentUser current user info
171154
*/
172155
export function acceptOrRefuseInvite(projectId, item, currentUser) {
173-
return (dispatch) => {
156+
return (dispatch, getState) => {
157+
const projectState = getState().projectState
158+
const inviteId = item.id ? item.id : projectState.userInvitationId
174159
return dispatch({
175160
type: ACCEPT_OR_REFUSE_INVITE,
176-
payload: updateProjectMemberInvite(projectId, item).then((response) =>
161+
payload: updateProjectMemberInvite(projectId, inviteId, item.status).then((response) =>
177162
// we have to add delay before applying the result of accepting/declining invitation
178163
// as it takes some time for the update to be reindexed in ES so the new state is reflected
179164
// everywhere

src/projects/detail/ProjectDetail.jsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -199,8 +199,7 @@ class ProjectDetail extends Component {
199199
isUserAcceptedInvitation,
200200
})
201201
acceptOrRefuseInvite(this.props.match.params.projectId, {
202-
userId: this.props.currentUserId,
203-
email: this.props.currentUserEmail,
202+
id: this.props.userInvitationId,
204203
status: isUserAcceptedInvitation ? PROJECT_MEMBER_INVITE_STATUS_ACCEPTED : PROJECT_MEMBER_INVITE_STATUS_REFUSED
205204
})
206205
.then(() => {
@@ -252,7 +251,7 @@ class ProjectDetail extends Component {
252251
return (<LoadingIndicator />)
253252
}
254253
return (
255-
!showUserInvited?
254+
!(showUserInvited || this.shouldForceCallAcceptRefuseRequest(this.props))?
256255
<EnhancedProjectDetailView
257256
{...this.props}
258257
currentMemberRole={currentMemberRole}
@@ -298,7 +297,8 @@ const mapStateToProps = ({projectState, projectDashboard, loadUser, productsTime
298297
productsTimelines,
299298
allProductTemplates: templates.productTemplates,
300299
currentUserRoles: loadUser.user.roles,
301-
showUserInvited: projectState.showUserInvited
300+
showUserInvited: projectState.showUserInvited,
301+
userInvitationId: projectState.userInvitationId
302302
}
303303
}
304304

src/projects/list/components/Projects/Projects.jsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ class Projects extends Component {
203203
* @param {Bool} isAccept is accept invite
204204
*/
205205
callInviteRequest(project, isAccept) {
206+
const { currentUser } = this.props
206207
const setIsAcceptingInvite = (isAccepting) => {
207208
const { isAcceptingInvite } = this.state
208209

@@ -216,11 +217,15 @@ class Projects extends Component {
216217

217218
setIsAcceptingInvite(true)
218219

220+
const invite = _.find(project.invites, m => ((m.userId === currentUser.userId || m.email === currentUser.email) && !m.deletedAt && m.status === 'pending'))
221+
if (!invite) {
222+
return
223+
}
224+
219225
this.props.acceptOrRefuseInvite(project.id, {
220-
userId: this.props.currentUser.userId,
221-
email: this.props.currentUser.email,
226+
id: invite.id,
222227
status: isAccept ? PROJECT_MEMBER_INVITE_STATUS_ACCEPTED : PROJECT_MEMBER_INVITE_STATUS_REFUSED
223-
}, this.props.currentUser).then(() => {
228+
}, currentUser).then(() => {
224229
setIsAcceptingInvite(false)
225230
}) .catch((err) => {
226231
// if we fail to accept invite

src/projects/reducers/project.js

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ const initialState = {
5151
phasesNonDirty: null,
5252
isLoadingPhases: false,
5353
showUserInvited: false,
54+
userInvitationId: null,
5455
phasesStates: {} // controls opened phases and tabs of the phases
5556
}
5657

@@ -206,8 +207,14 @@ export const projectState = function (state=initialState, action) {
206207
})
207208

208209
case LOAD_PROJECT_MEMBER_INVITE_SUCCESS: {
210+
const { invites, currentUserId, currentUserEmail } = action.payload
211+
let invite
212+
if (invites && invites.length > 0) {
213+
invite = _.find(invites, m => ((m.userId === currentUserId || m.email === currentUserEmail) && !m.deletedAt && m.status === 'pending'))
214+
}
209215
return Object.assign({}, state, {
210-
showUserInvited: true
216+
showUserInvited: !!invite,
217+
userInvitationId: invite ? invite.id : null
211218
})
212219
}
213220

@@ -697,20 +704,14 @@ export const projectState = function (state=initialState, action) {
697704
return newState
698705
}
699706

700-
case REMOVE_CUSTOMER_INVITE_SUCCESS: {
701-
const newState = Object.assign({}, state)
702-
_.remove(newState.project.invites, i => action.payload.id === i.id)
703-
newState.projectNonDirty.invites = newState.project.invites
704-
newState.processingInvites = false
705-
return newState
706-
}
707-
707+
case REMOVE_CUSTOMER_INVITE_SUCCESS:
708708
case REMOVE_TOPCODER_MEMBER_INVITE_SUCCESS: {
709-
const newState = Object.assign({}, state)
710-
_.remove(newState.project.invites, i => action.payload.id === i.id)
711-
newState.projectNonDirty.invites = newState.project.invites
712-
newState.processingInvites = false
713-
return newState
709+
const idx = _.findIndex(state.project.invites, { id: action.meta.inviteId })
710+
return update(state, {
711+
processingInvites: { $set : false },
712+
project: { invites: { $splice: [[idx, 1]] } },
713+
projectNonDirty: { invites: { $splice: [[idx, 1]] } }
714+
})
714715
}
715716

716717
case UPDATE_PROJECT_MEMBER_SUCCESS: {

0 commit comments

Comments
 (0)