mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
@@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user