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

Commitf911c8a

Browse files
feat: Add suspend user action (#1275)
1 parent34b91fd commitf911c8a

File tree

13 files changed

+23635
-31
lines changed

13 files changed

+23635
-31
lines changed

‎site/jest.setup.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import"@testing-library/jest-dom"
12
import{server}from"./src/testHelpers/server"
23

34
// Establish API mocking before all tests through MSW.

‎site/package-lock.json

Lines changed: 23318 additions & 0 deletions
Some generated files are not rendered by default. Learn more aboutcustomizing how changed files appear on GitHub.

‎site/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
"@storybook/addon-essentials":"6.4.22",
5959
"@storybook/addon-links":"6.4.22",
6060
"@storybook/react":"6.4.22",
61+
"@testing-library/jest-dom":"5.16.4",
6162
"@testing-library/react":"12.1.5",
6263
"@testing-library/user-event":"14.1.1",
6364
"@types/express":"4.17.13",

‎site/src/api/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ export const getApiKey = async (): Promise<Types.APIKeyResponse> => {
7676
}
7777

7878
exportconstgetUsers=async():Promise<TypesGen.User[]>=>{
79-
constresponse=awaitaxios.get<TypesGen.User[]>("/api/v2/users?offset=0&limit=1000")
79+
constresponse=awaitaxios.get<TypesGen.User[]>("/api/v2/users?status=active")
8080
returnresponse.data
8181
}
8282

@@ -135,3 +135,8 @@ export const updateProfile = async (userId: string, data: Types.UpdateProfileReq
135135
constresponse=awaitaxios.put(`/api/v2/users/${userId}/profile`,data)
136136
returnresponse.data
137137
}
138+
139+
exportconstsuspendUser=async(userId:TypesGen.User["id"]):Promise<TypesGen.User>=>{
140+
constresponse=awaitaxios.put<TypesGen.User>(`/api/v2/users/${userId}/suspend`)
141+
returnresponse.data
142+
}

‎site/src/components/GlobalSnackbar/utils.test.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import{displaySuccess,isNotificationTextPrefixed,MsgType,NotificationMsg}from"./utils"
1+
import{
2+
displayError,
3+
displaySuccess,
4+
isNotificationTextPrefixed,
5+
MsgType,
6+
NotificationMsg,
7+
SnackbarEventType,
8+
}from"./utils"
29

310
describe("Snackbar",()=>{
411
describe("isNotificationTextPrefixed",()=>{
@@ -76,4 +83,18 @@ describe("Snackbar", () => {
7683
expect(extractNotificationEvent(dispatchEventMock)).toStrictEqual(expected)
7784
})
7885
})
86+
87+
describe("displayError",()=>{
88+
it("shows the title and the message",(done)=>{
89+
constmessage="Some error happened"
90+
91+
window.addEventListener(SnackbarEventType,(event)=>{
92+
constnotificationEvent=eventasCustomEvent<NotificationMsg>
93+
expect(notificationEvent.detail.msg).toEqual(message)
94+
done()
95+
})
96+
97+
displayError(message)
98+
})
99+
})
79100
})

‎site/src/components/GlobalSnackbar/utils.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,7 @@ export const displayMsg = (msg: string, additionalMsg?: string): void => {
6060
exportconstdisplaySuccess=(msg:string,additionalMsg?:string):void=>{
6161
dispatchNotificationEvent(MsgType.Success,msg,additionalMsg ?[additionalMsg] :undefined)
6262
}
63+
64+
exportconstdisplayError=(msg:string,additionalMsg?:string):void=>{
65+
dispatchNotificationEvent(MsgType.Error,msg,additionalMsg ?[additionalMsg] :undefined)
66+
}

‎site/src/components/UsersTable/UsersTable.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { Column, Table } from "../Table/Table"
55
import{TableRowMenu}from"../TableRowMenu/TableRowMenu"
66
import{UserCell}from"../UserCell/UserCell"
77

8-
constLanguage={
8+
exportconstLanguage={
99
pageTitle:"Users",
1010
usersTitle:"All users",
1111
emptyMessage:"No users found",
@@ -27,9 +27,10 @@ const columns: Column<UserResponse>[] = [
2727

2828
exportinterfaceUsersTableProps{
2929
users:UserResponse[]
30+
onSuspendUser:(user:UserResponse)=>void
3031
}
3132

32-
exportconstUsersTable:React.FC<UsersTableProps>=({ users})=>{
33+
exportconstUsersTable:React.FC<UsersTableProps>=({ users, onSuspendUser})=>{
3334
return(
3435
<Table
3536
columns={columns}
@@ -42,9 +43,7 @@ export const UsersTable: React.FC<UsersTableProps> = ({ users }) => {
4243
menuItems={[
4344
{
4445
label:Language.suspendMenuItem,
45-
onClick:()=>{
46-
// TO-DO: Add suspend action here
47-
},
46+
onClick:onSuspendUser,
4847
},
4948
]}
5049
/>
Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,93 @@
1-
import{screen}from"@testing-library/react"
1+
import{fireEvent,screen,waitFor,within}from"@testing-library/react"
22
importReactfrom"react"
3-
import{render}from"../../testHelpers"
4-
import{UsersPage}from"./UsersPage"
3+
import*asAPIfrom"../../api"
4+
import{GlobalSnackbar}from"../../components/GlobalSnackbar/GlobalSnackbar"
5+
import{LanguageasUsersTableLanguage}from"../../components/UsersTable/UsersTable"
6+
import{MockUser,MockUser2,render}from"../../testHelpers"
7+
import{LanguageasusersXServiceLanguage}from"../../xServices/users/usersXService"
8+
import{LanguageasUsersPageLanguage,UsersPage}from"./UsersPage"
9+
10+
constsuspendUser=async(setupActionSpies:()=>void)=>{
11+
// Get the first user in the table
12+
constusers=awaitscreen.findAllByText(/.*@coder.com/)
13+
constfirstUserRow=users[0].closest("tr")
14+
if(!firstUserRow){
15+
thrownewError("Error on get the first user row")
16+
}
17+
18+
// Click on the "more" button to display the "Suspend" option
19+
constmoreButton=within(firstUserRow).getByLabelText("more")
20+
fireEvent.click(moreButton)
21+
constmenu=screen.getByRole("menu")
22+
constsuspendButton=within(menu).getByText(UsersTableLanguage.suspendMenuItem)
23+
fireEvent.click(suspendButton)
24+
25+
// Check if the confirm message is displayed
26+
constconfirmDialog=screen.getByRole("dialog")
27+
expect(confirmDialog).toHaveTextContent(`${UsersPageLanguage.suspendDialogMessagePrefix}${MockUser.username}?`)
28+
29+
// Setup spies to check the actions after
30+
setupActionSpies()
31+
32+
// Click on the "Confirm" button
33+
constconfirmButton=within(confirmDialog).getByText(UsersPageLanguage.suspendDialogAction)
34+
fireEvent.click(confirmButton)
35+
}
536

637
describe("Users Page",()=>{
738
it("shows users",async()=>{
839
render(<UsersPage/>)
940
constusers=awaitscreen.findAllByText(/.*@coder.com/)
1041
expect(users.length).toEqual(2)
1142
})
43+
44+
describe("suspend user",()=>{
45+
describe("when it is success",()=>{
46+
it("shows a success message and refresh the page",async()=>{
47+
render(
48+
<>
49+
<UsersPage/>
50+
<GlobalSnackbar/>
51+
</>,
52+
)
53+
54+
awaitsuspendUser(()=>{
55+
jest.spyOn(API,"suspendUser").mockResolvedValueOnce(MockUser)
56+
jest.spyOn(API,"getUsers").mockImplementationOnce(()=>Promise.resolve([MockUser,MockUser2]))
57+
})
58+
59+
// Check if the success message is displayed
60+
awaitscreen.findByText(usersXServiceLanguage.suspendUserSuccess)
61+
62+
// Check if the API was called correctly
63+
expect(API.suspendUser).toBeCalledTimes(1)
64+
expect(API.suspendUser).toBeCalledWith(MockUser.id)
65+
66+
// Check if the users list was reload
67+
awaitwaitFor(()=>expect(API.getUsers).toBeCalledTimes(1))
68+
})
69+
})
70+
71+
describe("when it fails",()=>{
72+
it("shows an error message",async()=>{
73+
render(
74+
<>
75+
<UsersPage/>
76+
<GlobalSnackbar/>
77+
</>,
78+
)
79+
80+
awaitsuspendUser(()=>{
81+
jest.spyOn(API,"suspendUser").mockRejectedValueOnce({})
82+
})
83+
84+
// Check if the success message is displayed
85+
awaitscreen.findByText(usersXServiceLanguage.suspendUserError)
86+
87+
// Check if the API was called correctly
88+
expect(API.suspendUser).toBeCalledTimes(1)
89+
expect(API.suspendUser).toBeCalledWith(MockUser.id)
90+
})
91+
})
92+
})
1293
})
Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
11
import{useActor}from"@xstate/react"
22
importReact,{useContext,useEffect}from"react"
33
import{useNavigate}from"react-router"
4-
import{ErrorSummary}from"../../components/ErrorSummary/ErrorSummary"
4+
import{ConfirmDialog}from"../../components/ConfirmDialog/ConfirmDialog"
55
import{FullScreenLoader}from"../../components/Loader/FullScreenLoader"
66
import{XServiceContext}from"../../xServices/StateContext"
77
import{UsersPageView}from"./UsersPageView"
88

9+
exportconstLanguage={
10+
suspendDialogTitle:"Suspend user",
11+
suspendDialogAction:"Suspend",
12+
suspendDialogMessagePrefix:"Do you want to suspend the user",
13+
}
14+
915
exportconstUsersPage:React.FC=()=>{
1016
constxServices=useContext(XServiceContext)
1117
const[usersState,usersSend]=useActor(xServices.usersXService)
12-
const{ users, getUsersError}=usersState.context
18+
const{ users, getUsersError, userIdToSuspend}=usersState.context
1319
constnavigate=useNavigate()
20+
constuserToBeSuspended=users?.find((u)=>u.id===userIdToSuspend)
1421

1522
/**
1623
* Fetch users on component mount
@@ -19,20 +26,42 @@ export const UsersPage: React.FC = () => {
1926
usersSend("GET_USERS")
2027
},[usersSend])
2128

22-
if(usersState.matches("error")){
23-
return<ErrorSummaryerror={getUsersError}/>
24-
}
25-
2629
if(!users){
2730
return<FullScreenLoader/>
2831
}else{
2932
return(
30-
<UsersPageView
31-
users={users}
32-
openUserCreationDialog={()=>{
33-
navigate("/users/create")
34-
}}
35-
/>
33+
<>
34+
<UsersPageView
35+
users={users}
36+
openUserCreationDialog={()=>{
37+
navigate("/users/create")
38+
}}
39+
onSuspendUser={(user)=>{
40+
usersSend({type:"SUSPEND_USER",userId:user.id})
41+
}}
42+
error={getUsersError}
43+
/>
44+
45+
<ConfirmDialog
46+
type="delete"
47+
hideCancel={false}
48+
open={usersState.matches("confirmUserSuspension")}
49+
confirmLoading={usersState.matches("suspendingUser")}
50+
title={Language.suspendDialogTitle}
51+
confirmText={Language.suspendDialogAction}
52+
onConfirm={()=>{
53+
usersSend("CONFIRM_USER_SUSPENSION")
54+
}}
55+
onClose={()=>{
56+
usersSend("CANCEL_USER_SUSPENSION")
57+
}}
58+
description={
59+
<>
60+
{Language.suspendDialogMessagePrefix}<strong>{userToBeSuspended?.username}</strong>?
61+
</>
62+
}
63+
/>
64+
</>
3665
)
3766
}
3867
}

‎site/src/pages/UsersPage/UsersPageView.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
importReactfrom"react"
22
import{UserResponse}from"../../api/types"
3+
import{ErrorSummary}from"../../components/ErrorSummary/ErrorSummary"
34
import{Header}from"../../components/Header/Header"
45
import{Margins}from"../../components/Margins/Margins"
56
import{Stack}from"../../components/Stack/Stack"
@@ -13,14 +14,21 @@ export const Language = {
1314
exportinterfaceUsersPageViewProps{
1415
users:UserResponse[]
1516
openUserCreationDialog:()=>void
17+
onSuspendUser:(user:UserResponse)=>void
18+
error?:unknown
1619
}
1720

18-
exportconstUsersPageView:React.FC<UsersPageViewProps>=({ users, openUserCreationDialog})=>{
21+
exportconstUsersPageView:React.FC<UsersPageViewProps>=({
22+
users,
23+
openUserCreationDialog,
24+
onSuspendUser,
25+
error,
26+
})=>{
1927
return(
2028
<Stackspacing={4}>
2129
<Headertitle={Language.pageTitle}action={{text:Language.newUserButton,onClick:openUserCreationDialog}}/>
2230
<Margins>
23-
<UsersTableusers={users}/>
31+
{error ?<ErrorSummaryerror={error}/> :<UsersTableusers={users}onSuspendUser={onSuspendUser}/>}
2432
</Margins>
2533
</Stack>
2634
)

‎site/src/testHelpers/index.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,11 @@ export function renderWithAuth(ui: JSX.Element, { route = "/" }: { route?: strin
3030
constrenderResult=wrappedRender(
3131
<MemoryRouterinitialEntries={[route]}>
3232
<XServiceProvider>
33-
<Routes>
34-
<Routepath={route}element={<RequireAuth>{ui}</RequireAuth>}/>
35-
</Routes>
33+
<ThemeProvidertheme={dark}>
34+
<Routes>
35+
<Routepath={route}element={<RequireAuth>{ui}</RequireAuth>}/>
36+
</Routes>
37+
</ThemeProvider>
3638
</XServiceProvider>
3739
</MemoryRouter>,
3840
)

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp