From 51d8bb7ee22da62bd5849e12111228a3b8a96066 Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Tue, 6 Dec 2022 15:24:51 +0000 Subject: [PATCH 1/8] Fix helper text - handles 0 ttl - uses helper text typography - pluralizes - still doesn't override error (once considered touched) --- .../TemplateSettingsForm.tsx | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx b/site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx index 4a861694493b5..6b403aa8d72e4 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx @@ -36,10 +36,18 @@ export const Language = { "Please enter a limit that is less than or equal to 168 hours (7 days).", descriptionMaxError: "Please enter a description that is less than or equal to 128 characters.", - ttlHelperText: (ttl: number): string => + ttlHelperText: (ttl: number): string => ttl === 1 ? + `Workspaces created from this template will default to stopping after ${ttl} hour.` : `Workspaces created from this template will default to stopping after ${ttl} hours.`, + noTTL: "Workspaces created from this template will run until stopped manually." } +const TTLHelperText = ({ ttl }: { ttl?: number }) => ( + ttl !== undefined ? + {ttl === 0 ? Language.noTTL : Language.ttlHelperText(ttl)} + : null +) + const MAX_DESCRIPTION_CHAR_LIMIT = 128 const MAX_TTL_DAYS = 7 const MS_HOUR_CONVERSION = 3600000 @@ -112,6 +120,9 @@ export const TemplateSettingsForm: FC = ({ const { t } = useTranslation("templatePage") + console.log(form) + console.log(getFieldHelpers("default_ttl_ms")) + return (
@@ -204,7 +215,7 @@ export const TemplateSettingsForm: FC = ({ )} disabled={isSubmitting} fullWidth inputProps={{ min: 0, step: 1 }} @@ -212,12 +223,6 @@ export const TemplateSettingsForm: FC = ({ variant="outlined" type="number" /> - {/* If a value for default_ttl_ms has been entered and - there are no validation errors for that field, display helper text. - We do not use the MUI helper-text prop because it overrides the validation error */} - {form.values.default_ttl_ms && !form.errors.default_ttl_ms && ( - {Language.ttlHelperText(form.values.default_ttl_ms)} - )}
From 10bc4af563528fad1b87f7e44f0e9cb487172400 Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Tue, 6 Dec 2022 15:32:57 +0000 Subject: [PATCH 2/8] Show user friendly field name in error text --- .../TemplateSettingsPage/TemplateSettingsForm.tsx | 2 +- site/src/util/formUtils.ts | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx b/site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx index 6b403aa8d72e4..fb5fece7a07fc 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx @@ -215,7 +215,7 @@ export const TemplateSettingsForm: FC = ({
)} + {...getFieldHelpers("default_ttl_ms", , "Time until auto-stop")} disabled={isSubmitting} fullWidth inputProps={{ min: 0, step: 1 }} diff --git a/site/src/util/formUtils.ts b/site/src/util/formUtils.ts index 790e070341c5c..05b63ab3027df 100644 --- a/site/src/util/formUtils.ts +++ b/site/src/util/formUtils.ts @@ -37,13 +37,12 @@ interface FormHelpers { helperText?: ReactNode } -// backendErrorName can be used if the backend names a field differently than the frontend does export const getFormHelpers = (form: FormikContextType, error?: Error | unknown) => ( name: keyof T, HelperText: ReactNode = "", - backendErrorName?: string, + friendlyLabel?: string, ): FormHelpers => { const apiValidationErrors = isApiError(error) && hasApiFieldErrors(error) @@ -54,19 +53,19 @@ export const getFormHelpers = `name must be type of string, instead received '${typeof name}'`, ) } - const apiErrorName = backendErrorName ?? name // getIn is a util function from Formik that gets at any depth of nesting // and is necessary for the types to work const touched = getIn(form.touched, name) - const apiError = getIn(apiValidationErrors, apiErrorName) + const apiError = getIn(apiValidationErrors, name) const frontendError = getIn(form.errors, name) const returnError = apiError ?? frontendError + const friendlyError = friendlyLabel && returnError ? returnError.replace(name, friendlyLabel) : returnError return { ...form.getFieldProps(name), id: name, - error: touched && Boolean(returnError), - helperText: touched ? returnError || HelperText : HelperText, + error: touched && Boolean(friendlyError), + helperText: touched ? friendlyError || HelperText : HelperText, } } From 6a339d7c460282895f554e8b7d064def364fd214 Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Tue, 6 Dec 2022 15:33:32 +0000 Subject: [PATCH 3/8] Format --- .../TemplateSettingsForm.tsx | 28 ++++++++++--------- site/src/util/formUtils.ts | 5 +++- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx b/site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx index fb5fece7a07fc..6891b5267350f 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx @@ -36,17 +36,18 @@ export const Language = { "Please enter a limit that is less than or equal to 168 hours (7 days).", descriptionMaxError: "Please enter a description that is less than or equal to 128 characters.", - ttlHelperText: (ttl: number): string => ttl === 1 ? - `Workspaces created from this template will default to stopping after ${ttl} hour.` : - `Workspaces created from this template will default to stopping after ${ttl} hours.`, - noTTL: "Workspaces created from this template will run until stopped manually." + ttlHelperText: (ttl: number): string => + ttl === 1 + ? `Workspaces created from this template will default to stopping after ${ttl} hour.` + : `Workspaces created from this template will default to stopping after ${ttl} hours.`, + noTTL: + "Workspaces created from this template will run until stopped manually.", } -const TTLHelperText = ({ ttl }: { ttl?: number }) => ( - ttl !== undefined ? - {ttl === 0 ? Language.noTTL : Language.ttlHelperText(ttl)} - : null -) +const TTLHelperText = ({ ttl }: { ttl?: number }) => + ttl !== undefined ? ( + {ttl === 0 ? Language.noTTL : Language.ttlHelperText(ttl)} + ) : null const MAX_DESCRIPTION_CHAR_LIMIT = 128 const MAX_TTL_DAYS = 7 @@ -120,9 +121,6 @@ export const TemplateSettingsForm: FC = ({ const { t } = useTranslation("templatePage") - console.log(form) - console.log(getFieldHelpers("default_ttl_ms")) - return ( @@ -215,7 +213,11 @@ export const TemplateSettingsForm: FC = ({ , "Time until auto-stop")} + {...getFieldHelpers( + "default_ttl_ms", + , + "Time until auto-stop", + )} disabled={isSubmitting} fullWidth inputProps={{ min: 0, step: 1 }} diff --git a/site/src/util/formUtils.ts b/site/src/util/formUtils.ts index 05b63ab3027df..1320924151bc0 100644 --- a/site/src/util/formUtils.ts +++ b/site/src/util/formUtils.ts @@ -60,7 +60,10 @@ export const getFormHelpers = const apiError = getIn(apiValidationErrors, name) const frontendError = getIn(form.errors, name) const returnError = apiError ?? frontendError - const friendlyError = friendlyLabel && returnError ? returnError.replace(name, friendlyLabel) : returnError + const friendlyError = + friendlyLabel && returnError + ? returnError.replace(name, friendlyLabel) + : returnError return { ...form.getFieldProps(name), id: name, From 8fd2a7a4b9b945b0ebe33260642e0e46a532434b Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Tue, 6 Dec 2022 20:03:41 +0000 Subject: [PATCH 4/8] Override label through Yup instead --- site/src/util/formUtils.ts | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/site/src/util/formUtils.ts b/site/src/util/formUtils.ts index 1320924151bc0..1aa8ac8584edc 100644 --- a/site/src/util/formUtils.ts +++ b/site/src/util/formUtils.ts @@ -39,11 +39,7 @@ interface FormHelpers { export const getFormHelpers = (form: FormikContextType, error?: Error | unknown) => - ( - name: keyof T, - HelperText: ReactNode = "", - friendlyLabel?: string, - ): FormHelpers => { + (name: keyof T, HelperText: ReactNode = ""): FormHelpers => { const apiValidationErrors = isApiError(error) && hasApiFieldErrors(error) ? (mapApiErrorToFieldErrors(error.response.data) as FormikErrors) @@ -60,15 +56,11 @@ export const getFormHelpers = const apiError = getIn(apiValidationErrors, name) const frontendError = getIn(form.errors, name) const returnError = apiError ?? frontendError - const friendlyError = - friendlyLabel && returnError - ? returnError.replace(name, friendlyLabel) - : returnError return { ...form.getFieldProps(name), id: name, - error: touched && Boolean(friendlyError), - helperText: touched ? friendlyError || HelperText : HelperText, + error: touched && Boolean(returnError), + helperText: touched ? returnError || HelperText : HelperText, } } From fbfe2f71e764ca4eaf91d4aeca01cfb99b003f40 Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Tue, 6 Dec 2022 20:07:16 +0000 Subject: [PATCH 5/8] Switch to i18n - wip --- site/src/i18n/en/index.ts | 2 + site/src/i18n/en/templatePage.json | 14 +---- site/src/i18n/en/templateSettingsPage.json | 24 ++++++++ .../TemplateSettingsForm.tsx | 60 ++++++++----------- .../TemplateSettingsPage.tsx | 8 +-- .../TemplateSettingsPageView.tsx | 14 ++--- site/src/util/formUtils.ts | 2 +- 7 files changed, 63 insertions(+), 61 deletions(-) create mode 100644 site/src/i18n/en/templateSettingsPage.json diff --git a/site/src/i18n/en/index.ts b/site/src/i18n/en/index.ts index a8146f03aa316..79b6bc3109560 100644 --- a/site/src/i18n/en/index.ts +++ b/site/src/i18n/en/index.ts @@ -8,6 +8,7 @@ import agent from "./agent.json" import buildPage from "./buildPage.json" import workspacesPage from "./workspacesPage.json" import usersPage from "./usersPage.json" +import templateSettingsPage from "./templateSettingsPage.json" import templateVersionPage from "./templateVersionPage.json" import loginPage from "./loginPage.json" import workspaceChangeVersionPage from "./workspaceChangeVersionPage.json" @@ -23,6 +24,7 @@ export const en = { buildPage, workspacesPage, usersPage, + templateSettingsPage, templateVersionPage, loginPage, workspaceChangeVersionPage, diff --git a/site/src/i18n/en/templatePage.json b/site/src/i18n/en/templatePage.json index 1b54652bf51ee..03daf50eb649f 100644 --- a/site/src/i18n/en/templatePage.json +++ b/site/src/i18n/en/templatePage.json @@ -1,16 +1,4 @@ { "deleteSuccess": "Template successfully deleted.", - "createdVersion": "created the version", - "templateSettings": { - "title": "Template settings", - "dangerZone": { - "dangerZoneHeader": "Danger Zone", - "deleteTemplateHeader": "Delete this template", - "deleteTemplateCaption": "Do you want to permanently delete this template?", - "deleteCta": "Delete Template" - } - }, - "displayNameLabel": "Display name", - "allowUserCancelWorkspaceJobsLabel": "Allow users to cancel in-progress workspace jobs.", - "allowUserCancelWorkspaceJobsNotice": "Depending on your template, canceling builds may leave workspaces in an unhealthy state. This option isn't recommended for most use cases." + "createdVersion": "created the version" } diff --git a/site/src/i18n/en/templateSettingsPage.json b/site/src/i18n/en/templateSettingsPage.json new file mode 100644 index 0000000000000..5d3c61b3b0356 --- /dev/null +++ b/site/src/i18n/en/templateSettingsPage.json @@ -0,0 +1,24 @@ +{ + "title": "Template settings", + "nameLabel": "Name", + "displayNameLabel": "Display name", + "descriptionLabel": "Description", + "descriptionMaxError": "Please enter a description that is less than or equal to 128 characters.", + "defaultTtlLabel": "Auto-stop default", + "iconLabel": "Icon", + "formAriaLabel": "Template settings form", + "selectEmoji": "Select emoji", + "ttlMaxError": "Please enter a limit that is less than or equal to 168 hours (7 days).", + "ttlMinError": "Default time until auto-stop must not be less than 0.", + "ttlHelperText_zero": "Workspaces created from this template will run until stopped manually.", + "ttlHelperText_one": "Workspaces created from this template will default to stopping after {{count}} hour.", + "ttlHelperText_other": "Workspaces created from this template will default to stopping after {{count}} hours.", + "allowUserCancelWorkspaceJobsLabel": "Allow users to cancel in-progress workspace jobs.", + "allowUserCancelWorkspaceJobsNotice": "Depending on your template, canceling builds may leave workspaces in an unhealthy state. This option isn't recommended for most use cases.", + "dangerZone": { + "dangerZoneHeader": "Danger Zone", + "deleteTemplateHeader": "Delete this template", + "deleteTemplateCaption": "Do you want to permanently delete this template?", + "deleteCta": "Delete Template" + } +} diff --git a/site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx b/site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx index 6891b5267350f..e5c77d4903b0c 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx @@ -24,50 +24,41 @@ import { import * as Yup from "yup" import i18next from "i18next" import { useTranslation } from "react-i18next" +import { Maybe } from "components/Conditionals/Maybe" -export const Language = { - nameLabel: "Name", - descriptionLabel: "Description", - defaultTtlLabel: "Auto-stop default", - iconLabel: "Icon", - formAriaLabel: "Template settings form", - selectEmoji: "Select emoji", - ttlMaxError: - "Please enter a limit that is less than or equal to 168 hours (7 days).", - descriptionMaxError: - "Please enter a description that is less than or equal to 128 characters.", - ttlHelperText: (ttl: number): string => - ttl === 1 - ? `Workspaces created from this template will default to stopping after ${ttl} hour.` - : `Workspaces created from this template will default to stopping after ${ttl} hours.`, - noTTL: - "Workspaces created from this template will run until stopped manually.", +const TTLHelperText = ({ ttl }: { ttl?: number }) => { + const { t } = useTranslation("templateSettingsPage") + const count = typeof ttl !== "number" ? 0 : ttl + return ( + // no helper text if ttl is negative - error will show once field is considered touched + = 0}> + {t("ttlHelperText", { count })} + + ) } -const TTLHelperText = ({ ttl }: { ttl?: number }) => - ttl !== undefined ? ( - {ttl === 0 ? Language.noTTL : Language.ttlHelperText(ttl)} - ) : null - const MAX_DESCRIPTION_CHAR_LIMIT = 128 const MAX_TTL_DAYS = 7 const MS_HOUR_CONVERSION = 3600000 export const validationSchema = Yup.object({ - name: nameValidator(Language.nameLabel), + name: nameValidator(i18next.t("nameLabel", { ns: "templateSettingsPage" })), display_name: templateDisplayNameValidator( i18next.t("displayNameLabel", { - ns: "templatePage", + ns: "templateSettingsPage", }), ), description: Yup.string().max( MAX_DESCRIPTION_CHAR_LIMIT, - Language.descriptionMaxError, + i18next.t("descriptionMaxError", { ns: "templateSettingsPage" }), ), default_ttl_ms: Yup.number() .integer() - .min(0) - .max(24 * MAX_TTL_DAYS /* 7 days in hours */, Language.ttlMaxError), + .min(0, i18next.t("ttlMinError", { ns: "templateSettingsPage" })) + .max( + 24 * MAX_TTL_DAYS /* 7 days in hours */, + i18next.t("ttlMaxError", { ns: "templateSettingsPage" }), + ), allow_user_cancel_workspace_jobs: Yup.boolean(), }) @@ -119,10 +110,10 @@ export const TemplateSettingsForm: FC = ({ const hasIcon = form.values.icon && form.values.icon !== "" const emojiButtonRef = useRef(null) - const { t } = useTranslation("templatePage") + const { t } = useTranslation("templateSettingsPage") return ( - + = ({ onChange={onChangeTrimmed(form)} autoFocus fullWidth - label={Language.nameLabel} + label={t("nameLabel")} variant="outlined" /> @@ -147,7 +138,7 @@ export const TemplateSettingsForm: FC = ({ multiline disabled={isSubmitting} fullWidth - label={Language.descriptionLabel} + label={t("descriptionLabel")} variant="outlined" rows={2} /> @@ -157,7 +148,7 @@ export const TemplateSettingsForm: FC = ({ {...getFieldHelpers("icon")} disabled={isSubmitting} fullWidth - label={Language.iconLabel} + label={t("iconLabel")} variant="outlined" InputProps={{ endAdornment: hasIcon ? ( @@ -186,7 +177,7 @@ export const TemplateSettingsForm: FC = ({ setIsEmojiPickerOpen((v) => !v) }} > - {Language.selectEmoji} + {t("selectEmoji")} = ({ {...getFieldHelpers( "default_ttl_ms", , - "Time until auto-stop", )} disabled={isSubmitting} fullWidth inputProps={{ min: 0, step: 1 }} - label={Language.defaultTtlLabel} + label={t("defaultTtlLabel")} variant="outlined" type="number" /> diff --git a/site/src/pages/TemplateSettingsPage/TemplateSettingsPage.tsx b/site/src/pages/TemplateSettingsPage/TemplateSettingsPage.tsx index e157e63880fa3..21816f6a8780b 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSettingsPage.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSettingsPage.tsx @@ -2,17 +2,15 @@ import { useMachine } from "@xstate/react" import { useOrganizationId } from "hooks/useOrganizationId" import { FC } from "react" import { Helmet } from "react-helmet-async" +import { useTranslation } from "react-i18next" import { useNavigate, useParams } from "react-router-dom" import { pageTitle } from "util/page" import { templateSettingsMachine } from "xServices/templateSettings/templateSettingsXService" import { TemplateSettingsPageView } from "./TemplateSettingsPageView" -const Language = { - title: "Template Settings", -} - export const TemplateSettingsPage: FC = () => { const { template: templateName } = useParams() as { template: string } + const { t } = useTranslation("templateSettingsPage") const navigate = useNavigate() const organizationId = useOrganizationId() const [state, send] = useMachine(templateSettingsMachine, { @@ -34,7 +32,7 @@ export const TemplateSettingsPage: FC = () => { return ( <> - {pageTitle(Language.title)} + {pageTitle(t("title"))} = ({ }) => { const classes = useStyles() const isLoading = !template && !errors.getTemplateError - const { t } = useTranslation("templatePage") + const { t } = useTranslation("templateSettingsPage") if (isDeleted) { return } return ( - + {Boolean(errors.getTemplateError) && ( @@ -78,24 +78,24 @@ export const TemplateSettingsPageView: FC = ({ />
- {t("templateSettings.dangerZone.dangerZoneHeader")} + {t("dangerZone.dangerZoneHeader")}

- {t("templateSettings.dangerZone.deleteTemplateHeader")} + {t("dangerZone.deleteTemplateHeader")}

- {t("templateSettings.dangerZone.deleteTemplateCaption")} + {t("dangerZone.deleteTemplateCaption")}
diff --git a/site/src/util/formUtils.ts b/site/src/util/formUtils.ts index 1aa8ac8584edc..2bbf50800e5ab 100644 --- a/site/src/util/formUtils.ts +++ b/site/src/util/formUtils.ts @@ -14,7 +14,7 @@ import * as Yup from "yup" export const Language = { nameRequired: (name: string): string => { - return `Please enter a ${name.toLowerCase()}.` + return name ? `Please enter a ${name.toLowerCase()}.` : "Required" }, nameInvalidChars: (name: string): string => { return `${name} must start with a-Z or 0-9 and can contain a-Z, 0-9 or -` From 1687370bceab437e9e937667f1726a909fd0d0dc Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Tue, 6 Dec 2022 20:44:33 +0000 Subject: [PATCH 6/8] Fix i18n by thunking schema --- site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx b/site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx index e5c77d4903b0c..5c4dcfb751cbe 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx @@ -41,7 +41,7 @@ const MAX_DESCRIPTION_CHAR_LIMIT = 128 const MAX_TTL_DAYS = 7 const MS_HOUR_CONVERSION = 3600000 -export const validationSchema = Yup.object({ +export const getValidationSchema = (): Yup.AnyObjectSchema => Yup.object({ name: nameValidator(i18next.t("nameLabel", { ns: "templateSettingsPage" })), display_name: templateDisplayNameValidator( i18next.t("displayNameLabel", { @@ -81,6 +81,7 @@ export const TemplateSettingsForm: FC = ({ initialTouched, }) => { const [isEmojiPickerOpen, setIsEmojiPickerOpen] = useState(false) + const validationSchema = getValidationSchema() const form: FormikContextType = useFormik({ initialValues: { From 2a09e4297a6fd90ec6e5701ccb1403bc405eaa9c Mon Sep 17 00:00:00 2001 From: Presley Pizzo Date: Tue, 6 Dec 2022 21:00:30 +0000 Subject: [PATCH 7/8] Fix template settings tests --- .../TemplateSettingsForm.tsx | 39 ++++++++-------- .../TemplateSettingsPage.test.tsx | 45 ++++++++++--------- .../TemplateSettingsPageView.tsx | 4 +- 3 files changed, 46 insertions(+), 42 deletions(-) diff --git a/site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx b/site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx index 5c4dcfb751cbe..193acde566c86 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx @@ -41,26 +41,27 @@ const MAX_DESCRIPTION_CHAR_LIMIT = 128 const MAX_TTL_DAYS = 7 const MS_HOUR_CONVERSION = 3600000 -export const getValidationSchema = (): Yup.AnyObjectSchema => Yup.object({ - name: nameValidator(i18next.t("nameLabel", { ns: "templateSettingsPage" })), - display_name: templateDisplayNameValidator( - i18next.t("displayNameLabel", { - ns: "templateSettingsPage", - }), - ), - description: Yup.string().max( - MAX_DESCRIPTION_CHAR_LIMIT, - i18next.t("descriptionMaxError", { ns: "templateSettingsPage" }), - ), - default_ttl_ms: Yup.number() - .integer() - .min(0, i18next.t("ttlMinError", { ns: "templateSettingsPage" })) - .max( - 24 * MAX_TTL_DAYS /* 7 days in hours */, - i18next.t("ttlMaxError", { ns: "templateSettingsPage" }), +export const getValidationSchema = (): Yup.AnyObjectSchema => + Yup.object({ + name: nameValidator(i18next.t("nameLabel", { ns: "templateSettingsPage" })), + display_name: templateDisplayNameValidator( + i18next.t("displayNameLabel", { + ns: "templateSettingsPage", + }), ), - allow_user_cancel_workspace_jobs: Yup.boolean(), -}) + description: Yup.string().max( + MAX_DESCRIPTION_CHAR_LIMIT, + i18next.t("descriptionMaxError", { ns: "templateSettingsPage" }), + ), + default_ttl_ms: Yup.number() + .integer() + .min(0, i18next.t("ttlMinError", { ns: "templateSettingsPage" })) + .max( + 24 * MAX_TTL_DAYS /* 7 days in hours */, + i18next.t("ttlMaxError", { ns: "templateSettingsPage" }), + ), + allow_user_cancel_workspace_jobs: Yup.boolean(), + }) export interface TemplateSettingsForm { template: Template diff --git a/site/src/pages/TemplateSettingsPage/TemplateSettingsPage.test.tsx b/site/src/pages/TemplateSettingsPage/TemplateSettingsPage.test.tsx index 2aa396cb3b8ca..6795f18b1505c 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSettingsPage.test.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSettingsPage.test.tsx @@ -6,19 +6,21 @@ import { Language as FooterFormLanguage } from "components/FormFooter/FormFooter import { MockTemplate } from "../../testHelpers/entities" import { renderWithAuth } from "../../testHelpers/renderHelpers" import { - Language as FormLanguage, - validationSchema, + getValidationSchema, } from "./TemplateSettingsForm" import { TemplateSettingsPage } from "./TemplateSettingsPage" import i18next from "i18next" +const { t } = i18next + const renderTemplateSettingsPage = async () => { const renderResult = renderWithAuth(, { route: `/templates/${MockTemplate.name}/settings`, path: `/templates/:templateId/settings`, }) // Wait the form to be rendered - await screen.findAllByLabelText(FormLanguage.nameLabel) + const label = t("nameLabel", { ns: "templateSettingsPage" }) + await screen.findAllByLabelText(label) return renderResult } @@ -39,28 +41,31 @@ const fillAndSubmitForm = async ({ icon, allow_user_cancel_workspace_jobs, }: Required) => { - const nameField = await screen.findByLabelText(FormLanguage.nameLabel) + const label = t("nameLabel", { ns: "templateSettingsPage" }) + const nameField = await screen.findByLabelText(label) await userEvent.clear(nameField) await userEvent.type(nameField, name) - const { t } = i18next - const displayNameLabel = t("displayNameLabel", { ns: "templatePage" }) + const displayNameLabel = t("displayNameLabel", { ns: "templateSettingsPage" }) const displayNameField = await screen.findByLabelText(displayNameLabel) await userEvent.clear(displayNameField) await userEvent.type(displayNameField, display_name) + const descriptionLabel = t("descriptionLabel", { ns: "templateSettingsPage" }) const descriptionField = await screen.findByLabelText( - FormLanguage.descriptionLabel, + descriptionLabel, ) await userEvent.clear(descriptionField) await userEvent.type(descriptionField, description) - const iconField = await screen.findByLabelText(FormLanguage.iconLabel) + const iconLabel = t("iconLabel", { ns: "templateSettingsPage" }) + const iconField = await screen.findByLabelText(iconLabel) await userEvent.clear(iconField) await userEvent.type(iconField, icon) - const maxTtlField = await screen.findByLabelText(FormLanguage.defaultTtlLabel) + const defaultTtlLabel = t("defaultTtlLabel", { ns: "templateSettingsPage" }) + const maxTtlField = await screen.findByLabelText(defaultTtlLabel) await userEvent.clear(maxTtlField) await userEvent.type(maxTtlField, default_ttl_ms.toString()) @@ -79,8 +84,8 @@ const fillAndSubmitForm = async ({ describe("TemplateSettingsPage", () => { it("renders", async () => { const { t } = i18next - const pageTitle = t("templateSettings.title", { - ns: "templatePage", + const pageTitle = t("title", { + ns: "templateSettingsPage", }) await renderTemplateSettingsPage() const element = await screen.findByText(pageTitle) @@ -90,8 +95,8 @@ describe("TemplateSettingsPage", () => { it("allows an admin to delete a template", async () => { const { t } = i18next await renderTemplateSettingsPage() - const deleteCta = t("templateSettings.dangerZone.deleteCta", { - ns: "templatePage", + const deleteCta = t("dangerZone.deleteCta", { + ns: "templateSettingsPage", }) const deleteButton = await screen.findByText(deleteCta) expect(deleteButton).toBeDefined() @@ -137,7 +142,7 @@ describe("TemplateSettingsPage", () => { ...validFormValues, default_ttl_ms: 24 * 7, } - const validate = () => validationSchema.validateSync(values) + const validate = () => getValidationSchema().validateSync(values) expect(validate).not.toThrowError() }) @@ -146,7 +151,7 @@ describe("TemplateSettingsPage", () => { ...validFormValues, default_ttl_ms: 0, } - const validate = () => validationSchema.validateSync(values) + const validate = () => getValidationSchema().validateSync(values) expect(validate).not.toThrowError() }) @@ -155,8 +160,8 @@ describe("TemplateSettingsPage", () => { ...validFormValues, default_ttl_ms: 24 * 7 + 1, } - const validate = () => validationSchema.validateSync(values) - expect(validate).toThrowError(FormLanguage.ttlMaxError) + const validate = () => getValidationSchema().validateSync(values) + expect(validate).toThrowError(t("ttlMaxError", { ns: "templateSettingsPage" })) }) it("allows a description of 128 chars", () => { @@ -165,7 +170,7 @@ describe("TemplateSettingsPage", () => { description: "Nam quis nulla. Integer malesuada. In in enim a arcu imperdiet malesuada. Sed vel lectus. Donec odio urna, tempus molestie, port", } - const validate = () => validationSchema.validateSync(values) + const validate = () => getValidationSchema().validateSync(values) expect(validate).not.toThrowError() }) @@ -175,7 +180,7 @@ describe("TemplateSettingsPage", () => { description: "Nam quis nulla. Integer malesuada. In in enim a arcu imperdiet malesuada. Sed vel lectus. Donec odio urna, tempus molestie, port a", } - const validate = () => validationSchema.validateSync(values) - expect(validate).toThrowError(FormLanguage.descriptionMaxError) + const validate = () => getValidationSchema().validateSync(values) + expect(validate).toThrowError(t("descriptionMaxError", { ns: "templateSettingsPage" })) }) }) diff --git a/site/src/pages/TemplateSettingsPage/TemplateSettingsPageView.tsx b/site/src/pages/TemplateSettingsPage/TemplateSettingsPageView.tsx index e9c0ea234bd26..148b8013b2581 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSettingsPageView.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSettingsPageView.tsx @@ -86,9 +86,7 @@ export const TemplateSettingsPageView: FC = ({

{t("dangerZone.deleteTemplateHeader")}

- - {t("dangerZone.deleteTemplateCaption")} - + {t("dangerZone.deleteTemplateCaption")}