Skip to content
This repository was archived by the owner on Mar 13, 2025. It is now read-only.

Commit 830ff4a

Browse files
committed
able to add invites to project
1 parent cd15a8d commit 830ff4a

File tree

5 files changed

+132
-4
lines changed

5 files changed

+132
-4
lines changed

src/routes/TeamAccess/actions/index.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
deleteTeamMember,
99
deleteInvite,
1010
getMemberSuggestions,
11+
postInvites
1112
} from "services/teams";
1213

1314
export const ACTION_TYPE = {
@@ -31,6 +32,10 @@ export const ACTION_TYPE = {
3132
REMOVE_INVITE_PENDING: "REMOVE_INVITE_PENDING",
3233
REMOVE_INVITE_SUCCESS: "REMOVE_INVITE_SUCCESS",
3334
REMOVE_INVITE_ERROR: "REMOVE_INVITE_ERROR",
35+
ADD_INVITES: "ADD_INVITES",
36+
ADD_INVITES_PENDING: "ADD_INVITES_PENDING",
37+
ADD_INVITES_SUCCESS: "ADD_INVITES_SUCCESS",
38+
ADD_INVITES_ERROR: "ADD_INVITES_ERROR",
3439
CLEAR_ALL: "CLEAR_ALL",
3540
CLEAR_SUGGESTIONS: "CLEAR_SUGGESTIONS"
3641
};
@@ -141,4 +146,21 @@ export const loadSuggestions = fragment => ({
141146
*/
142147
export const clearSuggestions = () => ({
143148
type: ACTION_TYPE.CLEAR_SUGGESTIONS
149+
})
150+
151+
/**
152+
* Adds invites to team
153+
*
154+
* @param {string|number} teamId
155+
* @param {string[]} handles
156+
* @param {string[]} emails
157+
*
158+
* @returns {Promise} list of successes and failures, or error
159+
*/
160+
export const addInvites = (teamId, handles, emails) => ({
161+
type: ACTION_TYPE.ADD_INVITES,
162+
payload: async () => {
163+
const res = await postInvites(teamId, handles, emails, "customer");
164+
return res.data;
165+
}
144166
})

src/routes/TeamAccess/components/AddModal/index.jsx

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,64 @@
11
import React, { useCallback, useState } from "react";
22
import _ from 'lodash';
33
import { useDispatch, useSelector } from "react-redux";
4-
import { loadSuggestions, clearSuggestions } from "../../actions";
4+
import { toastr } from "react-redux-toastr";
5+
import { loadSuggestions, clearSuggestions, addInvites } from "../../actions";
56
import Button from "components/Button";
67
import BaseModal from "components/BaseModal";
78
import ReactSelect from "components/ReactSelect";
89

910
const SUGGESTION_TRIGGER_LENGTH = 3;
1011

11-
function AddModal({ open, onClose }) {
12+
function AddModal({ open, onClose, teamId, validateInvites}) {
1213

14+
const [loading, setLoading] = useState(false);
15+
const [error, setError] = useState();
1316
const [selectedMembers, setSelectedMembers] = useState([]);
1417
const options = useSelector(state => state.teamMembers.suggestions.map(sugg => ({ label: sugg.handle, value: sugg.handle })));
1518
const dispatch = useDispatch();
1619

1720
const debouncedLoadSuggestions = _.debounce(arg => {dispatch(loadSuggestions(arg))}, 500, {leading: true});
1821

22+
const validateSelection = () => {
23+
if (validateInvites(selectedMembers)) {
24+
setError(new Error("Project members can't be invited again. Please remove them from list"));
25+
} else {
26+
setError(undefined);
27+
}
28+
}
29+
1930
const handleClose = useCallback(() => {
2031
setSelectedMembers([]);
2132
onClose()
2233
},[onClose])
2334

35+
const submitInvites = useCallback(() => {
36+
const handles = [];
37+
const emails = [];
38+
selectedMembers.forEach(member => {
39+
const val = member.label;
40+
if (member.isEmail) {
41+
emails.push(val);
42+
} else {
43+
handles.push(val);
44+
}
45+
})
46+
47+
setLoading(true);
48+
49+
dispatch(addInvites(teamId, handles, emails))
50+
.then((res) => {
51+
setLoading(false);
52+
if (!res.value.failed) {
53+
const numInvites = res.value.success.length;
54+
const plural = numInvites !== 1 ? "s": "";
55+
handleClose()
56+
toastr.success("Invites Added", `Successfully added ${numInvites} invite${plural}`)
57+
}
58+
})
59+
60+
}, [dispatch, selectedMembers, teamId])
61+
2462
const onInputChange = useCallback((val) => {
2563
const spaceIndex = val.indexOf(" ");
2664
const semiColonIndex = val.indexOf(";");
@@ -48,14 +86,18 @@ function AddModal({ open, onClose }) {
4886
}));
4987

5088
setSelectedMembers(normalizedArr);
89+
90+
validateSelection()
91+
5192
dispatch(clearSuggestions());
5293
}, [dispatch])
5394

5495
const inviteButton = (
5596
<Button
5697
type="primary"
5798
size="medium"
58-
onClick={() => console.log('yay')}
99+
onClick={submitInvites}
100+
disabled={loading || selectedMembers.length < 1}
59101
>
60102
Invite
61103
</Button>
@@ -67,6 +109,7 @@ function AddModal({ open, onClose }) {
67109
onClose={handleClose}
68110
button={inviteButton}
69111
title="Invite more people"
112+
disabled={loading}
70113
>
71114
<ReactSelect
72115
value={selectedMembers}
@@ -76,6 +119,7 @@ function AddModal({ open, onClose }) {
76119
isMulti
77120
placeholder="Enter one or more user handles"
78121
/>
122+
{error && error.message}
79123
</BaseModal>
80124
);
81125
}

src/routes/TeamAccess/components/MemberList/index.jsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
*/
44

55
import React, { useState } from "react";
6+
import _ from 'lodash';
67
import PT from "prop-types";
78
import CardHeader from "components/CardHeader";
89
import Button from "components/Button";
@@ -21,6 +22,12 @@ function MemberList({ teamId, members, invitees }) {
2122
const [inviteOpen, setInviteOpen] = useState(false);
2223
const [isInvite, setIsInvite] = useState(false);
2324
const [deleteOpen, setDeleteOpen] = useState(false);
25+
26+
const validateInvites = (newInvites) => {
27+
return _.some(newInvites, newInvite => {
28+
(members.find(member => newInvite.label === member.handle) || invitees.find(invite => newInvite.label === invite.handle))
29+
})
30+
}
2431

2532
const openDeleteModal = (member, isInvite = false) => {
2633
setIsInvite(isInvite);
@@ -114,7 +121,7 @@ function MemberList({ teamId, members, invitees }) {
114121
teamId={teamId}
115122
isInvite={isInvite}
116123
/>
117-
<AddModal open={inviteOpen} onClose={() => setInviteOpen(false)} />
124+
<AddModal open={inviteOpen} onClose={() => setInviteOpen(false)} teamId={teamId} validateInvites={validateInvites} />
118125
</>
119126
);
120127
}

src/routes/TeamAccess/reducers/index.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const initialState = {
1010
loading: false,
1111
error: undefined,
1212
updating: false,
13+
inviteError: undefined
1314
};
1415

1516
const reducer = (state = initialState, action) => {
@@ -133,6 +134,31 @@ const reducer = (state = initialState, action) => {
133134
suggestions: []
134135
}
135136

137+
case ACTION_TYPE.ADD_INVITES_PENDING:
138+
return {
139+
...state,
140+
updating: true,
141+
inviteError: undefined
142+
}
143+
144+
case ACTION_TYPE.ADD_INVITES_SUCCESS:
145+
return {
146+
...state,
147+
invites: [...state.invites, ...action.payload.success],
148+
updating: false,
149+
inviteError: action.payload.failed ? {
150+
type: "SOME_FAILED",
151+
failed: action.payload.failed
152+
} : undefined
153+
}
154+
155+
case ACTION_TYPE.ADD_INVITES_ERROR:
156+
return {
157+
...state,
158+
updating: false,
159+
inviteError: action.payload
160+
}
161+
136162
default:
137163
return state;
138164
}

src/services/teams.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,4 +134,33 @@ export const deleteInvite = (teamId, inviteId) => {
134134
export const getMemberSuggestions = (fragment) => {
135135
const url = `${config.API.V3}/members/_suggest/${fragment}`;
136136
return axios.get(url);
137+
}
138+
139+
/**
140+
* Post new team invites
141+
*
142+
* @param {string|number} teamId team id
143+
* @param {string[]} handles user handles to add
144+
* @param {string[]} emails user emails to add
145+
* @param {string} role role to assign to users
146+
*
147+
* @returns {Promise<object>} object with successfully added invites, and failed invites
148+
*/
149+
export const postInvites = (teamId, handles, emails, role) => {
150+
const url = `${config.API.V5}/projects/${teamId}/invites/?fields=id,projectId,userId,email,role,status,createdAt,updatedAt,createdBy,updatedBy,handle`;
151+
const bodyObj = {};
152+
if (handles && handles.length > 0) {
153+
bodyObj.handles = handles;
154+
}
155+
if (emails && emails.length > 0 ) {
156+
bodyObj.emails = emails;
157+
}
158+
bodyObj.role = role;
159+
160+
return new Promise((resolve, reject) => {
161+
axios
162+
.post(url, bodyObj, { validateStatus: status => ((status >= 200 && status < 300) || status === 403)})
163+
.then((res) => resolve(res))
164+
.catch((ex) => reject(ex))
165+
})
137166
}

0 commit comments

Comments
 (0)