Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Add button and route * Hook up api * Lint * Add basic form * Get users on page mount * Make cancel work * Creating -> idle bc users page refetches * Import as TypesGen * Handle api errors * Lint * Add handler * Add FormFooter * Add FullPageForm * Lint * Better form, error, stories bug in formErrors story * Make detail optional * Use Language * Remove detail prop * Add back autoFocus * Remove displayError, use displaySuccess * Lint, export Language * Tests - wip * Fix cancel tests * Switch back to mock * Add navigate to xservice Doesn't work in test * Move error type predicate to xservice * Lint * Switch to using creation mode in XState still problems in tests * Lint * Lint * Lint * Revert "Switch to using creation mode in XState" This reverts commit cf8442f. * Give XService a navigate action * Add missing validation messages * Fix XState warning * Fix tests IRL is broken bc I need to send org id * Pretend user has org id and make it work * Format * Lint * Switch to org ids array * Skip lines between tests Co-authored-by: G r e y <grey@coder.com> * Punctuate notification messages Co-authored-by: G r e y <grey@coder.com>
- Loading branch information
1 parent
4efde58
commit c16f105
Showing
17 changed files
with
412 additions
and
23 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
43 changes: 43 additions & 0 deletions
43
site/src/components/CreateUserForm/CreateUserForm.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import { action } from "@storybook/addon-actions" | ||
import { Story } from "@storybook/react" | ||
import React from "react" | ||
import { CreateUserForm, CreateUserFormProps } from "./CreateUserForm" | ||
|
||
export default { | ||
title: "components/CreateUserForm", | ||
component: CreateUserForm, | ||
} | ||
|
||
const Template: Story<CreateUserFormProps> = (args: CreateUserFormProps) => <CreateUserForm {...args} /> | ||
|
||
export const Ready = Template.bind({}) | ||
Ready.args = { | ||
onCancel: action("cancel"), | ||
onSubmit: action("submit"), | ||
isLoading: false, | ||
} | ||
|
||
export const UnknownError = Template.bind({}) | ||
UnknownError.args = { | ||
onCancel: action("cancel"), | ||
onSubmit: action("submit"), | ||
isLoading: false, | ||
error: "Something went wrong", | ||
} | ||
|
||
export const FormError = Template.bind({}) | ||
FormError.args = { | ||
onCancel: action("cancel"), | ||
onSubmit: action("submit"), | ||
isLoading: false, | ||
formErrors: { | ||
username: "Username taken", | ||
}, | ||
} | ||
|
||
export const Loading = Template.bind({}) | ||
Loading.args = { | ||
onCancel: action("cancel"), | ||
onSubmit: action("submit"), | ||
isLoading: true, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
import FormHelperText from "@material-ui/core/FormHelperText" | ||
import TextField from "@material-ui/core/TextField" | ||
import { FormikContextType, FormikErrors, useFormik } from "formik" | ||
import React from "react" | ||
import * as Yup from "yup" | ||
import { CreateUserRequest } from "../../api/types" | ||
import { getFormHelpers, onChangeTrimmed } from "../../util/formUtils" | ||
import { FormFooter } from "../FormFooter/FormFooter" | ||
import { FullPageForm } from "../FullPageForm/FullPageForm" | ||
|
||
export const Language = { | ||
emailLabel: "Email", | ||
passwordLabel: "Password", | ||
usernameLabel: "Username", | ||
emailInvalid: "Please enter a valid email address.", | ||
emailRequired: "Please enter an email address.", | ||
passwordRequired: "Please enter a password.", | ||
usernameRequired: "Please enter a username.", | ||
createUser: "Create", | ||
cancel: "Cancel", | ||
} | ||
|
||
export interface CreateUserFormProps { | ||
onSubmit: (user: CreateUserRequest) => void | ||
onCancel: () => void | ||
formErrors?: FormikErrors<CreateUserRequest> | ||
isLoading: boolean | ||
error?: string | ||
myOrgId: string | ||
} | ||
|
||
const validationSchema = Yup.object({ | ||
email: Yup.string().trim().email(Language.emailInvalid).required(Language.emailRequired), | ||
password: Yup.string().required(Language.passwordRequired), | ||
username: Yup.string().required(Language.usernameRequired), | ||
}) | ||
|
||
export const CreateUserForm: React.FC<CreateUserFormProps> = ({ | ||
onSubmit, | ||
onCancel, | ||
formErrors, | ||
isLoading, | ||
error, | ||
myOrgId, | ||
}) => { | ||
const form: FormikContextType<CreateUserRequest> = useFormik<CreateUserRequest>({ | ||
initialValues: { | ||
email: "", | ||
password: "", | ||
username: "", | ||
organization_id: myOrgId, | ||
}, | ||
validationSchema, | ||
onSubmit, | ||
}) | ||
const getFieldHelpers = getFormHelpers<CreateUserRequest>(form, formErrors) | ||
|
||
return ( | ||
<FullPageForm title="Create user" onCancel={onCancel}> | ||
<form onSubmit={form.handleSubmit}> | ||
<TextField | ||
{...getFieldHelpers("username")} | ||
onChange={onChangeTrimmed(form)} | ||
autoComplete="username" | ||
autoFocus | ||
fullWidth | ||
label={Language.usernameLabel} | ||
variant="outlined" | ||
/> | ||
<TextField | ||
{...getFieldHelpers("email")} | ||
onChange={onChangeTrimmed(form)} | ||
autoComplete="email" | ||
fullWidth | ||
label={Language.emailLabel} | ||
variant="outlined" | ||
/> | ||
<TextField | ||
{...getFieldHelpers("password")} | ||
autoComplete="current-password" | ||
fullWidth | ||
id="password" | ||
label={Language.passwordLabel} | ||
type="password" | ||
variant="outlined" | ||
/> | ||
{error && <FormHelperText error>{error}</FormHelperText>} | ||
<FormFooter onCancel={onCancel} isLoading={isLoading} /> | ||
</form> | ||
</FullPageForm> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
98 changes: 98 additions & 0 deletions
98
site/src/pages/UsersPage/CreateUserPage/CreateUserPage.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
import { screen } from "@testing-library/react" | ||
import userEvent from "@testing-library/user-event" | ||
import { rest } from "msw" | ||
import React from "react" | ||
import * as API from "../../../api" | ||
import { Language as FormLanguage } from "../../../components/CreateUserForm/CreateUserForm" | ||
import { Language as FooterLanguage } from "../../../components/FormFooter/FormFooter" | ||
import { history, render } from "../../../testHelpers" | ||
import { server } from "../../../testHelpers/server" | ||
import { Language as UserLanguage } from "../../../xServices/users/usersXService" | ||
import { CreateUserPage, Language } from "./CreateUserPage" | ||
|
||
const fillForm = async ({ | ||
username = "someuser", | ||
email = "someone@coder.com", | ||
password = "password", | ||
}: { | ||
username?: string | ||
email?: string | ||
password?: string | ||
}) => { | ||
const usernameField = screen.getByLabelText(FormLanguage.usernameLabel) | ||
const emailField = screen.getByLabelText(FormLanguage.emailLabel) | ||
const passwordField = screen.getByLabelText(FormLanguage.passwordLabel) | ||
await userEvent.type(usernameField, username) | ||
await userEvent.type(emailField, email) | ||
await userEvent.type(passwordField, password) | ||
const submitButton = await screen.findByText(FooterLanguage.defaultSubmitLabel) | ||
submitButton.click() | ||
} | ||
|
||
describe("Create User Page", () => { | ||
beforeEach(() => { | ||
history.replace("/users/create") | ||
}) | ||
|
||
it("shows validation error message", async () => { | ||
render(<CreateUserPage />) | ||
await fillForm({ email: "test" }) | ||
const errorMessage = await screen.findByText(FormLanguage.emailInvalid) | ||
expect(errorMessage).toBeDefined() | ||
}) | ||
|
||
it("shows generic error message", async () => { | ||
jest.spyOn(API, "createUser").mockRejectedValueOnce({ | ||
data: "unknown error", | ||
}) | ||
render(<CreateUserPage />) | ||
await fillForm({}) | ||
const errorMessage = await screen.findByText(Language.unknownError) | ||
expect(errorMessage).toBeDefined() | ||
}) | ||
|
||
it("shows API error message", async () => { | ||
const fieldErrorMessage = "username already in use" | ||
server.use( | ||
rest.post("/api/v2/users", async (req, res, ctx) => { | ||
return res( | ||
ctx.status(400), | ||
ctx.json({ | ||
message: "invalid field", | ||
errors: [ | ||
{ | ||
detail: fieldErrorMessage, | ||
field: "username", | ||
}, | ||
], | ||
}), | ||
) | ||
}), | ||
) | ||
render(<CreateUserPage />) | ||
await fillForm({}) | ||
const errorMessage = await screen.findByText(fieldErrorMessage) | ||
expect(errorMessage).toBeDefined() | ||
}) | ||
|
||
it("shows success notification and redirects to users page", async () => { | ||
render(<CreateUserPage />) | ||
await fillForm({}) | ||
const successMessage = screen.findByText(UserLanguage.createUserSuccess) | ||
expect(successMessage).toBeDefined() | ||
}) | ||
|
||
it("redirects to users page on cancel", async () => { | ||
render(<CreateUserPage />) | ||
const cancelButton = await screen.findByText(FooterLanguage.cancelLabel) | ||
cancelButton.click() | ||
expect(history.location.pathname).toEqual("/users") | ||
}) | ||
|
||
it("redirects to users page on close", async () => { | ||
render(<CreateUserPage />) | ||
const closeButton = await screen.findByText("ESC") | ||
closeButton.click() | ||
expect(history.location.pathname).toEqual("/users") | ||
}) | ||
}) |
33 changes: 33 additions & 0 deletions
33
site/src/pages/UsersPage/CreateUserPage/CreateUserPage.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import { useActor, useSelector } from "@xstate/react" | ||
import React, { useContext } from "react" | ||
import { useNavigate } from "react-router" | ||
import { CreateUserRequest } from "../../../api/types" | ||
import { CreateUserForm } from "../../../components/CreateUserForm/CreateUserForm" | ||
import { selectOrgId } from "../../../xServices/auth/authSelectors" | ||
import { XServiceContext } from "../../../xServices/StateContext" | ||
|
||
export const Language = { | ||
unknownError: "Oops, an unknown error occurred.", | ||
} | ||
|
||
export const CreateUserPage: React.FC = () => { | ||
const xServices = useContext(XServiceContext) | ||
const myOrgId = useSelector(xServices.authXService, selectOrgId) | ||
const [usersState, usersSend] = useActor(xServices.usersXService) | ||
const { createUserError, createUserFormErrors } = usersState.context | ||
const navigate = useNavigate() | ||
// There is no field for organization id in Community Edition, so handle its field error like a generic error | ||
const genericError = | ||
createUserError || createUserFormErrors?.organization_id || !myOrgId ? Language.unknownError : undefined | ||
|
||
return ( | ||
<CreateUserForm | ||
formErrors={createUserFormErrors} | ||
onSubmit={(user: CreateUserRequest) => usersSend({ type: "CREATE", user })} | ||
onCancel={() => navigate("/users")} | ||
isLoading={usersState.hasTag("loading")} | ||
error={genericError} | ||
myOrgId={myOrgId ?? ""} | ||
/> | ||
) | ||
} |
Oops, something went wrong.