diff --git a/cli/templateedit_test.go b/cli/templateedit_test.go index 23b181391b90d..558839de2c9d6 100644 --- a/cli/templateedit_test.go +++ b/cli/templateedit_test.go @@ -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) }) } diff --git a/coderd/templates.go b/coderd/templates.go index d7cdef2a1b123..7f27fb7c2f882 100644 --- a/coderd/templates.go +++ b/coderd/templates.go @@ -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 } diff --git a/site/src/components/TemplateLayout/TemplateLayout.tsx b/site/src/components/TemplateLayout/TemplateLayout.tsx index 7567d572207ef..04bb2d404f364 100644 --- a/site/src/components/TemplateLayout/TemplateLayout.tsx +++ b/site/src/components/TemplateLayout/TemplateLayout.tsx @@ -151,9 +151,12 @@ export const TemplateLayout: FC = ({ children }) => { )} -
- {template.name} + + {template.display_name.length > 0 + ? template.display_name + : template.name} + {template.description === "" ? Language.noDescription diff --git a/site/src/i18n/en/templatePage.json b/site/src/i18n/en/templatePage.json index fcbf8541dc492..8c78adf849901 100644 --- a/site/src/i18n/en/templatePage.json +++ b/site/src/i18n/en/templatePage.json @@ -9,5 +9,6 @@ "deleteTemplateCaption": "Once you delete a template, there is no going back. Please be certain.", "deleteCta": "Delete Template" } - } + }, + "displayNameLabel": "Display name" } diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx index 610603f5fb35a..ed9562351167d 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx @@ -182,7 +182,9 @@ export const CreateWorkspacePageView: FC<
- {props.selectedTemplate.name} + {props.selectedTemplate.display_name.length > 0 + ? props.selectedTemplate.display_name + : props.selectedTemplate.name} {props.selectedTemplate.description && ( diff --git a/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage.test.tsx b/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage.test.tsx index cc36c9595930d..bd2ab35e06ba2 100644 --- a/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage.test.tsx +++ b/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage.test.tsx @@ -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 diff --git a/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage.tsx b/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage.tsx index 7155f1b2d4354..42cbd7dd63ae8 100644 --- a/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage.tsx +++ b/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage.tsx @@ -22,7 +22,15 @@ export const TemplateSummaryPage: FC = () => { return ( <> - {pageTitle(`${template.name} · Template`)} + + {pageTitle( + `${ + template.display_name.length > 0 + ? template.display_name + : template.name + } · Template`, + )} + = ({ const hasIcon = form.values.icon && form.values.icon !== "" const emojiButtonRef = useRef(null) + const { t } = useTranslation("templatePage") + return (
@@ -105,6 +119,14 @@ export const TemplateSettingsForm: FC = ({ variant="outlined" /> + + { 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, ) diff --git a/site/src/pages/TemplatesPage/TemplatesPage.test.tsx b/site/src/pages/TemplatesPage/TemplatesPage.test.tsx index 16cffb1ff8f27..d06091976481e 100644 --- a/site/src/pages/TemplatesPage/TemplatesPage.test.tsx +++ b/site/src/pages/TemplatesPage/TemplatesPage.test.tsx @@ -46,7 +46,7 @@ describe("TemplatesPage", () => { render() // Then - await screen.findByText(MockTemplate.name) + await screen.findByText(MockTemplate.display_name) }) it("shows empty view without permissions to create", async () => { diff --git a/site/src/pages/TemplatesPage/TemplatesPageView.tsx b/site/src/pages/TemplatesPage/TemplatesPageView.tsx index cbfe68d55039c..0a5d103534f5a 100644 --- a/site/src/pages/TemplatesPage/TemplatesPageView.tsx +++ b/site/src/pages/TemplatesPage/TemplatesPageView.tsx @@ -209,7 +209,11 @@ export const TemplatesPageView: FC< > 0 + ? template.display_name + : template.name + } subtitle={template.description} highlightTitle avatar={ diff --git a/site/src/util/formUtils.ts b/site/src/util/formUtils.ts index ab411143f7718..790e070341c5c 100644 --- a/site/src/util/formUtils.ts +++ b/site/src/util/formUtils.ts @@ -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), + )