Show template.display_name in the site UI (#5069)

* Show display_name field in the template settings

* Show template.display_name on pages: Templates, CreateWorkspace

* Fix: template.display_name pattern

* make fmt/prettier

* Fix tests

* Fix: make fmt/prettier

* Fix: merge

* Fix: autoFocus

* i18n: display_name
This commit is contained in:
Marcin Tojek
2022-11-14 21:11:50 +01:00
committed by GitHub
parent e872e18883
commit 49b340e039
12 changed files with 88 additions and 22 deletions

View File

@ -218,8 +218,9 @@ func TestTemplateEdit(t *testing.T) {
// Properties don't change
assert.Equal(t, template.Name, updated.Name)
assert.Equal(t, template.Description, updated.Description)
assert.Equal(t, template.DisplayName, updated.DisplayName)
// Icon is removed, as the API considers it as "delete" request
// These properties are removed, as the API considers it as "delete" request
// See: https://github.com/coder/coder/issues/5066
assert.Equal(t, "", updated.Icon)
assert.Equal(t, "", updated.DisplayName)
})
}

View File

@ -480,7 +480,9 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
return nil
}
// Update template metadata -- empty fields are not overwritten.
// Update template metadata -- empty fields are not overwritten,
// except for display_name, icon, and default_ttl.
// The exception is required to clear content of these fields with UI.
name := req.Name
displayName := req.DisplayName
desc := req.Description
@ -490,9 +492,6 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
if name == "" {
name = template.Name
}
if displayName == "" {
displayName = template.DisplayName
}
if desc == "" {
desc = template.Description
}

View File

@ -151,9 +151,12 @@ export const TemplateLayout: FC<PropsWithChildren> = ({ children }) => {
</Avatar>
)}
</div>
<div>
<PageHeaderTitle>{template.name}</PageHeaderTitle>
<PageHeaderTitle>
{template.display_name.length > 0
? template.display_name
: template.name}
</PageHeaderTitle>
<PageHeaderSubtitle condensed>
{template.description === ""
? Language.noDescription

View File

@ -9,5 +9,6 @@
"deleteTemplateCaption": "Once you delete a template, there is no going back. Please be certain.",
"deleteCta": "Delete Template"
}
}
},
"displayNameLabel": "Display name"
}

View File

