diff --git a/site/src/components/PreferencesAccountForm/PreferencesAccountForm.tsx b/site/src/components/PreferencesAccountForm/PreferencesAccountForm.tsx index c64556e2de..b33b75fe9a 100644 --- a/site/src/components/PreferencesAccountForm/PreferencesAccountForm.tsx +++ b/site/src/components/PreferencesAccountForm/PreferencesAccountForm.tsx @@ -49,13 +49,14 @@ export const AccountForm: React.FC = ({ validationSchema, onSubmit, }) + const getFieldHelpers = getFormHelpers(form, formErrors) return ( <>
(form, "name")} + {...getFieldHelpers("name")} autoFocus autoComplete="name" fullWidth @@ -63,7 +64,7 @@ export const AccountForm: React.FC = ({ variant="outlined" /> (form, "email", formErrors.email)} + {...getFieldHelpers("email")} onChange={onChangeTrimmed(form)} autoComplete="email" fullWidth @@ -71,7 +72,7 @@ export const AccountForm: React.FC = ({ variant="outlined" /> (form, "username", formErrors.username)} + {...getFieldHelpers("username")} onChange={onChangeTrimmed(form)} autoComplete="username" fullWidth diff --git a/site/src/components/SignInForm/SignInForm.tsx b/site/src/components/SignInForm/SignInForm.tsx index f1c1165911..38e32b41a4 100644 --- a/site/src/components/SignInForm/SignInForm.tsx +++ b/site/src/components/SignInForm/SignInForm.tsx @@ -76,13 +76,14 @@ export const SignInForm: React.FC = ({ validationSchema, onSubmit, }) + const getFieldHelpers = getFormHelpers(form) return ( <> (form, "email")} + {...getFieldHelpers("email")} onChange={onChangeTrimmed(form)} autoFocus autoComplete="email" @@ -93,7 +94,7 @@ export const SignInForm: React.FC = ({ variant="outlined" /> (form, "password")} + {...getFieldHelpers("password")} autoComplete="current-password" className={styles.loginTextField} fullWidth diff --git a/site/src/util/formUtils.test.ts b/site/src/util/formUtils.test.ts index 9218cb906b..7d3a53ad51 100644 --- a/site/src/util/formUtils.test.ts +++ b/site/src/util/formUtils.test.ts @@ -37,30 +37,53 @@ const form = { describe("form util functions", () => { describe("getFormHelpers", () => { - const untouchedGoodResult = getFormHelpers(form, "untouchedGoodField") - const untouchedBadResult = getFormHelpers(form, "untouchedBadField") - const touchedGoodResult = getFormHelpers(form, "touchedGoodField") - const touchedBadResult = getFormHelpers(form, "touchedBadField") - it("populates the 'field props'", () => { - expect(untouchedGoodResult.name).toEqual("untouchedGoodField") - expect(untouchedGoodResult.onBlur).toBeDefined() - expect(untouchedGoodResult.onChange).toBeDefined() - expect(untouchedGoodResult.value).toBeDefined() + describe("without API errors", () => { + const getFieldHelpers = getFormHelpers(form) + const untouchedGoodResult = getFieldHelpers("untouchedGoodField") + const untouchedBadResult = getFieldHelpers("untouchedBadField") + const touchedGoodResult = getFieldHelpers("touchedGoodField") + const touchedBadResult = getFieldHelpers("touchedBadField") + it("populates the 'field props'", () => { + expect(untouchedGoodResult.name).toEqual("untouchedGoodField") + expect(untouchedGoodResult.onBlur).toBeDefined() + expect(untouchedGoodResult.onChange).toBeDefined() + expect(untouchedGoodResult.value).toBeDefined() + }) + it("sets the id to the name", () => { + expect(untouchedGoodResult.id).toEqual("untouchedGoodField") + }) + it("sets error to true if touched and invalid", () => { + expect(untouchedGoodResult.error).toBeFalsy + expect(untouchedBadResult.error).toBeFalsy + expect(touchedGoodResult.error).toBeFalsy + expect(touchedBadResult.error).toBeTruthy + }) + it("sets helperText to the error message if touched and invalid", () => { + expect(untouchedGoodResult.helperText).toBeUndefined + expect(untouchedBadResult.helperText).toBeUndefined + expect(touchedGoodResult.helperText).toBeUndefined + expect(touchedBadResult.helperText).toEqual("oops!") + }) }) - it("sets the id to the name", () => { - expect(untouchedGoodResult.id).toEqual("untouchedGoodField") - }) - it("sets error to true if touched and invalid", () => { - expect(untouchedGoodResult.error).toBeFalsy - expect(untouchedBadResult.error).toBeFalsy - expect(touchedGoodResult.error).toBeFalsy - expect(touchedBadResult.error).toBeTruthy - }) - it("sets helperText to the error message if touched and invalid", () => { - expect(untouchedGoodResult.helperText).toBeUndefined - expect(untouchedBadResult.helperText).toBeUndefined - expect(touchedGoodResult.helperText).toBeUndefined - expect(touchedBadResult.helperText).toEqual("oops!") + describe("with API errors", () => { + it("shows an error if there is only an API error", () => { + const getFieldHelpers = getFormHelpers(form, { touchedGoodField: "API error!" }) + const result = getFieldHelpers("touchedGoodField") + expect(result.error).toBeTruthy + expect(result.helperText).toEqual("API error!") + }) + it("shows an error if there is only a validation error", () => { + const getFieldHelpers = getFormHelpers(form, {}) + const result = getFieldHelpers("touchedBadField") + expect(result.error).toBeTruthy + expect(result.helperText).toEqual("oops!") + }) + it("shows the API error if both are present", () => { + const getFieldHelpers = getFormHelpers(form, { touchedBadField: "API error!" }) + const result = getFieldHelpers("touchedBadField") + expect(result.error).toBeTruthy + expect(result.helperText).toEqual("API error!") + }) }) }) diff --git a/site/src/util/formUtils.ts b/site/src/util/formUtils.ts index 772aaaecbe..c21dd9c29a 100644 --- a/site/src/util/formUtils.ts +++ b/site/src/util/formUtils.ts @@ -1,4 +1,4 @@ -import { FormikContextType, getIn } from "formik" +import { FormikContextType, FormikErrors, getIn } from "formik" import { ChangeEvent, ChangeEventHandler, FocusEventHandler } from "react" interface FormHelpers { @@ -11,22 +11,26 @@ interface FormHelpers { helperText?: string } -export const getFormHelpers = (form: FormikContextType, name: keyof T, error?: string): FormHelpers => { - if (typeof name !== "string") { - throw new Error(`name must be type of string, instead received '${typeof name}'`) - } +export const getFormHelpers = + (form: FormikContextType, formErrors?: FormikErrors) => + (name: keyof T): FormHelpers => { + if (typeof name !== "string") { + throw new Error(`name must be type of string, instead received '${typeof name}'`) + } - // getIn is a util function from Formik that gets at any depth of nesting - // and is necessary for the types to work - const touched = getIn(form.touched, name) - const errors = error ?? getIn(form.errors, name) - return { - ...form.getFieldProps(name), - id: name, - error: touched && Boolean(errors), - helperText: touched && errors, + // getIn is a util function from Formik that gets at any depth of nesting + // and is necessary for the types to work + const touched = getIn(form.touched, name) + const apiError = getIn(formErrors, name) + const validationError = getIn(form.errors, name) + const error = apiError ?? validationError + return { + ...form.getFieldProps(name), + id: name, + error: touched && Boolean(error), + helperText: touched && error, + } } -} export const onChangeTrimmed = (form: FormikContextType) =>