mirror of
https://github.com/coder/coder.git
synced 2025-07-13 21:36:50 +00:00
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:
@ -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)
|
||||
})
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -9,5 +9,6 @@
|
||||
"deleteTemplateCaption": "Once you delete a template, there is no going back. Please be certain.",
|
||||
"deleteCta": "Delete Template"
|
||||
}
|
||||
}
|
||||
},
|
||||
"displayNameLabel": "Display name"
|
||||
}
|
||||
|
@ -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}>
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
)
|
||||
|
@ -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 () => {
|
||||
|
@ -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={
|
||||
|
@ -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),
|
||||
)
|
||||
|
Reference in New Issue
Block a user