Skip to content

Commit 49b340e

Browse files
authored
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
1 parent e872e18 commit 49b340e

File tree

12 files changed

+88
-22
lines changed

12 files changed

+88
-22
lines changed

cli/templateedit_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -218,8 +218,9 @@ func TestTemplateEdit(t *testing.T) {
218218
// Properties don't change
219219
assert.Equal(t, template.Name, updated.Name)
220220
assert.Equal(t, template.Description, updated.Description)
221-
assert.Equal(t, template.DisplayName, updated.DisplayName)
222-
// Icon is removed, as the API considers it as "delete" request
221+
// These properties are removed, as the API considers it as "delete" request
222+
// See: https://github.com/coder/coder/issues/5066
223223
assert.Equal(t, "", updated.Icon)
224+
assert.Equal(t, "", updated.DisplayName)
224225
})
225226
}

coderd/templates.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -480,7 +480,9 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
480480
return nil
481481
}
482482

483-
// Update template metadata -- empty fields are not overwritten.
483+
// Update template metadata -- empty fields are not overwritten,
484+
// except for display_name, icon, and default_ttl.
485+
// The exception is required to clear content of these fields with UI.
484486
name := req.Name
485487
displayName := req.DisplayName
486488
desc := req.Description
@@ -490,9 +492,6 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
490492
if name == "" {
491493
name = template.Name
492494
}
493-
if displayName == "" {
494-
displayName = template.DisplayName
495-
}
496495
if desc == "" {
497496
desc = template.Description
498497
}

site/src/components/TemplateLayout/TemplateLayout.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,9 +151,12 @@ export const TemplateLayout: FC<PropsWithChildren> = ({ children }) => {
151151
</Avatar>
152152
)}
153153
</div>
154-
155154
<div>
156-
<PageHeaderTitle>{template.name}</PageHeaderTitle>
155+
<PageHeaderTitle>
156+
{template.display_name.length > 0
157+
? template.display_name
158+
: template.name}
159+
</PageHeaderTitle>
157160
<PageHeaderSubtitle condensed>
158161
{template.description === ""
159162
? Language.noDescription

site/src/i18n/en/templatePage.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@
99
"deleteTemplateCaption": "Once you delete a template, there is no going back. Please be certain.",
1010
"deleteCta": "Delete Template"
1111
}
12-
}
12+
},
13+
"displayNameLabel": "Display name"
1314
}

