Skip to content

Commit ca72c6a

Browse files
greyscaledkylecarbs
authored andcommitted
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
1 parent 9505202 commit ca72c6a

File tree

3 files changed

+75
-17
lines changed

3 files changed

+75
-17
lines changed

site/src/forms/CreateWorkspaceForm.test.tsx

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { render, screen } from "@testing-library/react"
22
import React from "react"
3+
import { reach, StringSchema } from "yup"
34
import { MockOrganization, MockTemplate, MockWorkspace } from "../testHelpers/renderHelpers"
4-
import { CreateWorkspaceForm } from "./CreateWorkspaceForm"
5+
import { CreateWorkspaceForm, validationSchema } from "./CreateWorkspaceForm"
6+
7+
const nameSchema = reach(validationSchema, "name") as StringSchema
58

69
describe("CreateWorkspaceForm", () => {
710
it("renders", async () => {
@@ -24,4 +27,38 @@ describe("CreateWorkspaceForm", () => {
2427
const element = await screen.findByText("Create Workspace")
2528
expect(element).toBeDefined()
2629
})
30+
31+
describe("validationSchema", () => {
32+
it("allows a 1-letter name", () => {
33+
const validate = () => nameSchema.validateSync("t")
34+
expect(validate).not.toThrow()
35+
})
36+
37+
it("allows a 32-letter name", () => {
38+
const input = Array(32).fill("a").join("")
39+
const validate = () => nameSchema.validateSync(input)
40+
expect(validate).not.toThrow()
41+
})
42+
43+
it("allows 'test-3' to be used as name", () => {
44+
const validate = () => nameSchema.validateSync("test-3")
45+
expect(validate).not.toThrow()
46+
})
47+
48+
it("allows '3-test' to be used as a name", () => {
49+
const validate = () => nameSchema.validateSync("3-test")
50+
expect(validate).not.toThrow()
51+
})
52+
53+
it("disallows a 33-letter name", () => {
54+
const input = Array(33).fill("a").join("")
55+
const validate = () => nameSchema.validateSync(input)
56+
expect(validate).toThrow()
57+
})
58+
59+
it("disallows a space", () => {
60+
const validate = () => nameSchema.validateSync("test 3")
61+
expect(validate).toThrow()
62+
})
63+
})
2764
})

site/src/forms/CreateWorkspaceForm.tsx

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,47 @@
11
import Button from "@material-ui/core/Button"
22
import { makeStyles } from "@material-ui/core/styles"
3-
import { FormikContextType, useFormik } from "formik"
3+
import TextField from "@material-ui/core/TextField"
4+
import { useFormik } from "formik"
45
import React from "react"
56
import * as Yup from "yup"
67
import * as TypesGen from "../api/typesGenerated"
78
import { FormCloseButton } from "../components/FormCloseButton/FormCloseButton"
89
import { FormSection } from "../components/FormSection/FormSection"
9-
import { FormTextField } from "../components/FormTextField/FormTextField"
1010
import { FormTitle } from "../components/FormTitle/FormTitle"
1111
import { LoadingButton } from "../components/LoadingButton/LoadingButton"
1212
import { maxWidth } from "../theme/constants"
13+
import { getFormHelpers, onChangeTrimmed } from "../util/formUtils"
1314

15+
export const Language = {
16+
nameHelperText: "A unique name describing your workspace",
17+
nameLabel: "Workspace Name",
18+
nameMatches: "Name must start with a-Z or 0-9 and can contain a-Z, 0-9 or -",
19+
nameMax: "Name cannot be longer than 32 characters",
20+
namePlaceholder: "my-workspace",
21+
nameRequired: "Name is required",
22+
}
1423
export interface CreateWorkspaceForm {
1524
template: TypesGen.Template
1625
onSubmit: (organizationId: string, request: TypesGen.CreateWorkspaceRequest) => Promise<TypesGen.Workspace>
1726
onCancel: () => void
1827
organizationId: string
1928
}
2029

21-
const validationSchema = Yup.object({
22-
name: Yup.string().required("Name is required"),
30+
export interface CreateWorkspaceFormValues {
31+
name: string
32+
}
33+
34+
// REMARK: Keep in sync with coderd/httpapi/httpapi.go#L40
35+
const maxLenName = 32
36+
37+
// REMARK: Keep in sync with coderd/httpapi/httpapi.go#L18
38+
const usernameRE = /^[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*$/
39+
40+
export const validationSchema = Yup.object({
41+
name: Yup.string()
42+
.matches(usernameRE, Language.nameMatches)
43+
.max(maxLenName, Language.nameMax)
44+
.required(Language.nameRequired),
2345
})
2446

2547
export const CreateWorkspaceForm: React.FC<CreateWorkspaceForm> = ({
@@ -30,19 +52,19 @@ export const CreateWorkspaceForm: React.FC<CreateWorkspaceForm> = ({
3052
}) => {
3153
const styles = useStyles()
3254

33-
const form: FormikContextType<{ name: string }> = useFormik<{ name: string }>({
55+
const form = useFormik<CreateWorkspaceFormValues>({
3456
initialValues: {
3557
name: "",
3658
},
37-
enableReinitialize: true,
38-
validationSchema: validationSchema,
3959
onSubmit: ({ name }) => {
4060
return onSubmit(organizationId, {
4161
template_id: template.id,
4262
name: name,
4363
})
4464
},
65+
validationSchema: validationSchema,
4566
})
67+
const getFieldHelpers = getFormHelpers<CreateWorkspaceFormValues>(form)
4668

4769
return (
4870
<div className={styles.root}>
@@ -57,14 +79,13 @@ export const CreateWorkspaceForm: React.FC<CreateWorkspaceForm> = ({
5779
<FormCloseButton onClose={onCancel} />
5880

5981
<FormSection title="Name">
60-
<FormTextField
61-
form={form}
62-
formFieldName="name"
82+
<TextField
83+
{...getFieldHelpers("name", Language.nameHelperText)}
84+
onChange={onChangeTrimmed(form)}
85+
autoFocus
6386
fullWidth
64-
helperText="A unique name describing your workspace."
65-
label="Workspace Name"
66-
placeholder="my-workspace"
67-
required
87+
label={Language.nameLabel}
88+
placeholder={Language.namePlaceholder}
6889
/>
6990
</FormSection>
7091

site/src/util/formUtils.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ interface FormHelpers {
1313

1414
export const getFormHelpers =
1515
<T>(form: FormikContextType<T>, formErrors?: FormikErrors<T>) =>
16-
(name: keyof T): FormHelpers => {
16+
(name: keyof T, helperText = ""): FormHelpers => {
1717
if (typeof name !== "string") {
1818
throw new Error(`name must be type of string, instead received '${typeof name}'`)
1919
}
@@ -28,7 +28,7 @@ export const getFormHelpers =
2828
...form.getFieldProps(name),
2929
id: name,
3030
error: touched && Boolean(error),
31-
helperText: touched && error,
31+
helperText: touched ? error : helperText,
3232
}
3333
}
3434

0 commit comments

Comments
 (0)