fix: CreateWorkspaceForm name validation (#1453)

* refactor: allow helperText in getFormHelpers

By passing in helperText, we will not accidentally overwrite it.

* fix: CreateWorkspaceForm name validation

Resolves: #1421
This commit is contained in:
G r e y
2022-05-16 15:29:59 -04:00
committed by GitHub
parent 4103ba0b71
commit 9d4182b189
3 changed files with 75 additions and 17 deletions
+38 -1
View File
@@ -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()
})
})
})
+35 -14
View File
@@ -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<TypesGen.Workspace>
@@ -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<CreateWorkspaceForm> = ({
@@ -30,19 +52,19 @@ export const CreateWorkspaceForm: React.FC<CreateWorkspaceForm> = ({
}) => {
const styles = useStyles()
const form: FormikContextType<{ name: string }> = useFormik<{ name: string }>({
const form = useFormik<CreateWorkspaceFormValues>({
initialValues: {
name: "",
},
enableReinitialize: true,
validationSchema: validationSchema,
onSubmit: ({ name }) => {
return onSubmit(organizationId, {
template_id: template.id,
name: name,
})
},
validationSchema: validationSchema,
})
const getFieldHelpers = getFormHelpers<CreateWorkspaceFormValues>(form)
return (
<div className={styles.root}>
@@ -57,14 +79,13 @@ export const CreateWorkspaceForm: React.FC<CreateWorkspaceForm> = ({
<FormCloseButton onClose={onCancel} />
<FormSection title="Name">
<FormTextField
form={form}
formFieldName="name"
<TextField
{...getFieldHelpers("name", Language.nameHelperText)}
onChange={onChangeTrimmed(form)}
autoFocus
fullWidth
helperText="A unique name describing your workspace."
label="Workspace Name"
placeholder="my-workspace"
required
label={Language.nameLabel}
placeholder={Language.namePlaceholder}
/>
</FormSection>
+2 -2
View File
@@ -13,7 +13,7 @@ interface FormHelpers {
export const getFormHelpers =
<T>(form: FormikContextType<T>, formErrors?: FormikErrors<T>) =>
(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,
}
}