diff --git a/site/src/forms/CreateWorkspaceForm.test.tsx b/site/src/forms/CreateWorkspaceForm.test.tsx index a3096706bd66d..8d523820e8a51 100644 --- a/site/src/forms/CreateWorkspaceForm.test.tsx +++ b/site/src/forms/CreateWorkspaceForm.test.tsx @@ -1,7 +1,10 @@ import { render, screen } from "@testing-library/react" import React from "react" +import { reach, StringSchema } from "yup" import { MockOrganization, MockTemplate, MockWorkspace } from "../testHelpers/renderHelpers" -import { CreateWorkspaceForm } from "./CreateWorkspaceForm" +import { CreateWorkspaceForm, validationSchema } from "./CreateWorkspaceForm" + +const nameSchema = reach(validationSchema, "name") as StringSchema describe("CreateWorkspaceForm", () => { it("renders", async () => { @@ -24,4 +27,38 @@ describe("CreateWorkspaceForm", () => { const element = await screen.findByText("Create Workspace") expect(element).toBeDefined() }) + + 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/forms/CreateWorkspaceForm.tsx b/site/src/forms/CreateWorkspaceForm.tsx index 56e7c0a44059d..a9d963543d169 100644 --- a/site/src/forms/CreateWorkspaceForm.tsx +++ b/site/src/forms/CreateWorkspaceForm.tsx @@ -1,16 +1,25 @@ import Button from "@material-ui/core/Button" import { makeStyles } from "@material-ui/core/styles" -import { FormikContextType, useFormik } from "formik" +import TextField from "@material-ui/core/TextField" +import { useFormik } from "formik" import React from "react" import * as Yup from "yup" import * as TypesGen from "../api/typesGenerated" import { FormCloseButton } from "../components/FormCloseButton/FormCloseButton" import { FormSection } from "../components/FormSection/FormSection" -import { FormTextField } from "../components/FormTextField/FormTextField" import { FormTitle } from "../components/FormTitle/FormTitle" import { LoadingButton } from "../components/LoadingButton/LoadingButton" import { maxWidth } from "../theme/constants" +import { getFormHelpers, onChangeTrimmed } from "../util/formUtils" +export const Language = { + nameHelperText: "A unique name describing your workspace", + nameLabel: "Workspace 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", + namePlaceholder: "my-workspace", + nameRequired: "Name is required", +} export interface CreateWorkspaceForm { template: TypesGen.Template onSubmit: (organizationId: string, request: TypesGen.CreateWorkspaceRequest) => Promise @@ -18,8 +27,21 @@ export interface CreateWorkspaceForm { organizationId: string } -const validationSchema = Yup.object({ - name: Yup.string().required("Name is required"), +export interface CreateWorkspaceFormValues { + name: string +} + +// 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 const validationSchema = Yup.object({ + name: Yup.string() + .matches(usernameRE, Language.nameMatches) + .max(maxLenName, Language.nameMax) + .required(Language.nameRequired), }) export const CreateWorkspaceForm: React.FC = ({ @@ -30,19 +52,19 @@ export const CreateWorkspaceForm: React.FC = ({ }) => { const styles = useStyles() - const form: FormikContextType<{ name: string }> = useFormik<{ name: string }>({ + const form = useFormik({ initialValues: { name: "", }, - enableReinitialize: true, - validationSchema: validationSchema, onSubmit: ({ name }) => { return onSubmit(organizationId, { template_id: template.id, name: name, }) }, + validationSchema: validationSchema, }) + const getFieldHelpers = getFormHelpers(form) return (
@@ -57,14 +79,13 @@ export const CreateWorkspaceForm: React.FC = ({ - diff --git a/site/src/util/formUtils.ts b/site/src/util/formUtils.ts index c21dd9c29aa21..da5d1b6b87a78 100644 --- a/site/src/util/formUtils.ts +++ b/site/src/util/formUtils.ts @@ -13,7 +13,7 @@ interface FormHelpers { export const getFormHelpers = (form: FormikContextType, formErrors?: FormikErrors) => - (name: keyof T): FormHelpers => { + (name: keyof T, helperText = ""): FormHelpers => { if (typeof name !== "string") { throw new Error(`name must be type of string, instead received '${typeof name}'`) } @@ -28,7 +28,7 @@ export const getFormHelpers = ...form.getFieldProps(name), id: name, error: touched && Boolean(error), - helperText: touched && error, + helperText: touched ? error : helperText, } }