@ -182,7 +182,9 @@ export const CreateWorkspacePageView: FC<
</div>
<Stack direction="column" spacing={0.5}>
<span className={styles.templateName}>
{props.selectedTemplate.name}
{props.selectedTemplate.display_name.length > 0
? props.selectedTemplate.display_name
: props.selectedTemplate.name}
</span>
{props.selectedTemplate.description && (
<span className={styles.templateDescription}>

View File

@ -37,7 +37,7 @@ describe("TemplateSummaryPage", () => {
mock.mockImplementation(() => "a minute ago")
renderPage()
await screen.findByText(MockTemplate.name)
await screen.findByText(MockTemplate.display_name)
await screen.findByTestId("markdown")
screen.getByText(MockWorkspaceResource.name)
screen.queryAllByText(`${MockTemplateVersion.name}`).length

View File

@ -22,7 +22,15 @@ export const TemplateSummaryPage: FC = () => {
return (
<>
<Helmet>
<title>{pageTitle(`${template.name} · Template`)}</title>
<title>
{pageTitle(
`${
template.display_name.length > 0
? template.display_name
: template.name
} · Template`,
)}
</title>
</Helmet>
<TemplateSummaryPageView
template={template}

View File

@ -12,8 +12,15 @@ import { Stack } from "components/Stack/Stack"
import { FormikContextType, FormikTouched, useFormik } from "formik"
import { FC, useRef, useState } from "react"
import { colors } from "theme/colors"
import { getFormHelpers, nameValidator, onChangeTrimmed } from "util/formUtils"
import {
getFormHelpers,
nameValidator,
templateDisplayNameValidator,
onChangeTrimmed,
} from "util/formUtils"
import * as Yup from "yup"
import i18next from "i18next"
import { useTranslation } from "react-i18next"
export const Language = {
nameLabel: "Name",
@ -36,6 +43,11 @@ const MS_HOUR_CONVERSION = 3600000
export const validationSchema = Yup.object({
name: nameValidator(Language.nameLabel),
display_name: templateDisplayNameValidator(
i18next.t("displayNameLabel", {
ns: "templatePage",
}),
),
description: Yup.string().max(
MAX_DESCRIPTION_CHAR_LIMIT,
Language.descriptionMaxError,
@ -92,6 +104,8 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
const hasIcon = form.values.icon && form.values.icon !== ""
const emojiButtonRef = useRef<HTMLButtonElement>(null)
const { t } = useTranslation("templatePage")
return (
<form onSubmit={form.handleSubmit} aria-label={Language.formAriaLabel}>
<Stack>
@ -105,6 +119,14 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
variant="outlined"
/>
<TextField
{...getFieldHelpers("display_name")}
disabled={isSubmitting}
fullWidth
label={t("displayNameLabel")}
variant="outlined"
/>
<TextField
{...getFieldHelpers("description")}
multiline

View File

@ -24,7 +24,7 @@ const renderTemplateSettingsPage = async () => {
const validFormValues = {
name: "Name",
display_name: "Test Template",
display_name: "A display name",
description: "A description",
icon: "A string",
default_ttl_ms: 1,
@ -32,6 +32,7 @@ const validFormValues = {
const fillAndSubmitForm = async ({
name,
display_name,
description,
default_ttl_ms,
icon,
@ -40,6 +41,15 @@ const fillAndSubmitForm = async ({
await userEvent.clear(nameField)
await userEvent.type(nameField, name)
const { t } = i18next
const displayNameLabel = t("displayNameLabel", {
ns: "templatePage",
})
const displayNameField = await screen.findByLabelText(displayNameLabel)
await userEvent.clear(displayNameField)
await userEvent.type(displayNameField, display_name)
const descriptionField = await screen.findByLabelText(
FormLanguage.descriptionLabel,
)

View File

@ -46,7 +46,7 @@ describe("TemplatesPage", () => {
render(<TemplatesPage />)
// Then
await screen.findByText(MockTemplate.name)
await screen.findByText(MockTemplate.display_name)
})
it("shows empty view without permissions to create", async () => {

View File

@ -209,7 +209,11 @@ export const TemplatesPageView: FC<
>
<TableCellLink to={templatePageLink}>
<AvatarData
title={template.name}
title={
template.display_name.length > 0
? template.display_name
: template.name
}
subtitle={template.description}
highlightTitle
avatar={

View File

@ -19,8 +19,11 @@ export const Language = {
nameInvalidChars: (name: string): string => {
return `${name} must start with a-Z or 0-9 and can contain a-Z, 0-9 or -`
},
nameTooLong: (name: string): string => {
return `${name} cannot be longer than 32 characters`
nameTooLong: (name: string, len: number): string => {
return `${name} cannot be longer than ${len} characters`
},
templateDisplayNameInvalidChars: (name: string): string => {
return `${name} must start and end with non-whitespace character`
},
}
@ -74,15 +77,28 @@ export const onChangeTrimmed =
form.handleChange(event)
}
// REMARK: Keep in sync with coderd/httpapi/httpapi.go#L40
// REMARK: Keep these consts in sync with coderd/httpapi/httpapi.go
const maxLenName = 32
// REMARK: Keep in sync with coderd/httpapi/httpapi.go#L18
const templateDisplayNameMaxLength = 64
const usernameRE = /^[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*$/
const templateDisplayNameRE = /^[^\s](.*[^\s])?$/
// REMARK: see #1756 for name/username semantics
export const nameValidator = (name: string): Yup.StringSchema =>
Yup.string()
.required(Language.nameRequired(name))
.matches(usernameRE, Language.nameInvalidChars(name))
.max(maxLenName, Language.nameTooLong(name))
.max(maxLenName, Language.nameTooLong(name, maxLenName))
export const templateDisplayNameValidator = (
displayName: string,
): Yup.StringSchema =>
Yup.string()
.matches(
templateDisplayNameRE,
Language.templateDisplayNameInvalidChars(displayName),
)
.max(
templateDisplayNameMaxLength,
Language.nameTooLong(displayName, templateDisplayNameMaxLength),
)