diff --git a/site/src/components/CreateUserForm/CreateUserForm.tsx b/site/src/components/CreateUserForm/CreateUserForm.tsx index cc7e8fcdeab76..710d41d6b6576 100644 --- a/site/src/components/CreateUserForm/CreateUserForm.tsx +++ b/site/src/components/CreateUserForm/CreateUserForm.tsx @@ -4,7 +4,7 @@ import { FormikContextType, FormikErrors, useFormik } from "formik" import React from "react" import * as Yup from "yup" import * as TypesGen from "../../api/typesGenerated" -import { getFormHelpers, onChangeTrimmed } from "../../util/formUtils" +import { getFormHelpers, nameValidator, onChangeTrimmed } from "../../util/formUtils" import { FormFooter } from "../FormFooter/FormFooter" import { FullPageForm } from "../FullPageForm/FullPageForm" @@ -15,7 +15,6 @@ export const Language = { 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", } @@ -32,7 +31,7 @@ export interface CreateUserFormProps { 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), + username: nameValidator(Language.usernameLabel), }) export const CreateUserForm: React.FC = ({ diff --git a/site/src/components/SettingsAccountForm/SettingsAccountForm.tsx b/site/src/components/SettingsAccountForm/SettingsAccountForm.tsx index 0e14a0d018653..392671cee5e46 100644 --- a/site/src/components/SettingsAccountForm/SettingsAccountForm.tsx +++ b/site/src/components/SettingsAccountForm/SettingsAccountForm.tsx @@ -3,7 +3,7 @@ import TextField from "@material-ui/core/TextField" import { FormikContextType, FormikErrors, useFormik } from "formik" import React from "react" import * as Yup from "yup" -import { getFormHelpers, onChangeTrimmed } from "../../util/formUtils" +import { getFormHelpers, nameValidator, onChangeTrimmed } from "../../util/formUtils" import { LoadingButton } from "../LoadingButton/LoadingButton" import { Stack } from "../Stack/Stack" @@ -22,7 +22,7 @@ export const Language = { const validationSchema = Yup.object({ email: Yup.string().trim().email(Language.emailInvalid).required(Language.emailRequired), - username: Yup.string().trim(), + username: nameValidator(Language.usernameLabel), }) export type AccountFormErrors = FormikErrors diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx index a0f11bd6e5f1a..008505671bb26 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx @@ -1,13 +1,13 @@ import { screen, waitFor } from "@testing-library/react" import userEvent from "@testing-library/user-event" import React from "react" -import { reach, StringSchema } from "yup" import * as API from "../../api/api" import { Language as FooterLanguage } from "../../components/FormFooter/FormFooter" import { MockTemplate, MockWorkspace } from "../../testHelpers/entities" import { renderWithAuth } from "../../testHelpers/renderHelpers" +import { Language as FormLanguage } from "../../util/formUtils" import CreateWorkspacePage from "./CreateWorkspacePage" -import { Language, validationSchema } from "./CreateWorkspacePageView" +import { Language } from "./CreateWorkspacePageView" const renderCreateWorkspacePage = () => { return renderWithAuth(, { @@ -23,8 +23,6 @@ const fillForm = async ({ name = "example" }: { name?: string }) => { await userEvent.click(submitButton) } -const nameSchema = reach(validationSchema, "name") as StringSchema - describe("CreateWorkspacePage", () => { it("renders", async () => { renderCreateWorkspacePage() @@ -35,7 +33,7 @@ describe("CreateWorkspacePage", () => { it("shows validation error message", async () => { renderCreateWorkspacePage() await fillForm({ name: "$$$" }) - const errorMessage = await screen.findByText(Language.nameMatches) + const errorMessage = await screen.findByText(FormLanguage.nameInvalidChars(Language.nameLabel)) expect(errorMessage).toBeDefined() }) @@ -47,38 +45,4 @@ describe("CreateWorkspacePage", () => { // Check if the request was made await waitFor(() => expect(API.createWorkspace).toBeCalledTimes(1)) }) - - describe("validationSchema", () => { - it("allows a 1-letter name", () => { - const validate = () => nameSchema.validateSync("t") - expect(validate).not.toThrow() - }) - - it("allows a 32-letter name", () => { - const input = Array(32).fill("a").join("") - const validate = () => nameSchema.validateSync(input) - expect(validate).not.toThrow() - }) - - it("allows 'test-3' to be used as name", () => { - const validate = () => nameSchema.validateSync("test-3") - expect(validate).not.toThrow() - }) - - it("allows '3-test' to be used as a name", () => { - const validate = () => nameSchema.validateSync("3-test") - expect(validate).not.toThrow() - }) - - it("disallows a 33-letter name", () => { - const input = Array(33).fill("a").join("") - const validate = () => nameSchema.validateSync(input) - expect(validate).toThrow() - }) - - it("disallows a space", () => { - const validate = () => nameSchema.validateSync("test 3") - expect(validate).toThrow() - }) - }) }) diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx index 3370c3bef2340..07c7dbe680fce 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx @@ -10,22 +10,13 @@ import { Loader } from "../../components/Loader/Loader" import { Margins } from "../../components/Margins/Margins" import { ParameterInput } from "../../components/ParameterInput/ParameterInput" import { Stack } from "../../components/Stack/Stack" -import { getFormHelpers, onChangeTrimmed } from "../../util/formUtils" +import { getFormHelpers, nameValidator, onChangeTrimmed } from "../../util/formUtils" export const Language = { templateLabel: "Template", nameLabel: "Name", - nameRequired: "Please enter a name.", - nameMatches: "Name must start with a-Z or 0-9 and can contain a-Z, 0-9 or -", - nameMax: "Name cannot be longer than 32 characters", } -// REMARK: Keep in sync with coderd/httpapi/httpapi.go#L40 -const maxLenName = 32 - -// REMARK: Keep in sync with coderd/httpapi/httpapi.go#L18 -const usernameRE = /^[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*$/ - export interface CreateWorkspacePageViewProps { loadingTemplates: boolean loadingTemplateSchema: boolean @@ -39,10 +30,7 @@ export interface CreateWorkspacePageViewProps { } export const validationSchema = Yup.object({ - name: Yup.string() - .required(Language.nameRequired) - .matches(usernameRE, Language.nameMatches) - .max(maxLenName, Language.nameMax), + name: nameValidator(Language.nameLabel), }) export const CreateWorkspacePageView: React.FC = (props) => { diff --git a/site/src/util/formUtils.test.ts b/site/src/util/formUtils.test.ts index 7d3a53ad51c3e..41d4005e71a6e 100644 --- a/site/src/util/formUtils.test.ts +++ b/site/src/util/formUtils.test.ts @@ -1,5 +1,5 @@ import { FormikContextType } from "formik/dist/types" -import { getFormHelpers, onChangeTrimmed } from "./formUtils" +import { getFormHelpers, nameValidator, onChangeTrimmed } from "./formUtils" interface TestType { untouchedGoodField: string @@ -35,6 +35,8 @@ const form = { }, } as unknown as FormikContextType +const nameSchema = nameValidator("name") + describe("form util functions", () => { describe("getFormHelpers", () => { describe("without API errors", () => { @@ -94,4 +96,38 @@ describe("form util functions", () => { expect(mockHandleChange).toHaveBeenCalledWith({ target: { value: "hello" } }) }) }) + + describe("nameValidator", () => { + it("allows a 1-letter name", () => { + const validate = () => nameSchema.validateSync("a") + expect(validate).not.toThrow() + }) + + it("allows a 32-letter name", () => { + const input = Array(32).fill("a").join("") + const validate = () => nameSchema.validateSync(input) + expect(validate).not.toThrow() + }) + + it("allows 'test-3' to be used as name", () => { + const validate = () => nameSchema.validateSync("test-3") + expect(validate).not.toThrow() + }) + + it("allows '3-test' to be used as a name", () => { + const validate = () => nameSchema.validateSync("3-test") + expect(validate).not.toThrow() + }) + + it("disallows a 33-letter name", () => { + const input = Array(33).fill("a").join("") + const validate = () => nameSchema.validateSync(input) + expect(validate).toThrow() + }) + + it("disallows a space", () => { + const validate = () => nameSchema.validateSync("test 3") + expect(validate).toThrow() + }) + }) }) diff --git a/site/src/util/formUtils.ts b/site/src/util/formUtils.ts index 108ffdbedf261..12d4939693d4d 100644 --- a/site/src/util/formUtils.ts +++ b/site/src/util/formUtils.ts @@ -1,5 +1,18 @@ import { FormikContextType, FormikErrors, getIn } from "formik" import { ChangeEvent, ChangeEventHandler, FocusEventHandler, ReactNode } from "react" +import * as Yup from "yup" + +export const Language = { + nameRequired: (name: string): string => { + return `Please enter a ${name.toLowerCase()}.` + }, + nameInvalidChars: (name: string): string => { + return `${name} must start with a-Z or 0-9 and can contain a-Z, 0-9 or -` + }, + nameTooLong: (name: string): string => { + return `${name} cannot be longer than 32 characters` + }, +} interface FormHelpers { name: string @@ -38,3 +51,16 @@ export const onChangeTrimmed = event.target.value = event.target.value.trim() form.handleChange(event) } + +// REMARK: Keep in sync with coderd/httpapi/httpapi.go#L40 +const maxLenName = 32 + +// REMARK: Keep in sync with coderd/httpapi/httpapi.go#L18 +const usernameRE = /^[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*$/ + +// REMARK: see #1756 for name/username semantics +export const nameValidator = (name: string): Yup.StringSchema => + Yup.string() + .required(Language.nameRequired(name)) + .matches(usernameRE, Language.nameInvalidChars(name)) + .max(maxLenName, Language.nameTooLong(name)) pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy