From 5899c4467da367e60655ae13ab913bb74fc30a77 Mon Sep 17 00:00:00 2001 From: Abhineet Jain Date: Mon, 25 Jul 2022 20:47:20 +0000 Subject: [PATCH 1/5] fix: manage backend authXService errors --- .../components/ErrorSummary/ErrorSummary.tsx | 3 +- .../SettingsAccountForm.tsx | 15 ++-- .../SettingsSecurityForm.tsx | 15 ++-- site/src/components/SignInForm/SignInForm.tsx | 88 +++++++++---------- site/src/pages/LoginPage/LoginPage.test.tsx | 5 +- site/src/pages/LoginPage/LoginPage.tsx | 11 +-- .../AccountPage/AccountPage.test.tsx | 5 +- .../AccountPage/AccountPage.tsx | 11 +-- .../SecurityPage/SecurityPage.test.tsx | 11 ++- .../SecurityPage/SecurityPage.tsx | 11 +-- site/src/util/formUtils.ts | 12 +++ site/src/xServices/auth/authXService.ts | 10 +-- 12 files changed, 87 insertions(+), 110 deletions(-) diff --git a/site/src/components/ErrorSummary/ErrorSummary.tsx b/site/src/components/ErrorSummary/ErrorSummary.tsx index 77bea7b056363..ceca96f7d72df 100644 --- a/site/src/components/ErrorSummary/ErrorSummary.tsx +++ b/site/src/components/ErrorSummary/ErrorSummary.tsx @@ -9,7 +9,7 @@ import { ApiError, getErrorDetail, getErrorMessage } from "api/errors" import { Stack } from "components/Stack/Stack" import { FC, useState } from "react" -const Language = { +export const Language = { retryMessage: "Retry", unknownErrorMessage: "An unknown error has occurred", moreDetails: "More", @@ -91,7 +91,6 @@ interface StyleProps { const useStyles = makeStyles((theme) => ({ root: { background: darken(theme.palette.error.main, 0.6), - margin: `${theme.spacing(2)}px`, padding: `${theme.spacing(2)}px`, borderRadius: theme.shape.borderRadius, gap: 0, diff --git a/site/src/components/SettingsAccountForm/SettingsAccountForm.tsx b/site/src/components/SettingsAccountForm/SettingsAccountForm.tsx index 6ac89d53a03ce..17f5f70f15446 100644 --- a/site/src/components/SettingsAccountForm/SettingsAccountForm.tsx +++ b/site/src/components/SettingsAccountForm/SettingsAccountForm.tsx @@ -1,9 +1,9 @@ -import FormHelperText from "@material-ui/core/FormHelperText" import TextField from "@material-ui/core/TextField" +import { ErrorSummary } from "components/ErrorSummary/ErrorSummary" import { FormikContextType, FormikErrors, useFormik } from "formik" import { FC } from "react" import * as Yup from "yup" -import { getFormHelpers, nameValidator, onChangeTrimmed } from "../../util/formUtils" +import { getFormHelpersWithError, nameValidator, onChangeTrimmed } from "../../util/formUtils" import { LoadingButton } from "../LoadingButton/LoadingButton" import { Stack } from "../Stack/Stack" @@ -28,8 +28,7 @@ export interface AccountFormProps { isLoading: boolean initialValues: AccountFormValues onSubmit: (values: AccountFormValues) => void - formErrors?: AccountFormErrors - error?: string + updateProfileError?: Error | unknown } export const AccountForm: FC = ({ @@ -37,20 +36,20 @@ export const AccountForm: FC = ({ isLoading, onSubmit, initialValues, - formErrors = {}, - error, + updateProfileError, }) => { const form: FormikContextType = useFormik({ initialValues, validationSchema, onSubmit, }) - const getFieldHelpers = getFormHelpers(form, formErrors) + const getFieldHelpers = getFormHelpersWithError(form, updateProfileError) return ( <>
+ {updateProfileError && } = ({ variant="outlined" /> - {error && {error}} -
{isLoading ? "" : Language.updateSettings} diff --git a/site/src/components/SettingsSecurityForm/SettingsSecurityForm.tsx b/site/src/components/SettingsSecurityForm/SettingsSecurityForm.tsx index d10726227f427..cbeedcaffc42e 100644 --- a/site/src/components/SettingsSecurityForm/SettingsSecurityForm.tsx +++ b/site/src/components/SettingsSecurityForm/SettingsSecurityForm.tsx @@ -1,9 +1,9 @@ -import FormHelperText from "@material-ui/core/FormHelperText" import TextField from "@material-ui/core/TextField" +import { ErrorSummary } from "components/ErrorSummary/ErrorSummary" import { FormikContextType, FormikErrors, useFormik } from "formik" import React from "react" import * as Yup from "yup" -import { getFormHelpers, onChangeTrimmed } from "../../util/formUtils" +import { getFormHelpersWithError, onChangeTrimmed } from "../../util/formUtils" import { LoadingButton } from "../LoadingButton/LoadingButton" import { Stack } from "../Stack/Stack" @@ -45,28 +45,27 @@ export interface SecurityFormProps { isLoading: boolean initialValues: SecurityFormValues onSubmit: (values: SecurityFormValues) => void - formErrors?: SecurityFormErrors - error?: string + updateSecurityError?: Error | unknown } export const SecurityForm: React.FC = ({ isLoading, onSubmit, initialValues, - formErrors = {}, - error, + updateSecurityError, }) => { const form: FormikContextType = useFormik({ initialValues, validationSchema, onSubmit, }) - const getFieldHelpers = getFormHelpers(form, formErrors) + const getFieldHelpers = getFormHelpersWithError(form, updateSecurityError) return ( <> + {updateSecurityError && } = ({ type="password" /> - {error && {error}} -
{isLoading ? "" : Language.updatePassword} diff --git a/site/src/components/SignInForm/SignInForm.tsx b/site/src/components/SignInForm/SignInForm.tsx index a3184572fc0d7..a20ad47080ecf 100644 --- a/site/src/components/SignInForm/SignInForm.tsx +++ b/site/src/components/SignInForm/SignInForm.tsx @@ -1,14 +1,15 @@ import Button from "@material-ui/core/Button" -import FormHelperText from "@material-ui/core/FormHelperText" import Link from "@material-ui/core/Link" import { makeStyles } from "@material-ui/core/styles" import TextField from "@material-ui/core/TextField" import GitHubIcon from "@material-ui/icons/GitHub" +import { ErrorSummary } from "components/ErrorSummary/ErrorSummary" +import { Stack } from "components/Stack/Stack" import { FormikContextType, useFormik } from "formik" import { FC } from "react" import * as Yup from "yup" import { AuthMethods } from "../../api/typesGenerated" -import { getFormHelpers, onChangeTrimmed } from "../../util/formUtils" +import { getFormHelpersWithError, onChangeTrimmed } from "../../util/formUtils" import { Welcome } from "../Welcome/Welcome" import { LoadingButton } from "./../LoadingButton/LoadingButton" @@ -39,17 +40,6 @@ const validationSchema = Yup.object({ }) const useStyles = makeStyles((theme) => ({ - loginBtnWrapper: { - marginTop: theme.spacing(6), - borderTop: `1px solid ${theme.palette.action.disabled}`, - paddingTop: theme.spacing(3), - }, - loginTextField: { - marginTop: theme.spacing(2), - }, - submitBtn: { - marginTop: theme.spacing(2), - }, buttonIcon: { width: 14, height: 14, @@ -78,8 +68,8 @@ const useStyles = makeStyles((theme) => ({ export interface SignInFormProps { isLoading: boolean redirectTo: string - authErrorMessage?: string - methodsErrorMessage?: string + authError?: Error | unknown + methodsError?: Error | unknown authMethods?: AuthMethods onSubmit: ({ email, password }: { email: string; password: string }) => Promise } @@ -88,8 +78,8 @@ export const SignInForm: FC = ({ authMethods, redirectTo, isLoading, - authErrorMessage, - methodsErrorMessage, + authError, + methodsError, onSubmit, }) => { const styles = useStyles() @@ -107,42 +97,44 @@ export const SignInForm: FC = ({ validateOnBlur: false, onSubmit, }) - const getFieldHelpers = getFormHelpers(form) + const getFieldHelpers = getFormHelpersWithError(form, authError) return ( <> - - - {authErrorMessage && {authErrorMessage}} - {methodsErrorMessage && ( - {Language.methodsErrorMessage} - )} -
- - {isLoading ? "" : Language.passwordSignIn} - -
+ + {authError && ( + + )} + {methodsError && ( + + )} + + +
+ + {isLoading ? "" : Language.passwordSignIn} + +
+
{authMethods?.github && ( <> diff --git a/site/src/pages/LoginPage/LoginPage.test.tsx b/site/src/pages/LoginPage/LoginPage.test.tsx index cd6242e9dc994..e1090454542fc 100644 --- a/site/src/pages/LoginPage/LoginPage.test.tsx +++ b/site/src/pages/LoginPage/LoginPage.test.tsx @@ -52,10 +52,11 @@ describe("LoginPage", () => { it("shows an error if fetching auth methods fails", async () => { // Given + const apiErrorMessage = "Unable to fetch methods" server.use( // Make login fail rest.get("/api/v2/users/authmethods", async (req, res, ctx) => { - return res(ctx.status(500), ctx.json({ message: "nope" })) + return res(ctx.status(500), ctx.json({ message: apiErrorMessage })) }), ) @@ -63,7 +64,7 @@ describe("LoginPage", () => { render() // Then - const errorMessage = await screen.findByText(Language.methodsErrorMessage) + const errorMessage = await screen.findByText(apiErrorMessage) expect(errorMessage).toBeDefined() }) diff --git a/site/src/pages/LoginPage/LoginPage.tsx b/site/src/pages/LoginPage/LoginPage.tsx index 02acc7311d9ab..9f6e7a0a93247 100644 --- a/site/src/pages/LoginPage/LoginPage.tsx +++ b/site/src/pages/LoginPage/LoginPage.tsx @@ -3,7 +3,6 @@ import { useActor } from "@xstate/react" import React, { useContext } from "react" import { Helmet } from "react-helmet" import { Navigate, useLocation } from "react-router-dom" -import { isApiError } from "../../api/errors" import { Footer } from "../../components/Footer/Footer" import { SignInForm } from "../../components/SignInForm/SignInForm" import { pageTitle } from "../../util/page" @@ -36,12 +35,6 @@ export const LoginPage: React.FC = () => { const [authState, authSend] = useActor(xServices.authXService) const isLoading = authState.hasTag("loading") const redirectTo = retrieveRedirect(location.search) - const authErrorMessage = isApiError(authState.context.authError) - ? authState.context.authError.response.data.message - : undefined - const getMethodsError = authState.context.getMethodsError - ? (authState.context.getMethodsError as Error).message - : undefined const onSubmit = async ({ email, password }: { email: string; password: string }) => { authSend({ type: "SIGN_IN", email, password }) @@ -61,8 +54,8 @@ export const LoginPage: React.FC = () => { authMethods={authState.context.methods} redirectTo={redirectTo} isLoading={isLoading} - authErrorMessage={authErrorMessage} - methodsErrorMessage={getMethodsError} + authError={authState.context.authError} + methodsError={authState.context.getMethodsError as Error} onSubmit={onSubmit} />
diff --git a/site/src/pages/UserSettingsPage/AccountPage/AccountPage.test.tsx b/site/src/pages/UserSettingsPage/AccountPage/AccountPage.test.tsx index 0d203dc005f5c..af67869d4237f 100644 --- a/site/src/pages/UserSettingsPage/AccountPage/AccountPage.test.tsx +++ b/site/src/pages/UserSettingsPage/AccountPage/AccountPage.test.tsx @@ -1,10 +1,11 @@ import { fireEvent, screen, waitFor } from "@testing-library/react" +import { Language as ErrorSummaryLanguage } from "components/ErrorSummary/ErrorSummary" import * as API from "../../../api/api" import { GlobalSnackbar } from "../../../components/GlobalSnackbar/GlobalSnackbar" import * as AccountForm from "../../../components/SettingsAccountForm/SettingsAccountForm" import { renderWithAuth } from "../../../testHelpers/renderHelpers" import * as AuthXService from "../../../xServices/auth/authXService" -import { AccountPage, Language } from "./AccountPage" +import { AccountPage } from "./AccountPage" const renderPage = () => { return renderWithAuth( @@ -80,7 +81,7 @@ describe("AccountPage", () => { const { user } = renderPage() await fillAndSubmitForm() - const errorMessage = await screen.findByText(Language.unknownError) + const errorMessage = await screen.findByText(ErrorSummaryLanguage.unknownErrorMessage) expect(errorMessage).toBeDefined() expect(API.updateProfile).toBeCalledTimes(1) expect(API.updateProfile).toBeCalledWith(user.id, newData) diff --git a/site/src/pages/UserSettingsPage/AccountPage/AccountPage.tsx b/site/src/pages/UserSettingsPage/AccountPage/AccountPage.tsx index 97e2defecb9ac..acc5ef3ddd549 100644 --- a/site/src/pages/UserSettingsPage/AccountPage/AccountPage.tsx +++ b/site/src/pages/UserSettingsPage/AccountPage/AccountPage.tsx @@ -1,25 +1,17 @@ import { useActor } from "@xstate/react" import React, { useContext } from "react" -import { isApiError, mapApiErrorToFieldErrors } from "../../../api/errors" import { Section } from "../../../components/Section/Section" import { AccountForm } from "../../../components/SettingsAccountForm/SettingsAccountForm" import { XServiceContext } from "../../../xServices/StateContext" export const Language = { title: "Account", - unknownError: "Oops, an unknown error occurred.", } export const AccountPage: React.FC = () => { const xServices = useContext(XServiceContext) const [authState, authSend] = useActor(xServices.authXService) const { me, updateProfileError } = authState.context - const hasError = !!updateProfileError - const formErrors = - hasError && isApiError(updateProfileError) - ? mapApiErrorToFieldErrors(updateProfileError.response.data) - : undefined - const hasUnknownError = hasError && !isApiError(updateProfileError) if (!me) { throw new Error("No current user found") @@ -29,8 +21,7 @@ export const AccountPage: React.FC = () => {
{ diff --git a/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.test.tsx b/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.test.tsx index ffc1800d205f7..2ca46a3b6c8e2 100644 --- a/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.test.tsx +++ b/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.test.tsx @@ -1,11 +1,12 @@ import { fireEvent, screen, waitFor } from "@testing-library/react" +import { Language as ErrorSummaryLanguage } from "components/ErrorSummary/ErrorSummary" import React from "react" import * as API from "../../../api/api" import { GlobalSnackbar } from "../../../components/GlobalSnackbar/GlobalSnackbar" import * as SecurityForm from "../../../components/SettingsSecurityForm/SettingsSecurityForm" import { renderWithAuth } from "../../../testHelpers/renderHelpers" import * as AuthXService from "../../../xServices/auth/authXService" -import { Language, SecurityPage } from "./SecurityPage" +import { SecurityPage } from "./SecurityPage" const renderPage = () => { return renderWithAuth( @@ -65,8 +66,9 @@ describe("SecurityPage", () => { const { user } = renderPage() await fillAndSubmitForm() - const errorMessage = await screen.findByText("Incorrect password.") + const errorMessage = await screen.findAllByText("Incorrect password.") expect(errorMessage).toBeDefined() + expect(errorMessage).toHaveLength(2) expect(API.updateUserPassword).toBeCalledTimes(1) expect(API.updateUserPassword).toBeCalledWith(user.id, newData) }) @@ -87,8 +89,9 @@ describe("SecurityPage", () => { const { user } = renderPage() await fillAndSubmitForm() - const errorMessage = await screen.findByText("Invalid password.") + const errorMessage = await screen.findAllByText("Invalid password.") expect(errorMessage).toBeDefined() + expect(errorMessage).toHaveLength(2) expect(API.updateUserPassword).toBeCalledTimes(1) expect(API.updateUserPassword).toBeCalledWith(user.id, newData) }) @@ -103,7 +106,7 @@ describe("SecurityPage", () => { const { user } = renderPage() await fillAndSubmitForm() - const errorMessage = await screen.findByText(Language.unknownError) + const errorMessage = await screen.findByText(ErrorSummaryLanguage.unknownErrorMessage) expect(errorMessage).toBeDefined() expect(API.updateUserPassword).toBeCalledTimes(1) expect(API.updateUserPassword).toBeCalledWith(user.id, newData) diff --git a/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.tsx b/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.tsx index 49157467b51c6..ac9e2ddb1937b 100644 --- a/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.tsx +++ b/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.tsx @@ -1,25 +1,17 @@ import { useActor } from "@xstate/react" import React, { useContext } from "react" -import { isApiError, mapApiErrorToFieldErrors } from "../../../api/errors" import { Section } from "../../../components/Section/Section" import { SecurityForm } from "../../../components/SettingsSecurityForm/SettingsSecurityForm" import { XServiceContext } from "../../../xServices/StateContext" export const Language = { title: "Security", - unknownError: "Oops, an unknown error occurred.", } export const SecurityPage: React.FC = () => { const xServices = useContext(XServiceContext) const [authState, authSend] = useActor(xServices.authXService) const { me, updateSecurityError } = authState.context - const hasError = !!updateSecurityError - const formErrors = - hasError && isApiError(updateSecurityError) - ? mapApiErrorToFieldErrors(updateSecurityError.response.data) - : undefined - const hasUnknownError = hasError && !isApiError(updateSecurityError) if (!me) { throw new Error("No current user found") @@ -28,8 +20,7 @@ export const SecurityPage: React.FC = () => { return (
{ diff --git a/site/src/util/formUtils.ts b/site/src/util/formUtils.ts index 12d4939693d4d..3dc137bf4d969 100644 --- a/site/src/util/formUtils.ts +++ b/site/src/util/formUtils.ts @@ -1,3 +1,4 @@ +import { hasApiFieldErrors, isApiError, mapApiErrorToFieldErrors } from "api/errors" import { FormikContextType, FormikErrors, getIn } from "formik" import { ChangeEvent, ChangeEventHandler, FocusEventHandler, ReactNode } from "react" import * as Yup from "yup" @@ -45,6 +46,17 @@ export const getFormHelpers = } } +export const getFormHelpersWithError = ( + form: FormikContextType, + error?: Error | unknown, +): ((name: keyof T, HelperText?: ReactNode) => FormHelpers) => { + const apiValidationErrors = + isApiError(error) && hasApiFieldErrors(error) + ? (mapApiErrorToFieldErrors(error.response.data) as FormikErrors) + : undefined + return getFormHelpers(form, apiValidationErrors) +} + export const onChangeTrimmed = (form: FormikContextType) => (event: ChangeEvent): void => { diff --git a/site/src/xServices/auth/authXService.ts b/site/src/xServices/auth/authXService.ts index 66d05dbfc8885..47433cc01d85c 100644 --- a/site/src/xServices/auth/authXService.ts +++ b/site/src/xServices/auth/authXService.ts @@ -48,7 +48,7 @@ export const permissionsToCheck = { type Permissions = Record export interface AuthContext { - getUserError?: Error | unknown + getUserError?: Error | unknown // not used anywhere getMethodsError?: Error | unknown authError?: Error | AxiosError | unknown updateProfileError?: Error | unknown @@ -56,11 +56,11 @@ export interface AuthContext { me?: TypesGen.User methods?: TypesGen.AuthMethods permissions?: Permissions - checkPermissionsError?: Error | unknown + checkPermissionsError?: Error | unknown // not used anywhere // SSH sshKey?: TypesGen.GitSSHKey - getSSHKeyError?: Error | unknown - regenerateSSHKeyError?: Error | unknown + getSSHKeyError?: Error | unknown // not used anywhere + regenerateSSHKeyError?: Error | unknown // not used anywhere } export type AuthEvent = @@ -194,12 +194,12 @@ export const authMachine = }, }, signingIn: { + entry: "clearAuthError", invoke: { src: "signIn", id: "signIn", onDone: [ { - actions: "clearAuthError", target: "gettingUser", }, ], From f7c1560f4b65cf968561bb592b7bd709ad475d95 Mon Sep 17 00:00:00 2001 From: Abhineet Jain Date: Mon, 25 Jul 2022 21:12:50 +0000 Subject: [PATCH 2/5] fix stories --- .../SignInForm/SignInForm.stories.tsx | 23 +++++++++++++++---- site/src/xServices/auth/authXService.ts | 13 ++++++----- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/site/src/components/SignInForm/SignInForm.stories.tsx b/site/src/components/SignInForm/SignInForm.stories.tsx index 13a834fee7555..38672acfed7df 100644 --- a/site/src/components/SignInForm/SignInForm.stories.tsx +++ b/site/src/components/SignInForm/SignInForm.stories.tsx @@ -6,7 +6,6 @@ export default { component: SignInForm, argTypes: { isLoading: "boolean", - authErrorMessage: "string", onSubmit: { action: "Submit" }, }, } @@ -16,7 +15,7 @@ const Template: Story = (args: SignInFormProps) => { return Promise.resolve() }, @@ -33,12 +32,28 @@ Loading.args = { } export const WithLoginError = Template.bind({}) -WithLoginError.args = { ...SignedOut.args, authErrorMessage: "Email or password was invalid" } +WithLoginError.args = { + ...SignedOut.args, + authError: { + response: { + data: { + message: "Email or password was invalid", + validations: [ + { + field: "password", + detail: "Password is invalid.", + }, + ], + }, + }, + isAxiosError: true, + }, +} export const WithAuthMethodsError = Template.bind({}) WithAuthMethodsError.args = { ...SignedOut.args, - methodsErrorMessage: "Failed to fetch auth methods", + methodsError: new Error("Failed to fetch auth methods"), } export const WithGithub = Template.bind({}) diff --git a/site/src/xServices/auth/authXService.ts b/site/src/xServices/auth/authXService.ts index 47433cc01d85c..11d2c9c68d7b3 100644 --- a/site/src/xServices/auth/authXService.ts +++ b/site/src/xServices/auth/authXService.ts @@ -1,4 +1,3 @@ -import { AxiosError } from "axios" import { assign, createMachine } from "xstate" import * as API from "../../api/api" import * as TypesGen from "../../api/typesGenerated" @@ -48,19 +47,21 @@ export const permissionsToCheck = { type Permissions = Record export interface AuthContext { - getUserError?: Error | unknown // not used anywhere + getUserError?: Error | unknown + // The getMethods API call does not return an ApiError. + // It can only error out in a generic fashion. getMethodsError?: Error | unknown - authError?: Error | AxiosError | unknown + authError?: Error | unknown updateProfileError?: Error | unknown updateSecurityError?: Error | unknown me?: TypesGen.User methods?: TypesGen.AuthMethods permissions?: Permissions - checkPermissionsError?: Error | unknown // not used anywhere + checkPermissionsError?: Error | unknown // SSH sshKey?: TypesGen.GitSSHKey - getSSHKeyError?: Error | unknown // not used anywhere - regenerateSSHKeyError?: Error | unknown // not used anywhere + getSSHKeyError?: Error | unknown + regenerateSSHKeyError?: Error | unknown } export type AuthEvent = From 047b6c77f45b6e8fa1af345aa78b480b40cca7d9 Mon Sep 17 00:00:00 2001 From: Abhineet Jain Date: Mon, 25 Jul 2022 21:33:26 +0000 Subject: [PATCH 3/5] add new stories --- .../SettingsAccountForm.stories.tsx | 50 ++++++++++++++++++ .../SettingsAccountForm.tsx | 4 +- .../SettingsSecurityForm.stories.tsx | 51 +++++++++++++++++++ .../SettingsSecurityForm.tsx | 3 +- 4 files changed, 103 insertions(+), 5 deletions(-) create mode 100644 site/src/components/SettingsAccountForm/SettingsAccountForm.stories.tsx create mode 100644 site/src/components/SettingsSecurityForm/SettingsSecurityForm.stories.tsx diff --git a/site/src/components/SettingsAccountForm/SettingsAccountForm.stories.tsx b/site/src/components/SettingsAccountForm/SettingsAccountForm.stories.tsx new file mode 100644 index 0000000000000..5021aec052134 --- /dev/null +++ b/site/src/components/SettingsAccountForm/SettingsAccountForm.stories.tsx @@ -0,0 +1,50 @@ +import { Story } from "@storybook/react" +import { AccountForm, AccountFormProps } from "./SettingsAccountForm" + +export default { + title: "components/SettingsAccountForm", + component: AccountForm, + argTypes: { + onSubmit: { action: "Submit" }, + } +} + +const Template: Story = (args: AccountFormProps) => + +export const Example = Template.bind({}) +Example.args = { + email: "test-user@org.com", + isLoading: false, + initialValues: { + username: "test-user", + }, + updateProfileError: undefined, + onSubmit: () => { + return Promise.resolve() + }, +} + +export const Loading = Template.bind({}) +Loading.args = { + ...Example.args, + isLoading: true, +} + +export const WithError = Template.bind({}) +WithError.args = { + ...Example.args, + updateProfileError: { + response: { + data: { + message: "Username is invalid", + validations: [ + { + field: "username", + detail: "Username is too long.", + }, + ], + }, + }, + isAxiosError: true, + }, +} diff --git a/site/src/components/SettingsAccountForm/SettingsAccountForm.tsx b/site/src/components/SettingsAccountForm/SettingsAccountForm.tsx index 17f5f70f15446..3cca22725f562 100644 --- a/site/src/components/SettingsAccountForm/SettingsAccountForm.tsx +++ b/site/src/components/SettingsAccountForm/SettingsAccountForm.tsx @@ -1,6 +1,6 @@ import TextField from "@material-ui/core/TextField" import { ErrorSummary } from "components/ErrorSummary/ErrorSummary" -import { FormikContextType, FormikErrors, useFormik } from "formik" +import { FormikContextType, useFormik } from "formik" import { FC } from "react" import * as Yup from "yup" import { getFormHelpersWithError, nameValidator, onChangeTrimmed } from "../../util/formUtils" @@ -21,8 +21,6 @@ const validationSchema = Yup.object({ username: nameValidator(Language.usernameLabel), }) -export type AccountFormErrors = FormikErrors - export interface AccountFormProps { email: string isLoading: boolean diff --git a/site/src/components/SettingsSecurityForm/SettingsSecurityForm.stories.tsx b/site/src/components/SettingsSecurityForm/SettingsSecurityForm.stories.tsx new file mode 100644 index 0000000000000..b50f99e1698ac --- /dev/null +++ b/site/src/components/SettingsSecurityForm/SettingsSecurityForm.stories.tsx @@ -0,0 +1,51 @@ +import { Story } from "@storybook/react" +import { SecurityForm, SecurityFormProps } from "./SettingsSecurityForm" + +export default { + title: "components/SettingsSecurityForm", + component: SecurityForm, + argTypes: { + onSubmit: { action: "Submit" }, + } +} + +const Template: Story = (args: SecurityFormProps) => + +export const Example = Template.bind({}) +Example.args = { + isLoading: false, + initialValues: { + old_password: "", + password: "", + confirm_password: "" + }, + updateSecurityError: undefined, + onSubmit: () => { + return Promise.resolve() + }, +} + +export const Loading = Template.bind({}) +Loading.args = { + ...Example.args, + isLoading: true, +} + +export const WithError = Template.bind({}) +WithError.args = { + ...Example.args, + updateSecurityError: { + response: { + data: { + message: "Old password is incorrect", + validations: [ + { + field: "old_password", + detail: "Old password is incorrect.", + }, + ], + }, + }, + isAxiosError: true, + }, +} diff --git a/site/src/components/SettingsSecurityForm/SettingsSecurityForm.tsx b/site/src/components/SettingsSecurityForm/SettingsSecurityForm.tsx index cbeedcaffc42e..80a84aea0d6dc 100644 --- a/site/src/components/SettingsSecurityForm/SettingsSecurityForm.tsx +++ b/site/src/components/SettingsSecurityForm/SettingsSecurityForm.tsx @@ -1,6 +1,6 @@ import TextField from "@material-ui/core/TextField" import { ErrorSummary } from "components/ErrorSummary/ErrorSummary" -import { FormikContextType, FormikErrors, useFormik } from "formik" +import { FormikContextType, useFormik } from "formik" import React from "react" import * as Yup from "yup" import { getFormHelpersWithError, onChangeTrimmed } from "../../util/formUtils" @@ -40,7 +40,6 @@ const validationSchema = Yup.object({ }), }) -export type SecurityFormErrors = FormikErrors export interface SecurityFormProps { isLoading: boolean initialValues: SecurityFormValues From 6a71be56075769541a42b8e56619b5f1ffb39ad3 Mon Sep 17 00:00:00 2001 From: Abhineet Jain Date: Mon, 25 Jul 2022 21:36:42 +0000 Subject: [PATCH 4/5] fix lint --- .../SettingsAccountForm/SettingsAccountForm.stories.tsx | 2 +- .../SettingsSecurityForm/SettingsSecurityForm.stories.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/site/src/components/SettingsAccountForm/SettingsAccountForm.stories.tsx b/site/src/components/SettingsAccountForm/SettingsAccountForm.stories.tsx index 5021aec052134..95bda47e28f34 100644 --- a/site/src/components/SettingsAccountForm/SettingsAccountForm.stories.tsx +++ b/site/src/components/SettingsAccountForm/SettingsAccountForm.stories.tsx @@ -6,7 +6,7 @@ export default { component: AccountForm, argTypes: { onSubmit: { action: "Submit" }, - } + }, } const Template: Story = (args: AccountFormProps) => diff --git a/site/src/components/SettingsSecurityForm/SettingsSecurityForm.stories.tsx b/site/src/components/SettingsSecurityForm/SettingsSecurityForm.stories.tsx index b50f99e1698ac..4150c9d928504 100644 --- a/site/src/components/SettingsSecurityForm/SettingsSecurityForm.stories.tsx +++ b/site/src/components/SettingsSecurityForm/SettingsSecurityForm.stories.tsx @@ -6,7 +6,7 @@ export default { component: SecurityForm, argTypes: { onSubmit: { action: "Submit" }, - } + }, } const Template: Story = (args: SecurityFormProps) => @@ -17,7 +17,7 @@ Example.args = { initialValues: { old_password: "", password: "", - confirm_password: "" + confirm_password: "", }, updateSecurityError: undefined, onSubmit: () => { From 9faca3c6adf6d62cf7b97de63acb1339b4b9d57a Mon Sep 17 00:00:00 2001 From: Abhineet Jain Date: Tue, 26 Jul 2022 19:30:47 +0000 Subject: [PATCH 5/5] fix error state stories --- .../SettingsAccountForm/SettingsAccountForm.stories.tsx | 3 +++ .../components/SettingsAccountForm/SettingsAccountForm.tsx | 6 +++++- .../SettingsSecurityForm/SettingsSecurityForm.stories.tsx | 3 +++ .../SettingsSecurityForm/SettingsSecurityForm.tsx | 6 +++++- site/src/components/SignInForm/SignInForm.stories.tsx | 3 +++ site/src/components/SignInForm/SignInForm.tsx | 6 +++++- 6 files changed, 24 insertions(+), 3 deletions(-) diff --git a/site/src/components/SettingsAccountForm/SettingsAccountForm.stories.tsx b/site/src/components/SettingsAccountForm/SettingsAccountForm.stories.tsx index 95bda47e28f34..2698f3b67fcb6 100644 --- a/site/src/components/SettingsAccountForm/SettingsAccountForm.stories.tsx +++ b/site/src/components/SettingsAccountForm/SettingsAccountForm.stories.tsx @@ -47,4 +47,7 @@ WithError.args = { }, isAxiosError: true, }, + initialTouched: { + username: true, + }, } diff --git a/site/src/components/SettingsAccountForm/SettingsAccountForm.tsx b/site/src/components/SettingsAccountForm/SettingsAccountForm.tsx index 3cca22725f562..2fb4c90f73754 100644 --- a/site/src/components/SettingsAccountForm/SettingsAccountForm.tsx +++ b/site/src/components/SettingsAccountForm/SettingsAccountForm.tsx @@ -1,6 +1,6 @@ import TextField from "@material-ui/core/TextField" import { ErrorSummary } from "components/ErrorSummary/ErrorSummary" -import { FormikContextType, useFormik } from "formik" +import { FormikContextType, FormikTouched, useFormik } from "formik" import { FC } from "react" import * as Yup from "yup" import { getFormHelpersWithError, nameValidator, onChangeTrimmed } from "../../util/formUtils" @@ -27,6 +27,8 @@ export interface AccountFormProps { initialValues: AccountFormValues onSubmit: (values: AccountFormValues) => void updateProfileError?: Error | unknown + // initialTouched is only used for testing the error state of the form. + initialTouched?: FormikTouched } export const AccountForm: FC = ({ @@ -35,11 +37,13 @@ export const AccountForm: FC = ({ onSubmit, initialValues, updateProfileError, + initialTouched, }) => { const form: FormikContextType = useFormik({ initialValues, validationSchema, onSubmit, + initialTouched, }) const getFieldHelpers = getFormHelpersWithError(form, updateProfileError) diff --git a/site/src/components/SettingsSecurityForm/SettingsSecurityForm.stories.tsx b/site/src/components/SettingsSecurityForm/SettingsSecurityForm.stories.tsx index 4150c9d928504..841011405c717 100644 --- a/site/src/components/SettingsSecurityForm/SettingsSecurityForm.stories.tsx +++ b/site/src/components/SettingsSecurityForm/SettingsSecurityForm.stories.tsx @@ -48,4 +48,7 @@ WithError.args = { }, isAxiosError: true, }, + initialTouched: { + old_password: true, + }, } diff --git a/site/src/components/SettingsSecurityForm/SettingsSecurityForm.tsx b/site/src/components/SettingsSecurityForm/SettingsSecurityForm.tsx index 80a84aea0d6dc..a92102012e153 100644 --- a/site/src/components/SettingsSecurityForm/SettingsSecurityForm.tsx +++ b/site/src/components/SettingsSecurityForm/SettingsSecurityForm.tsx @@ -1,6 +1,6 @@ import TextField from "@material-ui/core/TextField" import { ErrorSummary } from "components/ErrorSummary/ErrorSummary" -import { FormikContextType, useFormik } from "formik" +import { FormikContextType, FormikTouched, useFormik } from "formik" import React from "react" import * as Yup from "yup" import { getFormHelpersWithError, onChangeTrimmed } from "../../util/formUtils" @@ -45,6 +45,8 @@ export interface SecurityFormProps { initialValues: SecurityFormValues onSubmit: (values: SecurityFormValues) => void updateSecurityError?: Error | unknown + // initialTouched is only used for testing the error state of the form. + initialTouched?: FormikTouched } export const SecurityForm: React.FC = ({ @@ -52,11 +54,13 @@ export const SecurityForm: React.FC = ({ onSubmit, initialValues, updateSecurityError, + initialTouched, }) => { const form: FormikContextType = useFormik({ initialValues, validationSchema, onSubmit, + initialTouched, }) const getFieldHelpers = getFormHelpersWithError(form, updateSecurityError) diff --git a/site/src/components/SignInForm/SignInForm.stories.tsx b/site/src/components/SignInForm/SignInForm.stories.tsx index 38672acfed7df..7e6b24f79ed2a 100644 --- a/site/src/components/SignInForm/SignInForm.stories.tsx +++ b/site/src/components/SignInForm/SignInForm.stories.tsx @@ -48,6 +48,9 @@ WithLoginError.args = { }, isAxiosError: true, }, + initialTouched: { + password: true, + }, } export const WithAuthMethodsError = Template.bind({}) diff --git a/site/src/components/SignInForm/SignInForm.tsx b/site/src/components/SignInForm/SignInForm.tsx index a20ad47080ecf..bfa28ca8bd3cb 100644 --- a/site/src/components/SignInForm/SignInForm.tsx +++ b/site/src/components/SignInForm/SignInForm.tsx @@ -5,7 +5,7 @@ import TextField from "@material-ui/core/TextField" import GitHubIcon from "@material-ui/icons/GitHub" import { ErrorSummary } from "components/ErrorSummary/ErrorSummary" import { Stack } from "components/Stack/Stack" -import { FormikContextType, useFormik } from "formik" +import { FormikContextType, FormikTouched, useFormik } from "formik" import { FC } from "react" import * as Yup from "yup" import { AuthMethods } from "../../api/typesGenerated" @@ -72,6 +72,8 @@ export interface SignInFormProps { methodsError?: Error | unknown authMethods?: AuthMethods onSubmit: ({ email, password }: { email: string; password: string }) => Promise + // initialTouched is only used for testing the error state of the form. + initialTouched?: FormikTouched } export const SignInForm: FC = ({ @@ -81,6 +83,7 @@ export const SignInForm: FC = ({ authError, methodsError, onSubmit, + initialTouched, }) => { const styles = useStyles() @@ -96,6 +99,7 @@ export const SignInForm: FC = ({ // field), or after a submission attempt. validateOnBlur: false, onSubmit, + initialTouched, }) const getFieldHelpers = getFormHelpersWithError(form, authError)