Skip to content

Commit 9731ddc

Browse files
committed
winning submission from challenge 30085188 - Topcoder Connect - Handle invitation errors
1 parent 8a6d891 commit 9731ddc

File tree

6 files changed

+124
-16
lines changed

6 files changed

+124
-16
lines changed

src/api/projectMemberInvites.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,16 @@ export function updateProjectMemberInvite(projectId, member) {
2323
*/
2424
export function createProjectMemberInvite(projectId, member) {
2525
const url = `${PROJECTS_API_URL}/v4/projects/${projectId}/members/invite/`
26-
return axios.post(url, { param: member})
26+
return axios({
27+
method: 'post',
28+
url,
29+
data: {
30+
param: member
31+
},
32+
validateStatus (status) {
33+
return (status >= 200 && status < 300) || status === 403
34+
},
35+
})
2736
.then(resp => {
2837
return resp.data.result.content
2938
})

src/components/TeamManagement/AutocompleteInput.jsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React from 'react'
22
import PropTypes from 'prop-types'
3-
import {findIndex} from 'lodash'
3+
import {findIndex, find} from 'lodash'
44
import Select from '../Select/Select'
55
import './AutocompleteInput.scss'
66
import {loadMemberSuggestions} from '../../api/projectMembers'
@@ -51,6 +51,8 @@ class AutocompleteInput extends React.Component {
5151
}
5252
if (value.length >= AUTOCOMPLETE_TRIGGER_LENGTH) {
5353
return this.loadMemberSuggestionsDebounced(value).then(r => {
54+
const exists = find(r, (member) => member.handle === value)
55+
if(exists) createOption.userId = exists.userId
5456
// Remove current members from suggestions
5557
const suggestions = r.filter(suggestion => (
5658
findIndex(allMembers, (member) => member.handle === suggestion.handle) === -1 &&
@@ -59,7 +61,6 @@ class AutocompleteInput extends React.Component {
5961
))
6062
// Only allow creation if it is not already exists in members
6163
const shouldIncludeCreateOption = findIndex(allMembers, (member) => member.handle === value) === -1
62-
6364
return Promise.resolve({options: shouldIncludeCreateOption?[createOption, ...suggestions]: suggestions})
6465
}).catch( () => {
6566
return Promise.resolve({options: [createOption] })

src/components/TeamManagement/ProjectManagementDialog.js

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@ class Dialog extends React.Component {
1515
clearText: false,
1616
showAlreadyMemberError: false,
1717
invitedMembers: {},
18-
members: {}
18+
members: {},
19+
errorMessage: null
1920
}
2021
this.onChange = this.onChange.bind(this)
22+
this.onErrorMessage = this.onErrorMessage.bind(this)
2123
}
2224

2325
componentWillMount() {
@@ -27,6 +29,13 @@ class Dialog extends React.Component {
2729
})
2830
}
2931

32+
componentWillReceiveProps(nextProps) {
33+
const {processingInvites} = this.props
34+
if (processingInvites && !nextProps.processingInvites && nextProps.error ) {
35+
this.onErrorMessage(nextProps.error, this.props.selectedMembers)
36+
}
37+
}
38+
3039
onChange(selectedMembers) {
3140
// If a member invited with email exists in selectedMembers
3241
let present = _.some(this.state.invitedMembers, invited => _.findIndex(selectedMembers,
@@ -49,6 +58,33 @@ class Dialog extends React.Component {
4958
this.props.onSelectedMembersUpdate(selectedMembers)
5059
}
5160

61+
onErrorMessage(error, selectedMembers) {
62+
if(error.msg) {
63+
this.setState({
64+
errorMessage: error.msg
65+
})
66+
return
67+
}
68+
const msg = []
69+
_.forEach(error.failed, (failed) => {
70+
const existingMessage = _.find(msg, (m) => failed.message === m.message)
71+
if(existingMessage) {
72+
existingMessage.allUsers.push((failed.email ? failed.email: _.find(selectedMembers, (m) => m.userId === failed.id).handle))
73+
const index = _.find(msg, (m) => failed.message === m.message)
74+
msg.splice(index, 1, existingMessage)
75+
} else {
76+
msg.push({
77+
message: failed.message,
78+
allUsers: [ (failed.email ? failed.email: _.find(selectedMembers, (m) => m.userId === failed.id).handle) ]
79+
})
80+
}
81+
})
82+
const listMessages = _.map(msg, m => `${m.allUsers} : ${m.message}`)
83+
this.setState({
84+
errorMessage: _.join(listMessages, '\n')
85+
})
86+
}
87+
5288
render() {
5389
const {
5490
members, currentUser, isMember, removeMember, removeInvite,
@@ -161,6 +197,9 @@ class Dialog extends React.Component {
161197
{this.state.showAlreadyMemberError && <div className="error-message">
162198
Project Member(s) can't be invited again. Please remove them from list.
163199
</div>}
200+
{ this.state.errorMessage && <div className="error-message">
201+
{this.state.errorMessage}
202+
</div> }
164203
<button
165204
className="tc-btn tc-btn-primary tc-btn-md"
166205
type="submit"

src/components/TeamManagement/TopcoderManagementDialog.js

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,15 @@ class Dialog extends React.Component {
2222
managerType: {},
2323
clearText: false,
2424
members: {},
25-
showAlreadyMemberError: false
25+
showAlreadyMemberError: false,
26+
errorMessage: null,
2627
}
2728

2829
this.onUserRoleChange = this.onUserRoleChange.bind(this)
2930
this.handleRoles = this.handleRoles.bind(this)
3031
this.addUsers = this.addUsers.bind(this)
3132
this.onChange = this.onChange.bind(this)
33+
this.onErrorMessage = this.onErrorMessage.bind(this)
3234
}
3335

3436
componentWillMount(){
@@ -89,6 +91,40 @@ class Dialog extends React.Component {
8991
this.props.onSelectedMembersUpdate(selectedMembers)
9092
}
9193

94+
componentWillReceiveProps(nextProps) {
95+
const {processingInvites} = this.props
96+
if (processingInvites && !nextProps.processingInvites && nextProps.error ) {
97+
this.onErrorMessage(nextProps.error, this.props.selectedMembers)
98+
}
99+
}
100+
101+
onErrorMessage(error, selectedMembers) {
102+
if(error.msg) {
103+
this.setState({
104+
errorMessage: error.msg
105+
})
106+
return
107+
}
108+
const msg = []
109+
_.forEach(error.failed, (failed) => {
110+
const existingMessage = _.find(msg, (m) => failed.message === m.message)
111+
if(existingMessage) {
112+
existingMessage.allUsers.push((failed.email ? failed.email: _.find(selectedMembers, (m) => m.userId === failed.id).handle))
113+
const index = _.find(msg, (m) => failed.message === m.message)
114+
msg.splice(index, 1, existingMessage)
115+
} else {
116+
msg.push({
117+
message: failed.message,
118+
allUsers: [ (failed.email ? failed.email: _.find(selectedMembers, (m) => m.userId === failed.id).handle) ]
119+
})
120+
}
121+
})
122+
const listMessages = _.map(msg, m => `${m.allUsers} : ${m.message}`)
123+
this.setState({
124+
errorMessage: _.join(listMessages, '\n')
125+
})
126+
}
127+
92128
render() {
93129
const {
94130
members, currentUser, isMember, removeMember, onCancel, removeInvite, approveOrDecline, invites = [],
@@ -98,6 +134,12 @@ class Dialog extends React.Component {
98134
const showApproveDecline = currentUser.isAdmin || currentUser.isCopilotManager
99135
let i = 0
100136
const allMembers = [...members, ...invites.map(i => i.member)]
137+
const existingInvites = _.map(invites, (m) => m.member.handle)
138+
let newSelectedMembers = []
139+
if(selectedMembers) {
140+
newSelectedMembers = selectedMembers.filter(i => !_.includes(existingInvites, i.handle))
141+
}
142+
101143
return (
102144
<Modal
103145
isOpen
@@ -244,7 +286,7 @@ class Dialog extends React.Component {
244286
</div>
245287

246288
{
247-
invite.status===PROJECT_MEMBER_INVITE_STATUS_REQUESTED && showApproveDecline &&
289+
invite.status===PROJECT_MEMBER_INVITE_STATUS_REQUESTED && showApproveDecline &&
248290
<div className="member-remove">
249291
<span onClick={approve}>approve</span>
250292
<span onClick={decline}>decline</span>
@@ -254,15 +296,15 @@ class Dialog extends React.Component {
254296
</div>
255297
}
256298
{
257-
invite.status===PROJECT_MEMBER_INVITE_STATUS_REQUESTED && !showApproveDecline && showRemove &&
299+
invite.status===PROJECT_MEMBER_INVITE_STATUS_REQUESTED && !showApproveDecline && showRemove &&
258300
<div className="member-remove">
259301
<span className="email-date">
260302
Requested {moment(invite.createdAt).format('MMM D, YY')}
261303
</span>
262304
</div>
263305
}
264306
{
265-
invite.status===PROJECT_MEMBER_INVITE_STATUS_PENDING && showRemove &&
307+
invite.status===PROJECT_MEMBER_INVITE_STATUS_PENDING && showRemove &&
266308
<div className="member-remove" onClick={remove}>
267309
Remove
268310
<span className="email-date">
@@ -271,14 +313,14 @@ class Dialog extends React.Component {
271313
</div>
272314
}
273315
{
274-
invite.status===PROJECT_MEMBER_INVITE_STATUS_PENDING && !showRemove &&
316+
invite.status===PROJECT_MEMBER_INVITE_STATUS_PENDING && !showRemove &&
275317
<div className="member-remove" >
276318
<span className="email-date">
277319
Invited {moment(invite.createdAt).format('MMM D, YY')}
278320
</span>
279321
</div>
280322
}
281-
323+
282324
</div>
283325
)
284326
}))}
@@ -289,7 +331,7 @@ class Dialog extends React.Component {
289331
<AutocompleteInput
290332
onUpdate={this.onChange}
291333
currentUser={currentUser}
292-
selectedMembers={selectedMembers}
334+
selectedMembers={newSelectedMembers}
293335
disabled={processingInvites || (!currentUser.isAdmin && !isMember && !currentUser.isCopilotManager)}
294336
allMembers={allMembers}
295337
/>
@@ -305,13 +347,16 @@ class Dialog extends React.Component {
305347
onSelect={this.handleRoles}
306348
/>
307349
</Formsy.Form>
350+
{ this.state.errorMessage && <div className="error-message">
351+
{this.state.errorMessage}
352+
</div> }
308353
<button
309354
className="tc-btn tc-btn-primary tc-btn-md"
310355
type="submit"
311356
disabled={processingInvites || !this.state.validUserText || this.state.clearText}
312357
onClick={this.addUsers}
313358
>
314-
{_.find(this.roles, {value:this.state.userRole}).canAddDirectly && !showApproveDecline
359+
{_.find(this.roles, {value:this.state.userRole}).canAddDirectly && !showApproveDecline
315360
?'Request invite'
316361
:'Invite users'}
317362
</button>

src/projects/reducers/project.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -514,17 +514,29 @@ export const projectState = function (state=initialState, action) {
514514

515515
case INVITE_CUSTOMER_SUCCESS: {
516516
const newState = Object.assign({}, state)
517-
newState.project.invites.push(...action.payload)
517+
newState.project.invites.push(...action.payload.success)
518518
newState.processingInvites = false
519519
newState.error = false
520+
if (action.payload.failed) {
521+
newState.error = {
522+
type: action.type,
523+
failed: action.payload.failed,
524+
}
525+
}
520526
return newState
521527
}
522528

523529
case INVITE_TOPCODER_MEMBER_SUCCESS: {
524530
const newState = Object.assign({}, state)
525-
newState.project.invites.push(...action.payload)
531+
newState.project.invites.push(...action.payload.success)
526532
newState.processingInvites = false
527533
newState.error = false
534+
if (action.payload.failed) {
535+
newState.error = {
536+
type: action.type,
537+
failed: action.payload.failed,
538+
}
539+
}
528540
return newState
529541
}
530542

src/reducers/alerts.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ export default function(state = {}, action) {
132132
case ADD_PROJECT_ATTACHMENT_SUCCESS:
133133
Alert.success('Added attachment to the project successfully')
134134
return state
135-
135+
136136
case UPDATE_PROJECT_ATTACHMENT_SUCCESS:
137137
Alert.success('Updated attachment succcessfully')
138138
return state
@@ -142,7 +142,9 @@ export default function(state = {}, action) {
142142

143143
case INVITE_TOPCODER_MEMBER_SUCCESS:
144144
case INVITE_CUSTOMER_SUCCESS:
145-
Alert.success('You\'ve successfully invited member(s).')
145+
if(action.payload.success.length) {
146+
Alert.success('You\'ve successfully invited member(s).')
147+
}
146148
return state
147149

148150
case REMOVE_TOPCODER_MEMBER_INVITE_SUCCESS:

0 commit comments

Comments
 (0)