From 0292fdd083513fc72355a2d8d040cb84f95721ce Mon Sep 17 00:00:00 2001 From: Andrew Aquino Date: Thu, 3 Sep 2026 02:19:33 +0000 Subject: [PATCH 1/8] feat(site/src/pages/TemplateBuilder): overhaul module field placeholders Remove the '(optional)' indicator from optional template builder fields and stop writing 'Optional' into placeholders. Add a per-module placeholder override table (getModuleFieldPlaceholder) with precedence override -> default -> 'Required'/'', and dim placeholder text via content-disabled. Isolated slice of #27077 (item 4). DEVEX-830. --- .../BaseTemplateParametersStep.tsx | 2 +- .../TemplateBuilder/ConfigurationField.tsx | 32 ++++--------- .../TemplateBuilder/ModuleSettingsStep.tsx | 4 +- .../moduleFieldPlaceholders.test.ts | 45 ++++++++++++++++++ .../moduleFieldPlaceholders.ts | 46 +++++++++++++++++++ 5 files changed, 103 insertions(+), 26 deletions(-) create mode 100644 site/src/pages/TemplateBuilder/moduleFieldPlaceholders.test.ts create mode 100644 site/src/pages/TemplateBuilder/moduleFieldPlaceholders.ts diff --git a/site/src/pages/TemplateBuilder/BaseTemplateParametersStep.tsx b/site/src/pages/TemplateBuilder/BaseTemplateParametersStep.tsx index 34c547b04a9..e9590a5a26d 100644 --- a/site/src/pages/TemplateBuilder/BaseTemplateParametersStep.tsx +++ b/site/src/pages/TemplateBuilder/BaseTemplateParametersStep.tsx @@ -61,7 +61,7 @@ function variableToField( required: variable.required, placeholder: defaultPlaceholder(variable.default) ?? - (variable.required ? "Required" : "Optional"), + (variable.required ? "Required" : ""), field: { name: variable.name, id, diff --git a/site/src/pages/TemplateBuilder/ConfigurationField.tsx b/site/src/pages/TemplateBuilder/ConfigurationField.tsx index 4b00f78e715..75abd973d0e 100644 --- a/site/src/pages/TemplateBuilder/ConfigurationField.tsx +++ b/site/src/pages/TemplateBuilder/ConfigurationField.tsx @@ -104,6 +104,10 @@ const TextField: FC = ({ description={description} required={required} placeholder={placeholder} + // Placeholder text is instructional (a hint of what to enter), so it + // uses the dimmer content-disabled tone instead of the default + // content-secondary. The important flag beats the base Input class. + className="placeholder:!text-content-disabled" /> ); @@ -123,15 +127,13 @@ const SelectField: FC = ({
{description && ( @@ -173,15 +175,13 @@ const RadioField: FC = ({
{description && ( @@ -298,15 +298,13 @@ const SwitchGroupField: FC = ({
{description && ( @@ -343,22 +341,8 @@ export const ConfigurationFieldContainer: FC = ({ ); }; -const OptionalIndicator: FC = () => { - return ( - <> - {" "} - (optional) - - ); -}; - export const ConfigurationFieldLabel: FC<{ variable: TemplateBuilderModuleVariable; }> = ({ variable }) => { - return ( - <> - {variable.name} - {!variable.required && } - - ); + return <>{variable.name}; }; diff --git a/site/src/pages/TemplateBuilder/ModuleSettingsStep.tsx b/site/src/pages/TemplateBuilder/ModuleSettingsStep.tsx index ed844dd1d8b..a9b0fb5b7ac 100644 --- a/site/src/pages/TemplateBuilder/ModuleSettingsStep.tsx +++ b/site/src/pages/TemplateBuilder/ModuleSettingsStep.tsx @@ -17,6 +17,7 @@ import { } from "./ConfigurationField"; import { defaultPlaceholder } from "./defaultPlaceholder"; import { ModuleConfiguration } from "./ModuleConfiguration"; +import { getModuleFieldPlaceholder } from "./moduleFieldPlaceholders"; interface ModuleSettingsStepProps { baseId: string; @@ -59,8 +60,9 @@ function variableToField( description: variable.description || undefined, required: variable.required, placeholder: + getModuleFieldPlaceholder(moduleId, variable.name) ?? defaultPlaceholder(variable.default) ?? - (variable.required ? "Required" : "Optional"), + (variable.required ? "Required" : ""), field: { name: variable.name, id, diff --git a/site/src/pages/TemplateBuilder/moduleFieldPlaceholders.test.ts b/site/src/pages/TemplateBuilder/moduleFieldPlaceholders.test.ts new file mode 100644 index 00000000000..d7bfa6a0c45 --- /dev/null +++ b/site/src/pages/TemplateBuilder/moduleFieldPlaceholders.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from "vitest"; +import { + getModuleFieldPlaceholder, + MODULE_FIELD_PLACEHOLDERS, +} from "./moduleFieldPlaceholders"; + +describe("getModuleFieldPlaceholder", () => { + it("returns the override for known module/variable pairs", () => { + expect(getModuleFieldPlaceholder("codex", "model_reasoning_effort")).toBe( + "none, minimal, low, medium, high, or xhigh", + ); + expect(getModuleFieldPlaceholder("claude-code", "anthropic_api_key")).toBe( + "sk-ant-...", + ); + expect(getModuleFieldPlaceholder("claude-code", "model")).toBe( + "e.g. claude-sonnet-5", + ); + expect(getModuleFieldPlaceholder("git-clone", "base_dir")).toBe("$HOME"); + expect(getModuleFieldPlaceholder("git-clone", "branch_name")).toBe( + "Default branch", + ); + expect(getModuleFieldPlaceholder("dotfiles", "description")).toBe( + "e.g https://dotfiles.github.io or an SSH URL git@host:user/repo", + ); + }); + + it("returns undefined for a module without overrides", () => { + expect(getModuleFieldPlaceholder("code-server", "port")).toBeUndefined(); + }); + + it("returns undefined for an unmatched variable on a known module", () => { + expect( + getModuleFieldPlaceholder("codex", "does_not_exist"), + ).toBeUndefined(); + }); + + it("exports the full override table", () => { + expect(Object.keys(MODULE_FIELD_PLACEHOLDERS).sort()).toEqual([ + "claude-code", + "codex", + "dotfiles", + "git-clone", + ]); + }); +}); diff --git a/site/src/pages/TemplateBuilder/moduleFieldPlaceholders.ts b/site/src/pages/TemplateBuilder/moduleFieldPlaceholders.ts new file mode 100644 index 00000000000..531ec37ec9d --- /dev/null +++ b/site/src/pages/TemplateBuilder/moduleFieldPlaceholders.ts @@ -0,0 +1,46 @@ +/** + * Per-module placeholder text overrides for the template builder's + * module configuration fields. + * + * The API delivers each module's variable defaults verbatim from its + * `module.json`. Some of those defaults are prose that is more useful as + * documentation than as placeholder text (for example the dotfiles + * `description` default is a two-sentence explanation). This table lets + * us substitute short, actionable placeholder hints for a specific set + * of variables without changing the underlying module manifest. + * + * Lookup order used by the module configuration step: + * 1. Override in this table (if present). + * 2. Variable's `default` value. + * 3. `Required` when the variable is required, otherwise empty. + */ +export const MODULE_FIELD_PLACEHOLDERS: Readonly< + Record>> +> = { + codex: { + model_reasoning_effort: "none, minimal, low, medium, high, or xhigh", + }, + "claude-code": { + anthropic_api_key: "sk-ant-...", + model: "e.g. claude-sonnet-5", + }, + "git-clone": { + base_dir: "$HOME", + branch_name: "Default branch", + }, + dotfiles: { + description: + "e.g https://dotfiles.github.io or an SSH URL git@host:user/repo", + }, +}; + +/** + * Returns the placeholder override for the given module and variable, + * or `undefined` when no override exists. + */ +export function getModuleFieldPlaceholder( + moduleId: string, + variableName: string, +): string | undefined { + return MODULE_FIELD_PLACEHOLDERS[moduleId]?.[variableName]; +} From d045dee912f75f816d4d598e9117991aae18765b Mon Sep 17 00:00:00 2001 From: Andrew Aquino Date: Thu, 3 Sep 2026 02:48:04 +0000 Subject: [PATCH 2/8] refactor: rm ConfigurationFieldLabel component --- .../pages/TemplateBuilder/BaseTemplateParametersStep.tsx | 7 ++----- site/src/pages/TemplateBuilder/ConfigurationField.tsx | 7 ------- site/src/pages/TemplateBuilder/ModuleSettingsStep.tsx | 7 ++----- 3 files changed, 4 insertions(+), 17 deletions(-) diff --git a/site/src/pages/TemplateBuilder/BaseTemplateParametersStep.tsx b/site/src/pages/TemplateBuilder/BaseTemplateParametersStep.tsx index e9590a5a26d..a3ec873c9a8 100644 --- a/site/src/pages/TemplateBuilder/BaseTemplateParametersStep.tsx +++ b/site/src/pages/TemplateBuilder/BaseTemplateParametersStep.tsx @@ -11,10 +11,7 @@ import { TemplateBuilderTitle, } from "#/pages/TemplateBuilder/TemplateBuilderHeader"; import { cn } from "#/utils/cn"; -import { - type ConfigurationFieldDefinition, - ConfigurationFieldLabel, -} from "./ConfigurationField"; +import { type ConfigurationFieldDefinition } from "./ConfigurationField"; import { defaultPlaceholder } from "./defaultPlaceholder"; import { TemplateConfiguration } from "./TemplateConfiguration"; @@ -38,7 +35,7 @@ function variableToField( onChange: (name: string, value: string) => void, ): ConfigurationFieldDefinition { const id = `base-var-${variable.name}`; - const label = ; + const label = variable.name; if (variable.type === "bool") { return { diff --git a/site/src/pages/TemplateBuilder/ConfigurationField.tsx b/site/src/pages/TemplateBuilder/ConfigurationField.tsx index 75abd973d0e..37ca07091af 100644 --- a/site/src/pages/TemplateBuilder/ConfigurationField.tsx +++ b/site/src/pages/TemplateBuilder/ConfigurationField.tsx @@ -1,5 +1,4 @@ import type { FC, PropsWithChildren, ReactNode } from "react"; -import type { TemplateBuilderModuleVariable } from "#/api/typesGenerated"; import { FormField } from "#/components/FormField/FormField"; import { Label } from "#/components/Label/Label"; import { RadioGroup, RadioGroupItem } from "#/components/RadioGroup/RadioGroup"; @@ -340,9 +339,3 @@ export const ConfigurationFieldContainer: FC = ({
); }; - -export const ConfigurationFieldLabel: FC<{ - variable: TemplateBuilderModuleVariable; -}> = ({ variable }) => { - return <>{variable.name}; -}; diff --git a/site/src/pages/TemplateBuilder/ModuleSettingsStep.tsx b/site/src/pages/TemplateBuilder/ModuleSettingsStep.tsx index a9b0fb5b7ac..8e701b1174a 100644 --- a/site/src/pages/TemplateBuilder/ModuleSettingsStep.tsx +++ b/site/src/pages/TemplateBuilder/ModuleSettingsStep.tsx @@ -11,10 +11,7 @@ import { TemplateBuilderSubtitle, TemplateBuilderTitle, } from "#/pages/TemplateBuilder/TemplateBuilderHeader"; -import { - type ConfigurationFieldDefinition, - ConfigurationFieldLabel, -} from "./ConfigurationField"; +import { type ConfigurationFieldDefinition } from "./ConfigurationField"; import { defaultPlaceholder } from "./defaultPlaceholder"; import { ModuleConfiguration } from "./ModuleConfiguration"; import { getModuleFieldPlaceholder } from "./moduleFieldPlaceholders"; @@ -38,7 +35,7 @@ function variableToField( onChange: (name: string, value: string) => void, ): ConfigurationFieldDefinition { const id = `mod-${moduleId}-${variable.name}`; - const label = ; + const label = variable.name; if (variable.type === "bool") { return { From 6324b753f99b8e7173945906743e09225b8ee009 Mon Sep 17 00:00:00 2001 From: Andrew Aquino Date: Thu, 3 Sep 2026 02:53:29 +0000 Subject: [PATCH 3/8] style: lint:fix --- site/src/pages/TemplateBuilder/BaseTemplateParametersStep.tsx | 2 +- site/src/pages/TemplateBuilder/ModuleSettingsStep.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/site/src/pages/TemplateBuilder/BaseTemplateParametersStep.tsx b/site/src/pages/TemplateBuilder/BaseTemplateParametersStep.tsx index a3ec873c9a8..d8760ff8202 100644 --- a/site/src/pages/TemplateBuilder/BaseTemplateParametersStep.tsx +++ b/site/src/pages/TemplateBuilder/BaseTemplateParametersStep.tsx @@ -11,7 +11,7 @@ import { TemplateBuilderTitle, } from "#/pages/TemplateBuilder/TemplateBuilderHeader"; import { cn } from "#/utils/cn"; -import { type ConfigurationFieldDefinition } from "./ConfigurationField"; +import type { ConfigurationFieldDefinition } from "./ConfigurationField"; import { defaultPlaceholder } from "./defaultPlaceholder"; import { TemplateConfiguration } from "./TemplateConfiguration"; diff --git a/site/src/pages/TemplateBuilder/ModuleSettingsStep.tsx b/site/src/pages/TemplateBuilder/ModuleSettingsStep.tsx index 8e701b1174a..40271b1084e 100644 --- a/site/src/pages/TemplateBuilder/ModuleSettingsStep.tsx +++ b/site/src/pages/TemplateBuilder/ModuleSettingsStep.tsx @@ -11,7 +11,7 @@ import { TemplateBuilderSubtitle, TemplateBuilderTitle, } from "#/pages/TemplateBuilder/TemplateBuilderHeader"; -import { type ConfigurationFieldDefinition } from "./ConfigurationField"; +import type { ConfigurationFieldDefinition } from "./ConfigurationField"; import { defaultPlaceholder } from "./defaultPlaceholder"; import { ModuleConfiguration } from "./ModuleConfiguration"; import { getModuleFieldPlaceholder } from "./moduleFieldPlaceholders"; From 6ecf128013c640e913bf927876774fd9d8debbaa Mon Sep 17 00:00:00 2001 From: Andrew Aquino Date: Thu, 10 Sep 2026 01:15:19 +0000 Subject: [PATCH 4/8] refactor: remove unnecessary important and yap comment --- site/src/pages/TemplateBuilder/ConfigurationField.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/site/src/pages/TemplateBuilder/ConfigurationField.tsx b/site/src/pages/TemplateBuilder/ConfigurationField.tsx index 37ca07091af..263f5f7c497 100644 --- a/site/src/pages/TemplateBuilder/ConfigurationField.tsx +++ b/site/src/pages/TemplateBuilder/ConfigurationField.tsx @@ -103,10 +103,7 @@ const TextField: FC = ({ description={description} required={required} placeholder={placeholder} - // Placeholder text is instructional (a hint of what to enter), so it - // uses the dimmer content-disabled tone instead of the default - // content-secondary. The important flag beats the base Input class. - className="placeholder:!text-content-disabled" + className="placeholder:text-content-disabled" /> ); From 9cd61ae2581490757636fe3d5d75b7d6842189b8 Mon Sep 17 00:00:00 2001 From: Andrew Aquino Date: Thu, 10 Sep 2026 01:19:57 +0000 Subject: [PATCH 5/8] test: remove unnecessary test --- .../TemplateBuilder/moduleFieldPlaceholders.test.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/site/src/pages/TemplateBuilder/moduleFieldPlaceholders.test.ts b/site/src/pages/TemplateBuilder/moduleFieldPlaceholders.test.ts index d7bfa6a0c45..b61d9c472f7 100644 --- a/site/src/pages/TemplateBuilder/moduleFieldPlaceholders.test.ts +++ b/site/src/pages/TemplateBuilder/moduleFieldPlaceholders.test.ts @@ -33,13 +33,4 @@ describe("getModuleFieldPlaceholder", () => { getModuleFieldPlaceholder("codex", "does_not_exist"), ).toBeUndefined(); }); - - it("exports the full override table", () => { - expect(Object.keys(MODULE_FIELD_PLACEHOLDERS).sort()).toEqual([ - "claude-code", - "codex", - "dotfiles", - "git-clone", - ]); - }); }); From 9e97f333f72cfbeb24ce576ee4dab47b18319422 Mon Sep 17 00:00:00 2001 From: Andrew Aquino Date: Thu, 10 Sep 2026 01:20:29 +0000 Subject: [PATCH 6/8] test: rm unused import --- .../pages/TemplateBuilder/moduleFieldPlaceholders.test.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/site/src/pages/TemplateBuilder/moduleFieldPlaceholders.test.ts b/site/src/pages/TemplateBuilder/moduleFieldPlaceholders.test.ts index b61d9c472f7..23ab0760c6f 100644 --- a/site/src/pages/TemplateBuilder/moduleFieldPlaceholders.test.ts +++ b/site/src/pages/TemplateBuilder/moduleFieldPlaceholders.test.ts @@ -1,8 +1,5 @@ import { describe, expect, it } from "vitest"; -import { - getModuleFieldPlaceholder, - MODULE_FIELD_PLACEHOLDERS, -} from "./moduleFieldPlaceholders"; +import { getModuleFieldPlaceholder } from "./moduleFieldPlaceholders"; describe("getModuleFieldPlaceholder", () => { it("returns the override for known module/variable pairs", () => { From f1efb118c699e4739e116cdae019dc9b4a50aa85 Mon Sep 17 00:00:00 2001 From: Andrew Aquino Date: Thu, 10 Sep 2026 01:22:26 +0000 Subject: [PATCH 7/8] chore: rm yap comment --- site/src/pages/TemplateBuilder/moduleFieldPlaceholders.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/site/src/pages/TemplateBuilder/moduleFieldPlaceholders.ts b/site/src/pages/TemplateBuilder/moduleFieldPlaceholders.ts index 531ec37ec9d..deca5fb82d2 100644 --- a/site/src/pages/TemplateBuilder/moduleFieldPlaceholders.ts +++ b/site/src/pages/TemplateBuilder/moduleFieldPlaceholders.ts @@ -34,10 +34,6 @@ export const MODULE_FIELD_PLACEHOLDERS: Readonly< }, }; -/** - * Returns the placeholder override for the given module and variable, - * or `undefined` when no override exists. - */ export function getModuleFieldPlaceholder( moduleId: string, variableName: string, From f281e7c4414c2e5d89479d069601628978b67787 Mon Sep 17 00:00:00 2001 From: Andrew Aquino Date: Thu, 10 Sep 2026 16:49:37 +0000 Subject: [PATCH 8/8] refactor: don't export MODULE_FIELD_PLACEHOLDERS --- site/src/pages/TemplateBuilder/moduleFieldPlaceholders.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/pages/TemplateBuilder/moduleFieldPlaceholders.ts b/site/src/pages/TemplateBuilder/moduleFieldPlaceholders.ts index deca5fb82d2..e3a73f14f44 100644 --- a/site/src/pages/TemplateBuilder/moduleFieldPlaceholders.ts +++ b/site/src/pages/TemplateBuilder/moduleFieldPlaceholders.ts @@ -14,7 +14,7 @@ * 2. Variable's `default` value. * 3. `Required` when the variable is required, otherwise empty. */ -export const MODULE_FIELD_PLACEHOLDERS: Readonly< +const MODULE_FIELD_PLACEHOLDERS: Readonly< Record>> > = { codex: {