mirror of
https://github.com/coder/coder.git
synced 2025-07-13 21:36:50 +00:00
fix: UX issues in template settings form's default auto-stop field (#5330)
* Fix helper text - handles 0 ttl - uses helper text typography - pluralizes - still doesn't override error (once considered touched) * Show user friendly field name in error text * Format * Override label through Yup instead * Switch to i18n - wip * Fix i18n by thunking schema * Fix template settings tests * Replace third arg to getFieldHelpers -is used after all
This commit is contained in:
@ -8,6 +8,7 @@ import agent from "./agent.json"
|
|||||||
import buildPage from "./buildPage.json"
|
import buildPage from "./buildPage.json"
|
||||||
import workspacesPage from "./workspacesPage.json"
|
import workspacesPage from "./workspacesPage.json"
|
||||||
import usersPage from "./usersPage.json"
|
import usersPage from "./usersPage.json"
|
||||||
|
import templateSettingsPage from "./templateSettingsPage.json"
|
||||||
import templateVersionPage from "./templateVersionPage.json"
|
import templateVersionPage from "./templateVersionPage.json"
|
||||||
import loginPage from "./loginPage.json"
|
import loginPage from "./loginPage.json"
|
||||||
import workspaceChangeVersionPage from "./workspaceChangeVersionPage.json"
|
import workspaceChangeVersionPage from "./workspaceChangeVersionPage.json"
|
||||||
@ -24,6 +25,7 @@ export const en = {
|
|||||||
buildPage,
|
buildPage,
|
||||||
workspacesPage,
|
workspacesPage,
|
||||||
usersPage,
|
usersPage,
|
||||||
|
templateSettingsPage,
|
||||||
templateVersionPage,
|
templateVersionPage,
|
||||||
loginPage,
|
loginPage,
|
||||||
workspaceChangeVersionPage,
|
workspaceChangeVersionPage,
|
||||||
|
@ -1,16 +1,4 @@
|
|||||||
{
|
{
|
||||||
"deleteSuccess": "Template successfully deleted.",
|
"deleteSuccess": "Template successfully deleted.",
|
||||||
"createdVersion": "created the version",
|
"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."
|
|
||||||
}
|
}
|
||||||
|
24
site/src/i18n/en/templateSettingsPage.json
Normal file
24
site/src/i18n/en/templateSettingsPage.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
}
|
@ -24,43 +24,44 @@ import {
|
|||||||
import * as Yup from "yup"
|
import * as Yup from "yup"
|
||||||
import i18next from "i18next"
|
import i18next from "i18next"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { Maybe } from "components/Conditionals/Maybe"
|
||||||
|
|
||||||
export const Language = {
|
const TTLHelperText = ({ ttl }: { ttl?: number }) => {
|
||||||
nameLabel: "Name",
|
const { t } = useTranslation("templateSettingsPage")
|
||||||
descriptionLabel: "Description",
|
const count = typeof ttl !== "number" ? 0 : ttl
|
||||||
defaultTtlLabel: "Auto-stop default",
|
return (
|
||||||
iconLabel: "Icon",
|
// no helper text if ttl is negative - error will show once field is considered touched
|
||||||
formAriaLabel: "Template settings form",
|
<Maybe condition={count >= 0}>
|
||||||
selectEmoji: "Select emoji",
|
<span>{t("ttlHelperText", { count })}</span>
|
||||||
ttlMaxError:
|
</Maybe>
|
||||||
"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 =>
|
|
||||||
`Workspaces created from this template will default to stopping after ${ttl} hours.`,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_DESCRIPTION_CHAR_LIMIT = 128
|
const MAX_DESCRIPTION_CHAR_LIMIT = 128
|
||||||
const MAX_TTL_DAYS = 7
|
const MAX_TTL_DAYS = 7
|
||||||
const MS_HOUR_CONVERSION = 3600000
|
const MS_HOUR_CONVERSION = 3600000
|
||||||
|
|
||||||
export const validationSchema = Yup.object({
|
export const getValidationSchema = (): Yup.AnyObjectSchema =>
|
||||||
name: nameValidator(Language.nameLabel),
|
Yup.object({
|
||||||
display_name: templateDisplayNameValidator(
|
name: nameValidator(i18next.t("nameLabel", { ns: "templateSettingsPage" })),
|
||||||
i18next.t("displayNameLabel", {
|
display_name: templateDisplayNameValidator(
|
||||||
ns: "templatePage",
|
i18next.t("displayNameLabel", {
|
||||||
}),
|
ns: "templateSettingsPage",
|
||||||
),
|
}),
|
||||||
description: Yup.string().max(
|
),
|
||||||
MAX_DESCRIPTION_CHAR_LIMIT,
|
description: Yup.string().max(
|
||||||
Language.descriptionMaxError,
|
MAX_DESCRIPTION_CHAR_LIMIT,
|
||||||
),
|
i18next.t("descriptionMaxError", { ns: "templateSettingsPage" }),
|
||||||
default_ttl_ms: Yup.number()
|
),
|
||||||
.integer()
|
default_ttl_ms: Yup.number()
|
||||||
.min(0)
|
.integer()
|
||||||
.max(24 * MAX_TTL_DAYS /* 7 days in hours */, Language.ttlMaxError),
|
.min(0, i18next.t("ttlMinError", { ns: "templateSettingsPage" }))
|
||||||
allow_user_cancel_workspace_jobs: Yup.boolean(),
|
.max(
|
||||||
})
|
24 * MAX_TTL_DAYS /* 7 days in hours */,
|
||||||
|
i18next.t("ttlMaxError", { ns: "templateSettingsPage" }),
|
||||||
|
),
|
||||||
|
allow_user_cancel_workspace_jobs: Yup.boolean(),
|
||||||
|
})
|
||||||
|
|
||||||
export interface TemplateSettingsForm {
|
export interface TemplateSettingsForm {
|
||||||
template: Template
|
template: Template
|
||||||
@ -81,6 +82,7 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
|
|||||||
initialTouched,
|
initialTouched,
|
||||||
}) => {
|
}) => {
|
||||||
const [isEmojiPickerOpen, setIsEmojiPickerOpen] = useState(false)
|
const [isEmojiPickerOpen, setIsEmojiPickerOpen] = useState(false)
|
||||||
|
const validationSchema = getValidationSchema()
|
||||||
const form: FormikContextType<UpdateTemplateMeta> =
|
const form: FormikContextType<UpdateTemplateMeta> =
|
||||||
useFormik<UpdateTemplateMeta>({
|
useFormik<UpdateTemplateMeta>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
@ -110,10 +112,10 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
|
|||||||
const hasIcon = form.values.icon && form.values.icon !== ""
|
const hasIcon = form.values.icon && form.values.icon !== ""
|
||||||
const emojiButtonRef = useRef<HTMLButtonElement>(null)
|
const emojiButtonRef = useRef<HTMLButtonElement>(null)
|
||||||
|
|
||||||
const { t } = useTranslation("templatePage")
|
const { t } = useTranslation("templateSettingsPage")
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={form.handleSubmit} aria-label={Language.formAriaLabel}>
|
<form onSubmit={form.handleSubmit} aria-label={t("formAriaLabel")}>
|
||||||
<Stack>
|
<Stack>
|
||||||
<TextField
|
<TextField
|
||||||
{...getFieldHelpers("name")}
|
{...getFieldHelpers("name")}
|
||||||
@ -121,7 +123,7 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
|
|||||||
onChange={onChangeTrimmed(form)}
|
onChange={onChangeTrimmed(form)}
|
||||||
autoFocus
|
autoFocus
|
||||||
fullWidth
|
fullWidth
|
||||||
label={Language.nameLabel}
|
label={t("nameLabel")}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -138,7 +140,7 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
|
|||||||
multiline
|
multiline
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
fullWidth
|
fullWidth
|
||||||
label={Language.descriptionLabel}
|
label={t("descriptionLabel")}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
rows={2}
|
rows={2}
|
||||||
/>
|
/>
|
||||||
@ -148,7 +150,7 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
|
|||||||
{...getFieldHelpers("icon")}
|
{...getFieldHelpers("icon")}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
fullWidth
|
fullWidth
|
||||||
label={Language.iconLabel}
|
label={t("iconLabel")}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
InputProps={{
|
InputProps={{
|
||||||
endAdornment: hasIcon ? (
|
endAdornment: hasIcon ? (
|
||||||
@ -177,7 +179,7 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
|
|||||||
setIsEmojiPickerOpen((v) => !v)
|
setIsEmojiPickerOpen((v) => !v)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{Language.selectEmoji}
|
{t("selectEmoji")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Popover
|
<Popover
|
||||||
@ -204,20 +206,17 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
{...getFieldHelpers("default_ttl_ms")}
|
{...getFieldHelpers(
|
||||||
|
"default_ttl_ms",
|
||||||
|
<TTLHelperText ttl={form.values.default_ttl_ms} />,
|
||||||
|
)}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
fullWidth
|
fullWidth
|
||||||
inputProps={{ min: 0, step: 1 }}
|
inputProps={{ min: 0, step: 1 }}
|
||||||
label={Language.defaultTtlLabel}
|
label={t("defaultTtlLabel")}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
type="number"
|
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 && (
|
|
||||||
<span>{Language.ttlHelperText(form.values.default_ttl_ms)}</span>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Box display="flex">
|
<Box display="flex">
|
||||||
<div>
|
<div>
|
||||||
|
@ -5,20 +5,20 @@ import { UpdateTemplateMeta } from "api/typesGenerated"
|
|||||||
import { Language as FooterFormLanguage } from "components/FormFooter/FormFooter"
|
import { Language as FooterFormLanguage } from "components/FormFooter/FormFooter"
|
||||||
import { MockTemplate } from "../../testHelpers/entities"
|
import { MockTemplate } from "../../testHelpers/entities"
|
||||||
import { renderWithAuth } from "../../testHelpers/renderHelpers"
|
import { renderWithAuth } from "../../testHelpers/renderHelpers"
|
||||||
import {
|
import { getValidationSchema } from "./TemplateSettingsForm"
|
||||||
Language as FormLanguage,
|
|
||||||
validationSchema,
|
|
||||||
} from "./TemplateSettingsForm"
|
|
||||||
import { TemplateSettingsPage } from "./TemplateSettingsPage"
|
import { TemplateSettingsPage } from "./TemplateSettingsPage"
|
||||||
import i18next from "i18next"
|
import i18next from "i18next"
|
||||||
|
|
||||||
|
const { t } = i18next
|
||||||
|
|
||||||
const renderTemplateSettingsPage = async () => {
|
const renderTemplateSettingsPage = async () => {
|
||||||
const renderResult = renderWithAuth(<TemplateSettingsPage />, {
|
const renderResult = renderWithAuth(<TemplateSettingsPage />, {
|
||||||
route: `/templates/${MockTemplate.name}/settings`,
|
route: `/templates/${MockTemplate.name}/settings`,
|
||||||
path: `/templates/:templateId/settings`,
|
path: `/templates/:templateId/settings`,
|
||||||
})
|
})
|
||||||
// Wait the form to be rendered
|
// Wait the form to be rendered
|
||||||
await screen.findAllByLabelText(FormLanguage.nameLabel)
|
const label = t("nameLabel", { ns: "templateSettingsPage" })
|
||||||
|
await screen.findAllByLabelText(label)
|
||||||
return renderResult
|
return renderResult
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,28 +39,29 @@ const fillAndSubmitForm = async ({
|
|||||||
icon,
|
icon,
|
||||||
allow_user_cancel_workspace_jobs,
|
allow_user_cancel_workspace_jobs,
|
||||||
}: Required<UpdateTemplateMeta>) => {
|
}: Required<UpdateTemplateMeta>) => {
|
||||||
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.clear(nameField)
|
||||||
await userEvent.type(nameField, name)
|
await userEvent.type(nameField, name)
|
||||||
|
|
||||||
const { t } = i18next
|
const displayNameLabel = t("displayNameLabel", { ns: "templateSettingsPage" })
|
||||||
const displayNameLabel = t("displayNameLabel", { ns: "templatePage" })
|
|
||||||
|
|
||||||
const displayNameField = await screen.findByLabelText(displayNameLabel)
|
const displayNameField = await screen.findByLabelText(displayNameLabel)
|
||||||
await userEvent.clear(displayNameField)
|
await userEvent.clear(displayNameField)
|
||||||
await userEvent.type(displayNameField, display_name)
|
await userEvent.type(displayNameField, display_name)
|
||||||
|
|
||||||
const descriptionField = await screen.findByLabelText(
|
const descriptionLabel = t("descriptionLabel", { ns: "templateSettingsPage" })
|
||||||
FormLanguage.descriptionLabel,
|
const descriptionField = await screen.findByLabelText(descriptionLabel)
|
||||||
)
|
|
||||||
await userEvent.clear(descriptionField)
|
await userEvent.clear(descriptionField)
|
||||||
await userEvent.type(descriptionField, description)
|
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.clear(iconField)
|
||||||
await userEvent.type(iconField, icon)
|
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.clear(maxTtlField)
|
||||||
await userEvent.type(maxTtlField, default_ttl_ms.toString())
|
await userEvent.type(maxTtlField, default_ttl_ms.toString())
|
||||||
|
|
||||||
@ -79,8 +80,8 @@ const fillAndSubmitForm = async ({
|
|||||||
describe("TemplateSettingsPage", () => {
|
describe("TemplateSettingsPage", () => {
|
||||||
it("renders", async () => {
|
it("renders", async () => {
|
||||||
const { t } = i18next
|
const { t } = i18next
|
||||||
const pageTitle = t("templateSettings.title", {
|
const pageTitle = t("title", {
|
||||||
ns: "templatePage",
|
ns: "templateSettingsPage",
|
||||||
})
|
})
|
||||||
await renderTemplateSettingsPage()
|
await renderTemplateSettingsPage()
|
||||||
const element = await screen.findByText(pageTitle)
|
const element = await screen.findByText(pageTitle)
|
||||||
@ -90,8 +91,8 @@ describe("TemplateSettingsPage", () => {
|
|||||||
it("allows an admin to delete a template", async () => {
|
it("allows an admin to delete a template", async () => {
|
||||||
const { t } = i18next
|
const { t } = i18next
|
||||||
await renderTemplateSettingsPage()
|
await renderTemplateSettingsPage()
|
||||||
const deleteCta = t("templateSettings.dangerZone.deleteCta", {
|
const deleteCta = t("dangerZone.deleteCta", {
|
||||||
ns: "templatePage",
|
ns: "templateSettingsPage",
|
||||||
})
|
})
|
||||||
const deleteButton = await screen.findByText(deleteCta)
|
const deleteButton = await screen.findByText(deleteCta)
|
||||||
expect(deleteButton).toBeDefined()
|
expect(deleteButton).toBeDefined()
|
||||||
@ -137,7 +138,7 @@ describe("TemplateSettingsPage", () => {
|
|||||||
...validFormValues,
|
...validFormValues,
|
||||||
default_ttl_ms: 24 * 7,
|
default_ttl_ms: 24 * 7,
|
||||||
}
|
}
|
||||||
const validate = () => validationSchema.validateSync(values)
|
const validate = () => getValidationSchema().validateSync(values)
|
||||||
expect(validate).not.toThrowError()
|
expect(validate).not.toThrowError()
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -146,7 +147,7 @@ describe("TemplateSettingsPage", () => {
|
|||||||
...validFormValues,
|
...validFormValues,
|
||||||
default_ttl_ms: 0,
|
default_ttl_ms: 0,
|
||||||
}
|
}
|
||||||
const validate = () => validationSchema.validateSync(values)
|
const validate = () => getValidationSchema().validateSync(values)
|
||||||
expect(validate).not.toThrowError()
|
expect(validate).not.toThrowError()
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -155,8 +156,10 @@ describe("TemplateSettingsPage", () => {
|
|||||||
...validFormValues,
|
...validFormValues,
|
||||||
default_ttl_ms: 24 * 7 + 1,
|
default_ttl_ms: 24 * 7 + 1,
|
||||||
}
|
}
|
||||||
const validate = () => validationSchema.validateSync(values)
|
const validate = () => getValidationSchema().validateSync(values)
|
||||||
expect(validate).toThrowError(FormLanguage.ttlMaxError)
|
expect(validate).toThrowError(
|
||||||
|
t("ttlMaxError", { ns: "templateSettingsPage" }),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("allows a description of 128 chars", () => {
|
it("allows a description of 128 chars", () => {
|
||||||
@ -165,7 +168,7 @@ describe("TemplateSettingsPage", () => {
|
|||||||
description:
|
description:
|
||||||
"Nam quis nulla. Integer malesuada. In in enim a arcu imperdiet malesuada. Sed vel lectus. Donec odio urna, tempus molestie, port",
|
"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()
|
expect(validate).not.toThrowError()
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -175,7 +178,9 @@ describe("TemplateSettingsPage", () => {
|
|||||||
description:
|
description:
|
||||||
"Nam quis nulla. Integer malesuada. In in enim a arcu imperdiet malesuada. Sed vel lectus. Donec odio urna, tempus molestie, port a",
|
"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)
|
const validate = () => getValidationSchema().validateSync(values)
|
||||||
expect(validate).toThrowError(FormLanguage.descriptionMaxError)
|
expect(validate).toThrowError(
|
||||||
|
t("descriptionMaxError", { ns: "templateSettingsPage" }),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -2,17 +2,15 @@ import { useMachine } from "@xstate/react"
|
|||||||
import { useOrganizationId } from "hooks/useOrganizationId"
|
import { useOrganizationId } from "hooks/useOrganizationId"
|
||||||
import { FC } from "react"
|
import { FC } from "react"
|
||||||
import { Helmet } from "react-helmet-async"
|
import { Helmet } from "react-helmet-async"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
import { useNavigate, useParams } from "react-router-dom"
|
import { useNavigate, useParams } from "react-router-dom"
|
||||||
import { pageTitle } from "util/page"
|
import { pageTitle } from "util/page"
|
||||||
import { templateSettingsMachine } from "xServices/templateSettings/templateSettingsXService"
|
import { templateSettingsMachine } from "xServices/templateSettings/templateSettingsXService"
|
||||||
import { TemplateSettingsPageView } from "./TemplateSettingsPageView"
|
import { TemplateSettingsPageView } from "./TemplateSettingsPageView"
|
||||||
|
|
||||||
const Language = {
|
|
||||||
title: "Template Settings",
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TemplateSettingsPage: FC = () => {
|
export const TemplateSettingsPage: FC = () => {
|
||||||
const { template: templateName } = useParams() as { template: string }
|
const { template: templateName } = useParams() as { template: string }
|
||||||
|
const { t } = useTranslation("templateSettingsPage")
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const organizationId = useOrganizationId()
|
const organizationId = useOrganizationId()
|
||||||
const [state, send] = useMachine(templateSettingsMachine, {
|
const [state, send] = useMachine(templateSettingsMachine, {
|
||||||
@ -34,7 +32,7 @@ export const TemplateSettingsPage: FC = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>{pageTitle(Language.title)}</title>
|
<title>{pageTitle(t("title"))}</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<TemplateSettingsPageView
|
<TemplateSettingsPageView
|
||||||
isSubmitting={state.hasTag("submitting")}
|
isSubmitting={state.hasTag("submitting")}
|
||||||
|
@ -47,14 +47,14 @@ export const TemplateSettingsPageView: FC<TemplateSettingsPageViewProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
const isLoading = !template && !errors.getTemplateError
|
const isLoading = !template && !errors.getTemplateError
|
||||||
const { t } = useTranslation("templatePage")
|
const { t } = useTranslation("templateSettingsPage")
|
||||||
|
|
||||||
if (isDeleted) {
|
if (isDeleted) {
|
||||||
return <Navigate to="/templates" />
|
return <Navigate to="/templates" />
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FullPageForm title={t("templateSettings.title")} onCancel={onCancel}>
|
<FullPageForm title={t("title")} onCancel={onCancel}>
|
||||||
{Boolean(errors.getTemplateError) && (
|
{Boolean(errors.getTemplateError) && (
|
||||||
<Stack className={classes.errorContainer}>
|
<Stack className={classes.errorContainer}>
|
||||||
<AlertBanner severity="error" error={errors.getTemplateError} />
|
<AlertBanner severity="error" error={errors.getTemplateError} />
|
||||||
@ -78,24 +78,22 @@ export const TemplateSettingsPageView: FC<TemplateSettingsPageViewProps> = ({
|
|||||||
/>
|
/>
|
||||||
<Stack className={classes.dangerContainer}>
|
<Stack className={classes.dangerContainer}>
|
||||||
<div className={classes.dangerHeader}>
|
<div className={classes.dangerHeader}>
|
||||||
{t("templateSettings.dangerZone.dangerZoneHeader")}
|
{t("dangerZone.dangerZoneHeader")}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Stack className={classes.dangerBorder}>
|
<Stack className={classes.dangerBorder}>
|
||||||
<Stack spacing={0}>
|
<Stack spacing={0}>
|
||||||
<p className={classes.deleteTemplateHeader}>
|
<p className={classes.deleteTemplateHeader}>
|
||||||
{t("templateSettings.dangerZone.deleteTemplateHeader")}
|
{t("dangerZone.deleteTemplateHeader")}
|
||||||
</p>
|
</p>
|
||||||
<span>
|
<span>{t("dangerZone.deleteTemplateCaption")}</span>
|
||||||
{t("templateSettings.dangerZone.deleteTemplateCaption")}
|
|
||||||
</span>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
<Button
|
<Button
|
||||||
className={classes.deleteButton}
|
className={classes.deleteButton}
|
||||||
onClick={onDelete}
|
onClick={onDelete}
|
||||||
aria-label={t("templateSettings.dangerZone.deleteCta")}
|
aria-label={t("dangerZone.deleteCta")}
|
||||||
>
|
>
|
||||||
{t("templateSettings.dangerZone.deleteCta")}
|
{t("dangerZone.deleteCta")}
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
@ -14,7 +14,7 @@ import * as Yup from "yup"
|
|||||||
|
|
||||||
export const Language = {
|
export const Language = {
|
||||||
nameRequired: (name: string): string => {
|
nameRequired: (name: string): string => {
|
||||||
return `Please enter a ${name.toLowerCase()}.`
|
return name ? `Please enter a ${name.toLowerCase()}.` : "Required"
|
||||||
},
|
},
|
||||||
nameInvalidChars: (name: string): string => {
|
nameInvalidChars: (name: string): string => {
|
||||||
return `${name} must start with a-Z or 0-9 and can contain a-Z, 0-9 or -`
|
return `${name} must start with a-Z or 0-9 and can contain a-Z, 0-9 or -`
|
||||||
@ -37,7 +37,6 @@ interface FormHelpers {
|
|||||||
helperText?: ReactNode
|
helperText?: ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
// backendErrorName can be used if the backend names a field differently than the frontend does
|
|
||||||
export const getFormHelpers =
|
export const getFormHelpers =
|
||||||
<T>(form: FormikContextType<T>, error?: Error | unknown) =>
|
<T>(form: FormikContextType<T>, error?: Error | unknown) =>
|
||||||
(
|
(
|
||||||
@ -54,6 +53,7 @@ export const getFormHelpers =
|
|||||||
`name must be type of string, instead received '${typeof name}'`,
|
`name must be type of string, instead received '${typeof name}'`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiErrorName = backendErrorName ?? name
|
const apiErrorName = backendErrorName ?? name
|
||||||
|
|
||||||
// getIn is a util function from Formik that gets at any depth of nesting
|
// getIn is a util function from Formik that gets at any depth of nesting
|
||||||
|
Reference in New Issue
Block a user