site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,9 @@ export const CreateWorkspacePageView: FC<
182182
</div>
183183
<Stack direction="column" spacing={0.5}>
184184
<span className={styles.templateName}>
185-
{props.selectedTemplate.name}
185+
{props.selectedTemplate.display_name.length > 0
186+
? props.selectedTemplate.display_name
187+
: props.selectedTemplate.name}
186188
</span>
187189
{props.selectedTemplate.description && (
188190
<span className={styles.templateDescription}>

site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ describe("TemplateSummaryPage", () => {
3737
mock.mockImplementation(() => "a minute ago")
3838

3939
renderPage()
40-
await screen.findByText(MockTemplate.name)
40+
await screen.findByText(MockTemplate.display_name)
4141
await screen.findByTestId("markdown")
4242
screen.getByText(MockWorkspaceResource.name)
4343
screen.queryAllByText(`${MockTemplateVersion.name}`).length

site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,15 @@ export const TemplateSummaryPage: FC = () => {
2222
return (
2323
<>
2424
<Helmet>
25-
<title>{pageTitle(`${template.name} · Template`)}</title>
25+
<title>
26+
{pageTitle(
27+
`${
28+
template.display_name.length > 0
29+
? template.display_name
30+
: template.name
31+
} · Template`,
32+
)}
33+
</title>
2634
</Helmet>
2735
<TemplateSummaryPageView
2836
template={template}

site/src/pages/TemplateSettingsPage/TemplateSettingsForm.tsx

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,15 @@ import { Stack } from "components/Stack/Stack"
1212
import { FormikContextType, FormikTouched, useFormik } from "formik"
1313
import { FC, useRef, useState } from "react"
1414
import { colors } from "theme/colors"
15-
import { getFormHelpers, nameValidator, onChangeTrimmed } from "util/formUtils"
15+
import {
16+
getFormHelpers,
17+
nameValidator,
18+
templateDisplayNameValidator,
19+
onChangeTrimmed,
20+
} from "util/formUtils"
1621
import * as Yup from "yup"
22+
import i18next from "i18next"
23+
import { useTranslation } from "react-i18next"
1724

1825
export const Language = {
1926
nameLabel: "Name",
@@ -36,6 +43,11 @@ const MS_HOUR_CONVERSION = 3600000
3643

3744
export const validationSchema = Yup.object({
3845
name: nameValidator(Language.nameLabel),
46+
display_name: templateDisplayNameValidator(
47+
i18next.t("displayNameLabel", {
48+
ns: "templatePage",
49+
}),
50+
),
3951
description: Yup.string().max(
4052
MAX_DESCRIPTION_CHAR_LIMIT,
4153
Language.descriptionMaxError,
@@ -92,6 +104,8 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
92104
const hasIcon = form.values.icon && form.values.icon !== ""
93105
const emojiButtonRef = useRef<HTMLButtonElement>(null)
94106

107+
const { t } = useTranslation("templatePage")
108+
95109
return (
96110
<form onSubmit={form.handleSubmit} aria-label={Language.formAriaLabel}>
97111
<Stack>
@@ -105,6 +119,14 @@ export const TemplateSettingsForm: FC<TemplateSettingsForm> = ({
105119
variant="outlined"
106120
/>
107121

122+
<TextField
123+
{...getFieldHelpers("display_name")}
124+
disabled={isSubmitting}
125+
fullWidth
126+
label={t("displayNameLabel")}
127+
variant="outlined"
128+
/>
129+
108130
<TextField
109131
{...getFieldHelpers("description")}
110132
multiline

site/src/pages/TemplateSettingsPage/TemplateSettingsPage.test.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,15 @@ const renderTemplateSettingsPage = async () => {
2424

2525
const validFormValues = {
2626
name: "Name",
27-
display_name: "Test Template",
27+
display_name: "A display name",
2828
description: "A description",
2929
icon: "A string",
3030
default_ttl_ms: 1,
3131
}
3232

3333
const fillAndSubmitForm = async ({
3434
name,
35+
display_name,
3536
description,
3637
default_ttl_ms,
3738
icon,
@@ -40,6 +41,15 @@ const fillAndSubmitForm = async ({
4041
await userEvent.clear(nameField)
4142
await userEvent.type(nameField, name)
4243

44+
const { t } = i18next
45+
const displayNameLabel = t("displayNameLabel", {
46+
ns: "templatePage",
47+
})
48+
49+
const displayNameField = await screen.findByLabelText(displayNameLabel)
50+
await userEvent.clear(displayNameField)
51+
await userEvent.type(displayNameField, display_name)
52+
4353
const descriptionField = await screen.findByLabelText(
4454
FormLanguage.descriptionLabel,
4555
)

site/src/pages/TemplatesPage/TemplatesPage.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ describe("TemplatesPage", () => {
4646
render(<TemplatesPage />)
4747

4848
// Then
49-
await screen.findByText(MockTemplate.name)
49+
await screen.findByText(MockTemplate.display_name)
5050
})
5151

5252
it("shows empty view without permissions to create", async () => {

site/src/pages/TemplatesPage/TemplatesPageView.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,11 @@ export const TemplatesPageView: FC<
209209
>
210210
<TableCellLink to={templatePageLink}>
211211
<AvatarData
212-
title={template.name}
212+
title={
213+
template.display_name.length > 0
214+
? template.display_name
215+
: template.name
216+
}
213217
subtitle={template.description}
214218
highlightTitle
215219
avatar={

site/src/util/formUtils.ts

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,11 @@ export const Language = {
1919
nameInvalidChars: (name: string): string => {
2020
return `${name} must start with a-Z or 0-9 and can contain a-Z, 0-9 or -`
2121
},
22-
nameTooLong: (name: string): string => {
23-
return `${name} cannot be longer than 32 characters`
22+
nameTooLong: (name: string, len: number): string => {
23+
return `${name} cannot be longer than ${len} characters`
24+
},
25+
templateDisplayNameInvalidChars: (name: string): string => {
26+
return `${name} must start and end with non-whitespace character`
2427
},
2528
}
2629

@@ -74,15 +77,28 @@ export const onChangeTrimmed =
7477
form.handleChange(event)
7578
}
7679

77-
// REMARK: Keep in sync with coderd/httpapi/httpapi.go#L40
80+
// REMARK: Keep these consts in sync with coderd/httpapi/httpapi.go
7881
const maxLenName = 32
79-
80-
// REMARK: Keep in sync with coderd/httpapi/httpapi.go#L18
82+
const templateDisplayNameMaxLength = 64
8183
const usernameRE = /^[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*$/
84+
const templateDisplayNameRE = /^[^\s](.*[^\s])?$/
8285

8386
// REMARK: see #1756 for name/username semantics
8487
export const nameValidator = (name: string): Yup.StringSchema =>
8588
Yup.string()
8689
.required(Language.nameRequired(name))
8790
.matches(usernameRE, Language.nameInvalidChars(name))
88-
.max(maxLenName, Language.nameTooLong(name))
91+
.max(maxLenName, Language.nameTooLong(name, maxLenName))
92+
93+
export const templateDisplayNameValidator = (
94+
displayName: string,
95+
): Yup.StringSchema =>
96+
Yup.string()
97+
.matches(
98+
templateDisplayNameRE,
99+
Language.templateDisplayNameInvalidChars(displayName),
100+
)
101+
.max(
102+
templateDisplayNameMaxLength,
103+
Language.nameTooLong(displayName, templateDisplayNameMaxLength),
104+
)

0 commit comments

Comments
 (0)