Skip to content

Commit 9cd0e98

Browse files
author
dushimsam
committed
feat(UI): added manage group-users page
Signed-off-by: dushimsam <[email protected]>
1 parent cf11dae commit 9cd0e98

File tree

11 files changed

+564
-3
lines changed

11 files changed

+564
-3
lines changed

src/Routes.jsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ const DeleteGroup = React.lazy(() => import("pages/Admin/Group/Delete"));
9494
const DeleteUser = React.lazy(() => import("pages/Admin/Users/Delete"));
9595
const AddUser = React.lazy(() => import("pages/Admin/Users/Add"));
9696
const EditUser = React.lazy(() => import("pages/Admin/Users/Edit"));
97+
const ManageGroup = React.lazy(() => import("pages/Admin/Group/Manage"));
9798
const AddLicense = React.lazy(() => import("pages/Admin/License/Create"));
9899
const SelectLicense = React.lazy(() =>
99100
import("pages/Admin/License/SelectLicense")
@@ -292,6 +293,11 @@ const Routes = () => {
292293
path={routes.admin.group.delete}
293294
component={DeleteGroup}
294295
/>
296+
<AdminLayout
297+
exact
298+
path={routes.admin.group.manageGroup}
299+
component={ManageGroup}
300+
/>
295301
<AdminLayout
296302
exact
297303
path={routes.admin.license.create}

src/api/groups.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,42 @@ export const deleteGroupApi = (id) => {
7676
addGroupName: false,
7777
});
7878
};
79+
80+
// Get all group members
81+
export const getAllGroupMembersApi = (groupId) => {
82+
const url = endpoints.admin.groups.getAllGroupMembers(groupId);
83+
return sendRequest({
84+
url,
85+
method: "GET",
86+
headers: {
87+
Authorization: getToken(),
88+
},
89+
});
90+
};
91+
92+
// Remove Group Member
93+
export const removeGroupMemberApi = (groupId, userId) => {
94+
const url = endpoints.admin.groups.removeGroupMember(groupId, userId);
95+
return sendRequest({
96+
url,
97+
method: "DELETE",
98+
headers: {
99+
Authorization: getToken(),
100+
},
101+
});
102+
};
103+
104+
// Change user permission
105+
export const changeUserPermissionApi = (groupId, userId, permission) => {
106+
const url = endpoints.admin.groups.changeUserPermission(groupId, userId);
107+
return sendRequest({
108+
url,
109+
method: "PUT",
110+
headers: {
111+
Authorization: getToken(),
112+
},
113+
body: {
114+
perm: permission,
115+
},
116+
});
117+
};

src/api/groups.test.js

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,13 @@
1515

1616
import sendRequest from "api/sendRequest";
1717
import endpoints from "constants/endpoints";
18-
import { createGroupApi, getAllGroupsApi } from "api/groups";
18+
import {
19+
changeUserPermissionApi,
20+
createGroupApi,
21+
getAllGroupMembersApi,
22+
getAllGroupsApi,
23+
removeGroupMemberApi,
24+
} from "api/groups";
1925
import { getToken } from "shared/authHelper";
2026

2127
jest.mock("api/sendRequest");
@@ -56,4 +62,63 @@ describe("groups", () => {
5662
})
5763
);
5864
});
65+
66+
test("removeGroupMemberApi", () => {
67+
const groupId = 2;
68+
const userId = 1;
69+
const url = endpoints.admin.groups.removeGroupMember(groupId, userId);
70+
sendRequest.mockImplementation(() => true);
71+
72+
expect(removeGroupMemberApi(groupId, userId)).toBe(sendRequest({}));
73+
expect(sendRequest).toHaveBeenCalledWith(
74+
expect.objectContaining({
75+
url,
76+
method: "DELETE",
77+
headers: {
78+
Authorization: getToken(),
79+
},
80+
})
81+
);
82+
});
83+
84+
test("getAllGroupMembersApi", () => {
85+
const groupId = 1;
86+
const url = endpoints.admin.groups.getAllGroupMembers(groupId);
87+
sendRequest.mockImplementation(() => true);
88+
89+
expect(getAllGroupMembersApi(groupId)).toBe(sendRequest({}));
90+
expect(sendRequest).toHaveBeenCalledWith(
91+
expect.objectContaining({
92+
url,
93+
method: "GET",
94+
headers: {
95+
Authorization: getToken(),
96+
},
97+
})
98+
);
99+
});
100+
101+
test("changeUserPermissionApi", () => {
102+
const groupId = 1;
103+
const userId = 1;
104+
const permission = 2;
105+
const url = endpoints.admin.groups.changeUserPermission(groupId, userId);
106+
sendRequest.mockImplementation(() => true);
107+
108+
expect(changeUserPermissionApi(groupId, userId, permission)).toBe(
109+
sendRequest({})
110+
);
111+
expect(sendRequest).toHaveBeenCalledWith(
112+
expect.objectContaining({
113+
url,
114+
method: "PUT",
115+
headers: {
116+
Authorization: getToken(),
117+
},
118+
body: {
119+
perm: permission,
120+
},
121+
})
122+
);
123+
});
59124
});
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
/*
2+
Copyright (C) 2022 Samuel Dushimimana ([email protected])
3+
4+
SPDX-License-Identifier: GPL-2.0
5+
6+
This program is free software; you can redistribute it and/or
7+
modify it under the terms of the GNU General Public License
8+
version 2 as published by the Free Software Foundation.
9+
This program is distributed in the hope that it will be useful,
10+
but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
GNU General Public License for more details.
13+
14+
You should have received a copy of the GNU General Public License along
15+
with this program; if not, write to the Free Software Foundation, Inc.,
16+
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17+
*/
18+
19+
import React, { useEffect, useState } from "react";
20+
21+
import { InputContainer } from "components/Widgets";
22+
23+
// Required functions for calling APIs
24+
import { changeUserPermission, removeGroupMember } from "services/groups";
25+
26+
import PropTypes from "prop-types";
27+
28+
// Required constants
29+
import { userPermissions } from "constants/constants";
30+
31+
const ChangePermissionContainer = ({
32+
groupMembers,
33+
noneGroupMembers,
34+
setShowMessage,
35+
setMessage,
36+
currGroup,
37+
handleFetchGroupMembers,
38+
}) => {
39+
const [currUser, setCurrentUser] = useState(null);
40+
const [currNonMember, setCurrentNonMember] = useState(null);
41+
42+
useEffect(() => {
43+
if (groupMembers.length > 0) {
44+
setCurrentUser({
45+
user: groupMembers[0].id,
46+
perm: groupMembers[0].group_perm,
47+
});
48+
}
49+
}, [groupMembers]);
50+
51+
useEffect(() => {
52+
if (noneGroupMembers.length > 0) {
53+
setCurrentNonMember({
54+
user: noneGroupMembers[0].id,
55+
perm: -1,
56+
});
57+
}
58+
}, [noneGroupMembers]);
59+
60+
const handleChangeCurrUser = async (newUser, isMember = true) => {
61+
if (isMember) {
62+
let perm;
63+
groupMembers.forEach((item) => {
64+
if (item.id === parseInt(newUser, 10)) {
65+
perm = item.group_perm;
66+
}
67+
});
68+
setCurrentUser({ user: parseInt(newUser, 10), perm });
69+
} else {
70+
setCurrentNonMember({ user: parseInt(newUser, 10), perm: -1 });
71+
}
72+
};
73+
74+
const handleSetNewPermission = async (newPerm, isMember = true) => {
75+
try {
76+
let res;
77+
78+
if (parseInt(newPerm, 10) === -1) {
79+
res = await removeGroupMember(currGroup, currUser.user);
80+
} else {
81+
res = await changeUserPermission(
82+
currGroup,
83+
isMember ? currUser.user : currNonMember.user,
84+
parseInt(newPerm, 10)
85+
);
86+
}
87+
88+
setShowMessage(true);
89+
setMessage({
90+
type: "success",
91+
text: res.message,
92+
});
93+
94+
handleFetchGroupMembers(currGroup);
95+
} catch (e) {
96+
setMessage({
97+
type: "danger",
98+
text: e.message,
99+
});
100+
} finally {
101+
setTimeout(() => {
102+
setShowMessage(false);
103+
}, [3000]);
104+
}
105+
};
106+
return (
107+
<>
108+
<div className="container">
109+
<div className="row">
110+
{groupMembers.length > 0 ? (
111+
<div className="col-md-12 col-sm-12 col-12 mt-3">
112+
<TableFill
113+
title="Group Members"
114+
ContentFill={
115+
<tr>
116+
<td>
117+
<InputContainer
118+
type="select"
119+
name="name"
120+
options={groupMembers}
121+
id="select-user-tag"
122+
value={currUser?.user}
123+
property="name"
124+
onChange={(e) => handleChangeCurrUser(e.target.value)}
125+
/>
126+
</td>
127+
<td>
128+
<InputContainer
129+
type="select"
130+
name="name"
131+
options={userPermissions}
132+
id="select-tag"
133+
value={currUser?.perm}
134+
property="name"
135+
onChange={(e) => handleSetNewPermission(e.target.value)}
136+
/>
137+
</td>
138+
</tr>
139+
}
140+
/>
141+
</div>
142+
) : (
143+
<></>
144+
)}
145+
{noneGroupMembers.length > 0 ? (
146+
<div className="col-md-12 col-sm-12 col-12 mt-3">
147+
<TableFill
148+
title="None Group Members"
149+
ContentFill={
150+
<tr>
151+
<td>
152+
<InputContainer
153+
type="select"
154+
name="name"
155+
options={noneGroupMembers}
156+
id="select-user-tag"
157+
value={currNonMember?.user}
158+
property="name"
159+
onChange={(e) =>
160+
handleChangeCurrUser(e.target.value, false)
161+
}
162+
/>
163+
</td>
164+
<td>
165+
<InputContainer
166+
type="select"
167+
name="name"
168+
options={userPermissions}
169+
id="select-tag"
170+
value={-1}
171+
property="name"
172+
onChange={(e) =>
173+
handleSetNewPermission(e.target.value, false)
174+
}
175+
/>
176+
</td>
177+
</tr>
178+
}
179+
/>
180+
</div>
181+
) : (
182+
<></>
183+
)}
184+
</div>
185+
</div>
186+
</>
187+
);
188+
};
189+
190+
// eslint-disable-next-line react/prop-types
191+
export const TableFill = ({ ContentFill, title }) => (
192+
<>
193+
<h5>{title}</h5>
194+
<table className="table table-striped table-bordered rounded mt-1">
195+
<thead className="bg-dark text-light font-weight-bold">
196+
<tr>
197+
<th>User</th>
198+
<th>Permission</th>
199+
</tr>
200+
</thead>
201+
<tbody>{ContentFill}</tbody>
202+
</table>
203+
</>
204+
);
205+
206+
TableFill.prototype = {
207+
ContentFill: PropTypes.any,
208+
title: PropTypes.string,
209+
};
210+
211+
ChangePermissionContainer.propTypes = {
212+
groupMembers: PropTypes.arrayOf(PropTypes.objectOf(PropTypes.any)).isRequired,
213+
noneGroupMembers: PropTypes.arrayOf(PropTypes.objectOf(PropTypes.any))
214+
.isRequired,
215+
setMessage: PropTypes.func,
216+
currGroup: PropTypes.number,
217+
handleFetchGroupMembers: PropTypes.func,
218+
setShowMessage: PropTypes.func,
219+
};
220+
221+
export default ChangePermissionContainer;

src/components/Header/index.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -258,9 +258,9 @@ const Header = () => {
258258
</NavDropdown.Item>
259259
<NavDropdown.Item
260260
as={Link}
261-
to={routes.admin.group.delete}
261+
to={routes.admin.group.manageGroup}
262262
>
263-
Delete Group
263+
Manage Group Users
264264
</NavDropdown.Item>
265265
</div>
266266
</DropdownButton>

src/components/Widgets/Input/index.jsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const InputContainer = ({
2626
id,
2727
className,
2828
onChange,
29+
defaultValue = null,
2930
children,
3031
checked = false,
3132
placeholder = null,
@@ -70,6 +71,7 @@ const InputContainer = ({
7071
className ? `mr-2 form-control ${className}` : `mr-2 form-control`
7172
}
7273
value={value}
74+
defaultValue={defaultValue}
7375
onChange={onChange}
7476
multiple={multiple && multiple}
7577
size={multiple ? "15" : ""}
@@ -125,6 +127,7 @@ InputContainer.propTypes = {
125127
onChange: PropTypes.func,
126128
checked: PropTypes.bool,
127129
disabled: PropTypes.bool,
130+
defaultValue: PropTypes.string,
128131
children: PropTypes.node,
129132
options: PropTypes.arrayOf(
130133
PropTypes.shape({

0 commit comments

Comments
 (0)