- Notifications
You must be signed in to change notification settings - Fork928
feat: Add suspend user action#1275
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Uh oh!
There was an error while loading.Please reload this page.
Changes from6 commits
c2f6e1c
1f113e8
fa5eae5
ab5f160
0067770
286fde6
5a43eac
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Uh oh!
There was an error while loading.Please reload this page.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -58,6 +58,7 @@ | ||
"@storybook/addon-essentials": "6.4.22", | ||
"@storybook/addon-links": "6.4.22", | ||
"@storybook/react": "6.4.22", | ||
"@testing-library/jest-dom": "5.16.4", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. This adds some helpful test matchers! | ||
"@testing-library/react": "12.1.5", | ||
"@testing-library/user-event": "14.1.1", | ||
"@types/express": "4.17.13", | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -76,7 +76,7 @@ export const getApiKey = async (): Promise<Types.APIKeyResponse> => { | ||
} | ||
export const getUsers = async (): Promise<TypesGen.User[]> => { | ||
const response = await axios.get<TypesGen.User[]>("/api/v2/users?status=active") | ||
BrunoQuaresma marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
return response.data | ||
} | ||
@@ -135,3 +135,8 @@ export const updateProfile = async (userId: string, data: Types.UpdateProfileReq | ||
const response = await axios.put(`/api/v2/users/${userId}/profile`, data) | ||
return response.data | ||
} | ||
export const suspendUser = async (userId: TypesGen.User["id"]): Promise<TypesGen.User> => { | ||
const response = await axios.put<TypesGen.User>(`/api/v2/users/${userId}/suspend`) | ||
return response.data | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,93 @@ | ||
import {fireEvent,screen, waitFor, within } from "@testing-library/react" | ||
import React from "react" | ||
import * as API from "../../api" | ||
import { GlobalSnackbar } from "../../components/GlobalSnackbar/GlobalSnackbar" | ||
import { Language as UsersTableLanguage } from "../../components/UsersTable/UsersTable" | ||
import { MockUser, MockUser2, render } from "../../testHelpers" | ||
import { Language as usersXServiceLanguage } from "../../xServices/users/usersXService" | ||
import { Language as UsersPageLanguage, UsersPage } from "./UsersPage" | ||
const suspendUser = async (setupActionSpies: () => void) => { | ||
// Get the first user in the table | ||
const users = await screen.findAllByText(/.*@coder.com/) | ||
const firstUserRow = users[0].closest("tr") | ||
if (!firstUserRow) { | ||
throw new Error("Error on get the first user row") | ||
} | ||
// Click on the "more" button to display the "Suspend" option | ||
const moreButton = within(firstUserRow).getByLabelText("more") | ||
fireEvent.click(moreButton) | ||
const menu = screen.getByRole("menu") | ||
const suspendButton = within(menu).getByText(UsersTableLanguage.suspendMenuItem) | ||
fireEvent.click(suspendButton) | ||
// Check if the confirm message is displayed | ||
const confirmDialog = screen.getByRole("dialog") | ||
expect(confirmDialog).toHaveTextContent(`${UsersPageLanguage.suspendDialogMessagePrefix} ${MockUser.username}?`) | ||
// Setup spies to check the actions after | ||
setupActionSpies() | ||
// Click on the "Confirm" button | ||
const confirmButton = within(confirmDialog).getByText(UsersPageLanguage.suspendDialogAction) | ||
fireEvent.click(confirmButton) | ||
} | ||
describe("Users Page", () => { | ||
it("shows users", async () => { | ||
render(<UsersPage />) | ||
const users = await screen.findAllByText(/.*@coder.com/) | ||
expect(users.length).toEqual(2) | ||
}) | ||
describe("suspend user", () => { | ||
describe("when it is success", () => { | ||
it("shows a success message and refresh the page", async () => { | ||
render( | ||
<> | ||
<UsersPage /> | ||
<GlobalSnackbar /> | ||
</>, | ||
) | ||
await suspendUser(() => { | ||
jest.spyOn(API, "suspendUser").mockResolvedValueOnce(MockUser) | ||
jest.spyOn(API, "getUsers").mockImplementationOnce(() => Promise.resolve([MockUser, MockUser2])) | ||
}) | ||
// Check if the success message is displayed | ||
await screen.findByText(usersXServiceLanguage.suspendUserSuccess) | ||
// Check if the API was called correctly | ||
expect(API.suspendUser).toBeCalledTimes(1) | ||
expect(API.suspendUser).toBeCalledWith(MockUser.id) | ||
// Check if the users list was reload | ||
await waitFor(() => expect(API.getUsers).toBeCalledTimes(1)) | ||
}) | ||
}) | ||
describe("when it fails", () => { | ||
it("shows an error message", async () => { | ||
render( | ||
<> | ||
<UsersPage /> | ||
<GlobalSnackbar /> | ||
</>, | ||
) | ||
await suspendUser(() => { | ||
jest.spyOn(API, "suspendUser").mockRejectedValueOnce({}) | ||
}) | ||
// Check if the success message is displayed | ||
await screen.findByText(usersXServiceLanguage.suspendUserError) | ||
// Check if the API was called correctly | ||
expect(API.suspendUser).toBeCalledTimes(1) | ||
expect(API.suspendUser).toBeCalledWith(MockUser.id) | ||
}) | ||
}) | ||
}) | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,16 +1,23 @@ | ||
import { useActor } from "@xstate/react" | ||
import React, { useContext, useEffect } from "react" | ||
import { useNavigate } from "react-router" | ||
import {ConfirmDialog } from "../../components/ConfirmDialog/ConfirmDialog" | ||
import { FullScreenLoader } from "../../components/Loader/FullScreenLoader" | ||
import { XServiceContext } from "../../xServices/StateContext" | ||
import { UsersPageView } from "./UsersPageView" | ||
export const Language = { | ||
suspendDialogTitle: "Suspend user", | ||
suspendDialogAction: "Suspend", | ||
suspendDialogMessagePrefix: "Do you want to suspend the user", | ||
} | ||
export const UsersPage: React.FC = () => { | ||
const xServices = useContext(XServiceContext) | ||
const [usersState, usersSend] = useActor(xServices.usersXService) | ||
const { users, getUsersError, userIdToSuspend } = usersState.context | ||
const navigate = useNavigate() | ||
const userToBeSuspended = users?.find((u) => u.id === userIdToSuspend) | ||
/** | ||
* Fetch users on component mount | ||
@@ -19,20 +26,42 @@ export const UsersPage: React.FC = () => { | ||
usersSend("GET_USERS") | ||
}, [usersSend]) | ||
CollaboratorAuthor
| ||
if (!users) { | ||
return <FullScreenLoader /> | ||
} else { | ||
return ( | ||
<> | ||
<UsersPageView | ||
users={users} | ||
openUserCreationDialog={() => { | ||
navigate("/users/create") | ||
}} | ||
onSuspendUser={(user) => { | ||
usersSend({ type: "SUSPEND_USER", userId: user.id }) | ||
}} | ||
error={getUsersError} | ||
/> | ||
<ConfirmDialog | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. This is so clean, love it | ||
type="delete" | ||
hideCancel={false} | ||
open={usersState.matches("confirmUserSuspension")} | ||
confirmLoading={usersState.matches("suspendingUser")} | ||
title={Language.suspendDialogTitle} | ||
confirmText={Language.suspendDialogAction} | ||
onConfirm={() => { | ||
usersSend("CONFIRM_USER_SUSPENSION") | ||
}} | ||
onClose={() => { | ||
usersSend("CANCEL_USER_SUSPENSION") | ||
}} | ||
description={ | ||
<> | ||
{Language.suspendDialogMessagePrefix} <strong>{userToBeSuspended?.username}</strong>? | ||
</> | ||
} | ||
/> | ||
</> | ||
) | ||
} | ||
} |
Uh oh!
There was an error while loading.Please reload this page.