Skip to content

Commit a6a06d4

Browse files
authored
feat: ability to activate suspended users (#2344)
* add ability to activate users resolves #2254 * added test * PR feedback
1 parent d48ab96 commit a6a06d4

File tree

7 files changed

+190
-11
lines changed

7 files changed

+190
-11
lines changed

site/src/components/UsersTable/UsersTable.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export interface UsersTableProps {
3333
canEditUsers?: boolean
3434
isLoading?: boolean
3535
onSuspendUser: (user: TypesGen.User) => void
36+
onActivateUser: (user: TypesGen.User) => void
3637
onResetUserPassword: (user: TypesGen.User) => void
3738
onUpdateUserRoles: (user: TypesGen.User, roles: TypesGen.Role["name"][]) => void
3839
}
@@ -41,6 +42,7 @@ export const UsersTable: FC<UsersTableProps> = ({
4142
users,
4243
roles,
4344
onSuspendUser,
45+
onActivateUser,
4446
onResetUserPassword,
4547
onUpdateUserRoles,
4648
isUpdatingUserRoles,
@@ -115,12 +117,10 @@ export const UsersTable: FC<UsersTableProps> = ({
115117
},
116118
]
117119
: [
118-
// TODO: Uncomment this and add activate user functionality.
119-
// {
120-
// label: Language.activateMenuItem,
121-
// // eslint-disable-next-line @typescript-eslint/no-empty-function
122-
// onClick: function () {},
123-
// },
120+
{
121+
label: Language.activateMenuItem,
122+
onClick: onActivateUser,
123+
},
124124
]
125125
).concat({
126126
label: Language.resetPasswordMenuItem,

site/src/pages/UsersPage/UsersPage.test.tsx

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { GlobalSnackbar } from "../../components/GlobalSnackbar/GlobalSnackbar"
66
import { Language as ResetPasswordDialogLanguage } from "../../components/ResetPasswordDialog/ResetPasswordDialog"
77
import { Language as RoleSelectLanguage } from "../../components/RoleSelect/RoleSelect"
88
import { Language as UsersTableLanguage } from "../../components/UsersTable/UsersTable"
9-
import { MockAuditorRole, MockUser, MockUser2, render } from "../../testHelpers/renderHelpers"
9+
import { MockAuditorRole, MockUser, MockUser2, render, SuspendedMockUser } from "../../testHelpers/renderHelpers"
1010
import { server } from "../../testHelpers/server"
1111
import { permissionsToCheck } from "../../xServices/auth/authXService"
1212
import { Language as usersXServiceLanguage } from "../../xServices/users/usersXService"
@@ -40,6 +40,35 @@ const suspendUser = async (setupActionSpies: () => void) => {
4040
fireEvent.click(confirmButton)
4141
}
4242

43+
const activateUser = async (setupActionSpies: () => void) => {
44+
// Get the first user in the table
45+
const users = await screen.findAllByText(/.*@coder.com/)
46+
const firstUserRow = users[2].closest("tr")
47+
if (!firstUserRow) {
48+
throw new Error("Error on get the first user row")
49+
}
50+
51+
// Click on the "more" button to display the "Activate" option
52+
const moreButton = within(firstUserRow).getByLabelText("more")
53+
fireEvent.click(moreButton)
54+
const menu = screen.getByRole("menu")
55+
const activateButton = within(menu).getByText(UsersTableLanguage.activateMenuItem)
56+
fireEvent.click(activateButton)
57+
58+
// Check if the confirm message is displayed
59+
const confirmDialog = screen.getByRole("dialog")
60+
expect(confirmDialog).toHaveTextContent(
61+
`${UsersPageLanguage.activateDialogMessagePrefix} ${SuspendedMockUser.username}?`,
62+
)
63+
64+
// Setup spies to check the actions after
65+
setupActionSpies()
66+
67+
// Click on the "Confirm" button
68+
const confirmButton = within(confirmDialog).getByText(UsersPageLanguage.activateDialogAction)
69+
fireEvent.click(confirmButton)
70+
}
71+
4372
const resetUserPassword = async (setupActionSpies: () => void) => {
4473
// Get the first user in the table
4574
const users = await screen.findAllByText(/.*@coder.com/)
@@ -99,7 +128,7 @@ describe("Users Page", () => {
99128
it("shows users", async () => {
100129
render(<UsersPage />)
101130
const users = await screen.findAllByText(/.*@coder.com/)
102-
expect(users.length).toEqual(2)
131+
expect(users.length).toEqual(3)
103132
})
104133

105134
it("shows 'Create user' button to an authorized user", () => {
@@ -178,6 +207,54 @@ describe("Users Page", () => {
178207
})
179208
})
180209

210+
describe("activate user", () => {
211+
describe("when user is successfully activated", () => {
212+
it("shows a success message and refreshes the page", async () => {
213+
render(
214+
<>
215+
<UsersPage />
216+
<GlobalSnackbar />
217+
</>,
218+
)
219+
220+
await activateUser(() => {
221+
jest.spyOn(API, "activateUser").mockResolvedValueOnce(SuspendedMockUser)
222+
jest
223+
.spyOn(API, "getUsers")
224+
.mockImplementationOnce(() => Promise.resolve([MockUser, MockUser2, SuspendedMockUser]))
225+
})
226+
227+
// Check if the success message is displayed
228+
await screen.findByText(usersXServiceLanguage.activateUserSuccess)
229+
230+
// Check if the API was called correctly
231+
expect(API.activateUser).toBeCalledTimes(1)
232+
expect(API.activateUser).toBeCalledWith(SuspendedMockUser.id)
233+
})
234+
})
235+
describe("when activation fails", () => {
236+
it("shows an error message", async () => {
237+
render(
238+
<>
239+
<UsersPage />
240+
<GlobalSnackbar />
241+
</>,
242+
)
243+
244+
await activateUser(() => {
245+
jest.spyOn(API, "activateUser").mockRejectedValueOnce({})
246+
})
247+
248+
// Check if the error message is displayed
249+
await screen.findByText(usersXServiceLanguage.activateUserError)
250+
251+
// Check if the API was called correctly
252+
expect(API.activateUser).toBeCalledTimes(1)
253+
expect(API.activateUser).toBeCalledWith(SuspendedMockUser.id)
254+
})
255+
})
256+
})
257+
181258
describe("reset user password", () => {
182259
describe("when it is success", () => {
183260
it("shows a success message", async () => {

site/src/pages/UsersPage/UsersPage.tsx

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,20 @@ export const Language = {
1313
suspendDialogTitle: "Suspend user",
1414
suspendDialogAction: "Suspend",
1515
suspendDialogMessagePrefix: "Do you want to suspend the user",
16+
activateDialogTitle: "Activate user",
17+
activateDialogAction: "Activate",
18+
activateDialogMessagePrefix: "Do you want to activate the user",
1619
}
1720

1821
export const UsersPage: React.FC = () => {
1922
const xServices = useContext(XServiceContext)
2023
const [usersState, usersSend] = useActor(xServices.usersXService)
2124
const [rolesState, rolesSend] = useActor(xServices.siteRolesXService)
22-
const { users, getUsersError, userIdToSuspend, userIdToResetPassword, newUserPassword } = usersState.context
25+
const { users, getUsersError, userIdToSuspend, userIdToActivate, userIdToResetPassword, newUserPassword } =
26+
usersState.context
2327
const navigate = useNavigate()
2428
const userToBeSuspended = users?.find((u) => u.id === userIdToSuspend)
29+
const userToBeActivated = users?.find((u) => u.id === userIdToActivate)
2530
const userToResetPassword = users?.find((u) => u.id === userIdToResetPassword)
2631
const permissions = useSelector(xServices.authXService, selectPermissions)
2732
const canEditUsers = permissions && permissions.updateUsers
@@ -62,6 +67,9 @@ export const UsersPage: React.FC = () => {
6267
onSuspendUser={(user) => {
6368
usersSend({ type: "SUSPEND_USER", userId: user.id })
6469
}}
70+
onActivateUser={(user) => {
71+
usersSend({ type: "ACTIVATE_USER", userId: user.id })
72+
}}
6573
onResetUserPassword={(user) => {
6674
usersSend({ type: "RESET_USER_PASSWORD", userId: user.id })
6775
}}
@@ -99,6 +107,26 @@ export const UsersPage: React.FC = () => {
99107
}
100108
/>
101109

110+
<ConfirmDialog
111+
type="success"
112+
hideCancel={false}
113+
open={usersState.matches("confirmUserActivation")}
114+
confirmLoading={usersState.matches("activatingUser")}
115+
title={Language.activateDialogTitle}
116+
confirmText={Language.activateDialogAction}
117+
onConfirm={() => {
118+
usersSend("CONFIRM_USER_ACTIVATION")
119+
}}
120+
onClose={() => {
121+
usersSend("CANCEL_USER_ACTIVATION")
122+
}}
123+
description={
124+
<>
125+
{Language.activateDialogMessagePrefix} <strong>{userToBeActivated?.username}</strong>?
126+
</>
127+
}
128+
/>
129+
102130
<ResetPasswordDialog
103131
loading={usersState.matches("resettingUserPassword")}
104132
user={userToResetPassword}

site/src/pages/UsersPage/UsersPageView.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export interface UsersPageViewProps {
2222
isLoading?: boolean
2323
openUserCreationDialog: () => void
2424
onSuspendUser: (user: TypesGen.User) => void
25+
onActivateUser: (user: TypesGen.User) => void
2526
onResetUserPassword: (user: TypesGen.User) => void
2627
onUpdateUserRoles: (user: TypesGen.User, roles: TypesGen.Role["name"][]) => void
2728
}
@@ -31,6 +32,7 @@ export const UsersPageView: FC<UsersPageViewProps> = ({
3132
roles,
3233
openUserCreationDialog,
3334
onSuspendUser,
35+
onActivateUser,
3436
onResetUserPassword,
3537
onUpdateUserRoles,
3638
error,
@@ -60,6 +62,7 @@ export const UsersPageView: FC<UsersPageViewProps> = ({
6062
users={users}
6163
roles={roles}
6264
onSuspendUser={onSuspendUser}
65+
onActivateUser={onActivateUser}
6366
onResetUserPassword={onResetUserPassword}
6467
onUpdateUserRoles={onUpdateUserRoles}
6568
isUpdatingUserRoles={isUpdatingUserRoles}

site/src/testHelpers/entities.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,16 @@ export const MockUser2: TypesGen.User = {
5151
roles: [],
5252
}
5353

54+
export const SuspendedMockUser: TypesGen.User = {
55+
id: "suspended-mock-user",
56+
username: "SuspendedMockUser",
57+
email: "iamsuspendedsad!@coder.com",
58+
created_at: "",
59+
status: "suspended",
60+
organization_ids: ["fc0774ce-cc9e-48d4-80ae-88f7a4d4a8b0"],
61+
roles: [],
62+
}
63+
5464
export const MockOrganization: TypesGen.Organization = {
5565
id: "test-org",
5666
name: "Test Organization",

site/src/testHelpers/handlers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export const handlers = [
3737

3838
// users
3939
rest.get("/api/v2/users", async (req, res, ctx) => {
40-
return res(ctx.status(200), ctx.json([M.MockUser, M.MockUser2]))
40+
return res(ctx.status(200), ctx.json([M.MockUser, M.MockUser2, M.SuspendedMockUser]))
4141
}),
4242
rest.post("/api/v2/users", async (req, res, ctx) => {
4343
return res(ctx.status(200), ctx.json(M.MockUser))

site/src/xServices/users/usersXService.ts

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ export const Language = {
1616
createUserSuccess: "Successfully created user.",
1717
createUserError: "Error on creating the user.",
1818
suspendUserSuccess: "Successfully suspended the user.",
19-
suspendUserError: "Error on suspending the user.",
19+
suspendUserError: "Error suspending user.",
20+
activateUserSuccess: "Successfully activated the user.",
21+
activateUserError: "Error activating user.",
2022
resetUserPasswordSuccess: "Successfully updated the user password.",
2123
resetUserPasswordError: "Error on resetting the user password.",
2224
updateUserRolesSuccess: "Successfully updated the user roles.",
@@ -32,6 +34,9 @@ export interface UsersContext {
3234
// Suspend user
3335
userIdToSuspend?: TypesGen.User["id"]
3436
suspendUserError?: Error | unknown
37+
// Activate user
38+
userIdToActivate?: TypesGen.User["id"]
39+
activateUserError?: Error | unknown
3540
// Reset user password
3641
userIdToResetPassword?: TypesGen.User["id"]
3742
resetUserPasswordError?: Error | unknown
@@ -49,6 +54,10 @@ export type UsersEvent =
4954
| { type: "SUSPEND_USER"; userId: TypesGen.User["id"] }
5055
| { type: "CONFIRM_USER_SUSPENSION" }
5156
| { type: "CANCEL_USER_SUSPENSION" }
57+
// Activate events
58+
| { type: "ACTIVATE_USER"; userId: TypesGen.User["id"] }
59+
| { type: "CONFIRM_USER_ACTIVATION" }
60+
| { type: "CANCEL_USER_ACTIVATION" }
5261
// Reset password events
5362
| { type: "RESET_USER_PASSWORD"; userId: TypesGen.User["id"] }
5463
| { type: "CONFIRM_USER_PASSWORD_RESET" }
@@ -72,6 +81,9 @@ export const usersMachine = createMachine(
7281
suspendUser: {
7382
data: TypesGen.User
7483
}
84+
activateUser: {
85+
data: TypesGen.User
86+
}
7587
updateUserPassword: {
7688
data: undefined
7789
}
@@ -92,6 +104,10 @@ export const usersMachine = createMachine(
92104
target: "confirmUserSuspension",
93105
actions: ["assignUserIdToSuspend"],
94106
},
107+
ACTIVATE_USER: {
108+
target: "confirmUserActivation",
109+
actions: ["assignUserIdToActivate"],
110+
},
95111
RESET_USER_PASSWORD: {
96112
target: "confirmUserPasswordReset",
97113
actions: ["assignUserIdToResetPassword", "generateRandomPassword"],
@@ -150,6 +166,12 @@ export const usersMachine = createMachine(
150166
CANCEL_USER_SUSPENSION: "idle",
151167
},
152168
},
169+
confirmUserActivation: {
170+
on: {
171+
CONFIRM_USER_ACTIVATION: "activatingUser",
172+
CANCEL_USER_ACTIVATION: "idle",
173+
},
174+
},
153175
suspendingUser: {
154176
entry: "clearSuspendUserError",
155177
invoke: {
@@ -166,6 +188,22 @@ export const usersMachine = createMachine(
166188
},
167189
},
168190
},
191+
activatingUser: {
192+
entry: "clearActivateUserError",
193+
invoke: {
194+
src: "activateUser",
195+
id: "activateUser",
196+
onDone: {
197+
// Update users list
198+
target: "gettingUsers",
199+
actions: ["displayActivateSuccess"],
200+
},
201+
onError: {
202+
target: "idle",
203+
actions: ["assignActivateUserError", "displayActivatedErrorMessage"],
204+
},
205+
},
206+
},
169207
confirmUserPasswordReset: {
170208
on: {
171209
CONFIRM_USER_PASSWORD_RESET: "resettingUserPassword",
@@ -223,6 +261,13 @@ export const usersMachine = createMachine(
223261

224262
return API.suspendUser(context.userIdToSuspend)
225263
},
264+
activateUser: (context) => {
265+
if (!context.userIdToActivate) {
266+
throw new Error("userIdToActivate is undefined")
267+
}
268+
269+
return API.activateUser(context.userIdToActivate)
270+
},
226271
resetUserPassword: (context) => {
227272
if (!context.userIdToResetPassword) {
228273
throw new Error("userIdToResetPassword is undefined")
@@ -258,6 +303,9 @@ export const usersMachine = createMachine(
258303
assignUserIdToSuspend: assign({
259304
userIdToSuspend: (_, event) => event.userId,
260305
}),
306+
assignUserIdToActivate: assign({
307+
userIdToActivate: (_, event) => event.userId,
308+
}),
261309
assignUserIdToResetPassword: assign({
262310
userIdToResetPassword: (_, event) => event.userId,
263311
}),
@@ -278,6 +326,9 @@ export const usersMachine = createMachine(
278326
assignSuspendUserError: assign({
279327
suspendUserError: (_, event) => event.data,
280328
}),
329+
assignActivateUserError: assign({
330+
activateUserError: (_, event) => event.data,
331+
}),
281332
assignResetUserPasswordError: assign({
282333
resetUserPasswordError: (_, event) => event.data,
283334
}),
@@ -292,6 +343,9 @@ export const usersMachine = createMachine(
292343
clearSuspendUserError: assign({
293344
suspendUserError: (_) => undefined,
294345
}),
346+
clearActivateUserError: assign({
347+
activateUserError: (_) => undefined,
348+
}),
295349
clearResetUserPasswordError: assign({
296350
resetUserPasswordError: (_) => undefined,
297351
}),
@@ -308,6 +362,13 @@ export const usersMachine = createMachine(
308362
const message = getErrorMessage(context.suspendUserError, Language.suspendUserError)
309363
displayError(message)
310364
},
365+
displayActivateSuccess: () => {
366+
displaySuccess(Language.activateUserSuccess)
367+
},
368+
displayActivatedErrorMessage: (context) => {
369+
const message = getErrorMessage(context.activateUserError, Language.activateUserError)
370+
displayError(message)
371+
},
311372
displayResetPasswordSuccess: () => {
312373
displaySuccess(Language.resetUserPasswordSuccess)
313374
},

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy