Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commita6a06d4

Browse files
authored
feat: ability to activate suspended users (#2344)
* add ability to activate usersresolves#2254* added test* PR feedback
1 parentd48ab96 commita6a06d4

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{LanguageasResetPasswordDialogLanguage}from"../../components/ResetPasswordDialog/ResetPasswordDialog"
77
import{LanguageasRoleSelectLanguage}from"../../components/RoleSelect/RoleSelect"
88
import{LanguageasUsersTableLanguage}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{LanguageasusersXServiceLanguage}from"../../xServices/users/usersXService"
@@ -40,6 +40,35 @@ const suspendUser = async (setupActionSpies: () => void) => {
4040
fireEvent.click(confirmButton)
4141
}
4242

43+
constactivateUser=async(setupActionSpies:()=>void)=>{
44+
// Get the first user in the table
45+
constusers=awaitscreen.findAllByText(/.*@coder.com/)
46+
constfirstUserRow=users[2].closest("tr")
47+
if(!firstUserRow){
48+
thrownewError("Error on get the first user row")
49+
}
50+
51+
// Click on the "more" button to display the "Activate" option
52+
constmoreButton=within(firstUserRow).getByLabelText("more")
53+
fireEvent.click(moreButton)
54+
constmenu=screen.getByRole("menu")
55+
constactivateButton=within(menu).getByText(UsersTableLanguage.activateMenuItem)
56+
fireEvent.click(activateButton)
57+
58+
// Check if the confirm message is displayed
59+
constconfirmDialog=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+
constconfirmButton=within(confirmDialog).getByText(UsersPageLanguage.activateDialogAction)
69+
fireEvent.click(confirmButton)
70+
}
71+
4372
constresetUserPassword=async(setupActionSpies:()=>void)=>{
4473
// Get the first user in the table
4574
constusers=awaitscreen.findAllByText(/.*@coder.com/)
@@ -99,7 +128,7 @@ describe("Users Page", () => {
99128
it("shows users",async()=>{
100129
render(<UsersPage/>)
101130
constusers=awaitscreen.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+
awaitactivateUser(()=>{
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+
awaitscreen.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+
awaitactivateUser(()=>{
245+
jest.spyOn(API,"activateUser").mockRejectedValueOnce({})
246+
})
247+
248+
// Check if the error message is displayed
249+
awaitscreen.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
exportconstUsersPage:React.FC=()=>{
1922
constxServices=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
constnavigate=useNavigate()
2428
constuserToBeSuspended=users?.find((u)=>u.id===userIdToSuspend)
29+
constuserToBeActivated=users?.find((u)=>u.id===userIdToActivate)
2530
constuserToResetPassword=users?.find((u)=>u.id===userIdToResetPassword)
2631
constpermissions=useSelector(xServices.authXService,selectPermissions)
2732
constcanEditUsers=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+
exportconstSuspendedMockUser: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
exportconstMockOrganization: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-
returnres(ctx.status(200),ctx.json([M.MockUser,M.MockUser2]))
40+
returnres(ctx.status(200),ctx.json([M.MockUser,M.MockUser2,M.SuspendedMockUser]))
4141
}),
4242
rest.post("/api/v2/users",async(req,res,ctx)=>{
4343
returnres(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
returnAPI.suspendUser(context.userIdToSuspend)
225263
},
264+
activateUser:(context)=>{
265+
if(!context.userIdToActivate){
266+
thrownewError("userIdToActivate is undefined")
267+
}
268+
269+
returnAPI.activateUser(context.userIdToActivate)
270+
},
226271
resetUserPassword:(context)=>{
227272
if(!context.userIdToResetPassword){
228273
thrownewError("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
constmessage=getErrorMessage(context.suspendUserError,Language.suspendUserError)
309363
displayError(message)
310364
},
365+
displayActivateSuccess:()=>{
366+
displaySuccess(Language.activateUserSuccess)
367+
},
368+
displayActivatedErrorMessage:(context)=>{
369+
constmessage=getErrorMessage(context.activateUserError,Language.activateUserError)
370+
displayError(message)
371+
},
311372
displayResetPasswordSuccess:()=>{
312373
displaySuccess(Language.resetUserPasswordSuccess)
313374
},

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp