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

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

Merged
BrunoQuaresma merged 7 commits intomainfrombq/738/suspend-user
May 4, 2022
Merged
Show file tree
Hide file tree
Changes from6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletionssite/jest.setup.ts
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
import "@testing-library/jest-dom"
import { server } from "./src/testHelpers/server"

// Establish API mocking before all tests through MSW.
Expand Down
23,318 changes: 23,318 additions & 0 deletionssite/package-lock.json
View file
Open in desktop

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletionssite/package.json
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -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",
Copy link
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

This adds some helpful test matchers!

presleyp reacted with thumbs up emoji
"@testing-library/react": "12.1.5",
"@testing-library/user-event": "14.1.1",
"@types/express": "4.17.13",
Expand Down
7 changes: 6 additions & 1 deletionsite/src/api/index.ts
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -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?offset=0&limit=1000")
const response = await axios.get<TypesGen.User[]>("/api/v2/users?status=active")
return response.data
}

Expand DownExpand Up@@ -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
}
23 changes: 22 additions & 1 deletionsite/src/components/GlobalSnackbar/utils.test.ts
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
import { displaySuccess, isNotificationTextPrefixed, MsgType, NotificationMsg } from "./utils"
import {
displayError,
displaySuccess,
isNotificationTextPrefixed,
MsgType,
NotificationMsg,
SnackbarEventType,
} from "./utils"

describe("Snackbar", () => {
describe("isNotificationTextPrefixed", () => {
Expand DownExpand Up@@ -76,4 +83,18 @@ describe("Snackbar", () => {
expect(extractNotificationEvent(dispatchEventMock)).toStrictEqual(expected)
})
})

describe("displayError", () => {
it("shows the title and the message", (done) => {
const message = "Some error happened"

window.addEventListener(SnackbarEventType, (event) => {
const notificationEvent = event as CustomEvent<NotificationMsg>
expect(notificationEvent.detail.msg).toEqual(message)
done()
})

displayError(message)
})
})
})
4 changes: 4 additions & 0 deletionssite/src/components/GlobalSnackbar/utils.ts
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -60,3 +60,7 @@ export const displayMsg = (msg: string, additionalMsg?: string): void => {
export const displaySuccess = (msg: string, additionalMsg?: string): void => {
dispatchNotificationEvent(MsgType.Success, msg, additionalMsg ? [additionalMsg] : undefined)
}

export const displayError = (msg: string, additionalMsg?: string): void => {
dispatchNotificationEvent(MsgType.Error, msg, additionalMsg ? [additionalMsg] : undefined)
}
9 changes: 4 additions & 5 deletionssite/src/components/UsersTable/UsersTable.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,7 @@ import { Column, Table } from "../Table/Table"
import { TableRowMenu } from "../TableRowMenu/TableRowMenu"
import { UserCell } from "../UserCell/UserCell"

const Language = {
exportconst Language = {
pageTitle: "Users",
usersTitle: "All users",
emptyMessage: "No users found",
Expand All@@ -27,9 +27,10 @@ const columns: Column<UserResponse>[] = [

export interface UsersTableProps {
users: UserResponse[]
onSuspendUser: (user: UserResponse) => void
}

export const UsersTable: React.FC<UsersTableProps> = ({ users }) => {
export const UsersTable: React.FC<UsersTableProps> = ({ users, onSuspendUser }) => {
return (
<Table
columns={columns}
Expand All@@ -42,9 +43,7 @@ export const UsersTable: React.FC<UsersTableProps> = ({ users }) => {
menuItems={[
{
label: Language.suspendMenuItem,
onClick: () => {
// TO-DO: Add suspend action here
},
onClick: onSuspendUser,
},
]}
/>
Expand Down
87 changes: 84 additions & 3 deletionssite/src/pages/UsersPage/UsersPage.test.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,93 @@
import { screen } from "@testing-library/react"
import {fireEvent,screen, waitFor, within } from "@testing-library/react"
import React from "react"
import { render } from "../../testHelpers"
import { UsersPage } from "./UsersPage"
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)
})
})
})
})
53 changes: 41 additions & 12 deletionssite/src/pages/UsersPage/UsersPage.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
import { useActor } from "@xstate/react"
import React, { useContext, useEffect } from "react"
import { useNavigate } from "react-router"
import {ErrorSummary } from "../../components/ErrorSummary/ErrorSummary"
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 } = usersState.context
const { users, getUsersError, userIdToSuspend } = usersState.context
const navigate = useNavigate()
const userToBeSuspended = users?.find((u) => u.id === userIdToSuspend)

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

if (usersState.matches("error")) {
return <ErrorSummary error={getUsersError} />
Copy link
CollaboratorAuthor

@BrunoQuaresmaBrunoQuaresmaMay 3, 2022
edited
Loading

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

I moved this to be inside of UsersPage, so the getUsersError does not block the UI(not showing the table) for the other non-blocking errors like the suspendUserError.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

My idea for that was to not go to theerror state for non-blocking errors. This may work fine too.

Copy link
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Sorry, I didn't understand. Without removing this statement, any error will remove the user table from the screen and show the error summary.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

I'm suggesting that every error gets assigned to context so that we can display error messages, but that only blocking errors put the page in theerror finite state. I see that state as meaning "the page is unusable."

BrunoQuaresma reacted with thumbs up emoji
Copy link
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Ahh I see it, good to know!

}

if (!users) {
return <FullScreenLoader />
} else {
return (
<UsersPageView
users={users}
openUserCreationDialog={() => {
navigate("/users/create")
}}
/>
<>
<UsersPageView
users={users}
openUserCreationDialog={() => {
navigate("/users/create")
}}
onSuspendUser={(user) => {
usersSend({ type: "SUSPEND_USER", userId: user.id })
}}
error={getUsersError}
/>

<ConfirmDialog
Copy link
Contributor

Choose a reason for hiding this comment

The 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>?
</>
}
/>
</>
)
}
}
12 changes: 10 additions & 2 deletionssite/src/pages/UsersPage/UsersPageView.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,7 @@ import Paper from "@material-ui/core/Paper"
import { makeStyles } from "@material-ui/core/styles"
import React from "react"
import { UserResponse } from "../../api/types"
import { ErrorSummary } from "../../components/ErrorSummary/ErrorSummary"
import { Header } from "../../components/Header/Header"
import { UsersTable } from "../../components/UsersTable/UsersTable"

Expand All@@ -13,16 +14,23 @@ export const Language = {
export interface UsersPageViewProps {
users: UserResponse[]
openUserCreationDialog: () => void
onSuspendUser: (user: UserResponse) => void
error?: unknown
}

export const UsersPageView: React.FC<UsersPageViewProps> = ({ users, openUserCreationDialog }) => {
export const UsersPageView: React.FC<UsersPageViewProps> = ({
users,
openUserCreationDialog,
onSuspendUser,
error,
}) => {
const styles = useStyles()

return (
<div className={styles.flexColumn}>
<Header title={Language.pageTitle} action={{ text: Language.newUserButton, onClick: openUserCreationDialog }} />
<Paper style={{ maxWidth: "1380px", margin: "1em auto", width: "100%" }}>
<UsersTable users={users}/>
{error ? <ErrorSummary error={error} /> : <UsersTable users={users}onSuspendUser={onSuspendUser} />}
</Paper>
</div>
)
Expand Down
8 changes: 5 additions & 3 deletionssite/src/testHelpers/index.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -30,9 +30,11 @@ export function renderWithAuth(ui: JSX.Element, { route = "/" }: { route?: strin
const renderResult = wrappedRender(
<MemoryRouter initialEntries={[route]}>
<XServiceProvider>
<Routes>
<Route path={route} element={<RequireAuth>{ui}</RequireAuth>} />
</Routes>
<ThemeProvider theme={dark}>
<Routes>
<Route path={route} element={<RequireAuth>{ui}</RequireAuth>} />
</Routes>
</ThemeProvider>
</XServiceProvider>
</MemoryRouter>,
)
Expand Down
Loading

[8]ページ先頭

©2009-2025 Movatter.jp