diff --git a/site/src/pages/TemplateBuilder/BaseTemplateParametersStep.tsx b/site/src/pages/TemplateBuilder/BaseTemplateParametersStep.tsx index b5f01f0763..b23cd716c1 100644 --- a/site/src/pages/TemplateBuilder/BaseTemplateParametersStep.tsx +++ b/site/src/pages/TemplateBuilder/BaseTemplateParametersStep.tsx @@ -11,7 +11,11 @@ import { TemplateBuilderTitle, } from "#/pages/TemplateBuilder/TemplateBuilderHeader"; import { cn } from "#/utils/cn"; -import type { ConfigurationFieldDefinition } from "./ConfigurationField"; +import { + type ConfigurationFieldDefinition, + ConfigurationFieldLabel, +} from "./ConfigurationField"; +import { defaultPlaceholder } from "./defaultPlaceholder"; import { TemplateConfiguration } from "./TemplateConfiguration"; interface BaseTemplateParametersStepProps { @@ -34,12 +38,13 @@ function variableToField( onChange: (name: string, value: string) => void, ): ConfigurationFieldDefinition { const id = `base-var-${variable.name}`; + const label = ; if (variable.type === "bool") { return { type: "switch", id, - label: variable.name, + label, description: variable.description || undefined, required: variable.required, checked: value === "true", @@ -51,10 +56,12 @@ function variableToField( return { type: "text", id, - label: variable.name, + label, description: variable.description || undefined, required: variable.required, - placeholder: variable.required ? "Required" : "Optional", + placeholder: + defaultPlaceholder(variable.default) ?? + (variable.required ? "Required" : "Optional"), field: { name: variable.name, id, diff --git a/site/src/pages/TemplateBuilder/ConfigurationField.tsx b/site/src/pages/TemplateBuilder/ConfigurationField.tsx index 0e7124d13a..b8e68550bf 100644 --- a/site/src/pages/TemplateBuilder/ConfigurationField.tsx +++ b/site/src/pages/TemplateBuilder/ConfigurationField.tsx @@ -1,4 +1,5 @@ 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"; @@ -122,13 +123,15 @@ const SelectField: FC = ({ {label} - {required && ( + {required ? ( <> {" "} * > + ) : ( + )} {description && ( @@ -170,13 +173,15 @@ const RadioField: FC = ({ {label} - {required && ( + {required ? ( <> {" "} * > + ) : ( + )} {description && ( @@ -293,13 +298,15 @@ const SwitchGroupField: FC = ({ {label} - {required && ( + {required ? ( <> {" "} * > + ) : ( + )} {description && ( @@ -335,3 +342,23 @@ export const ConfigurationFieldContainer: FC = ({ ); }; + +const OptionalIndicator: FC = () => { + return ( + <> + {" "} + (optional) + > + ); +}; + +export const ConfigurationFieldLabel: FC<{ + variable: TemplateBuilderModuleVariable; +}> = ({ variable }) => { + return ( + <> + {variable.name} + {!variable.required && } + > + ); +}; diff --git a/site/src/pages/TemplateBuilder/ModuleSettingsStep.tsx b/site/src/pages/TemplateBuilder/ModuleSettingsStep.tsx index 428182a1a5..d1a720d8d1 100644 --- a/site/src/pages/TemplateBuilder/ModuleSettingsStep.tsx +++ b/site/src/pages/TemplateBuilder/ModuleSettingsStep.tsx @@ -11,7 +11,11 @@ import { TemplateBuilderSubtitle, TemplateBuilderTitle, } from "#/pages/TemplateBuilder/TemplateBuilderHeader"; -import type { ConfigurationFieldDefinition } from "./ConfigurationField"; +import { + type ConfigurationFieldDefinition, + ConfigurationFieldLabel, +} from "./ConfigurationField"; +import { defaultPlaceholder } from "./defaultPlaceholder"; import { ModuleConfiguration } from "./ModuleConfiguration"; interface ModuleSettingsStepProps { @@ -31,12 +35,13 @@ function variableToField( onChange: (name: string, value: string) => void, ): ConfigurationFieldDefinition { const id = `mod-${moduleId}-${variable.name}`; + const label = ; if (variable.type === "bool") { return { type: "switch", id, - label: variable.name, + label, description: variable.description || undefined, required: variable.required, checked: value === "true", @@ -48,10 +53,12 @@ function variableToField( return { type: "text", id, - label: variable.name, + label, description: variable.description || undefined, required: variable.required, - placeholder: variable.required ? "Required" : "Optional", + placeholder: + defaultPlaceholder(variable.default) ?? + (variable.required ? "Required" : "Optional"), field: { name: variable.name, id, diff --git a/site/src/pages/TemplateBuilder/defaultPlaceholder.test.ts b/site/src/pages/TemplateBuilder/defaultPlaceholder.test.ts new file mode 100644 index 0000000000..90eef2c455 --- /dev/null +++ b/site/src/pages/TemplateBuilder/defaultPlaceholder.test.ts @@ -0,0 +1,28 @@ +import { defaultPlaceholder } from "./defaultPlaceholder"; + +describe("defaultPlaceholder", () => { + it("formats numeric defaults, including zero", () => { + expect(defaultPlaceholder(13337)).toBe("13337"); + expect(defaultPlaceholder(0)).toBe("0"); + }); + + it("formats boolean defaults", () => { + expect(defaultPlaceholder(false)).toBe("false"); + expect(defaultPlaceholder(true)).toBe("true"); + }); + + it("returns non-empty string defaults unchanged", () => { + expect(defaultPlaceholder("v0.10.0")).toBe("v0.10.0"); + }); + + it("returns undefined for empty or absent defaults", () => { + expect(defaultPlaceholder("")).toBeUndefined(); + expect(defaultPlaceholder(undefined)).toBeUndefined(); + expect(defaultPlaceholder(null)).toBeUndefined(); + }); + + it("returns undefined for object and array defaults", () => { + expect(defaultPlaceholder({ key: "value" })).toBeUndefined(); + expect(defaultPlaceholder(["a", "b"])).toBeUndefined(); + }); +}); diff --git a/site/src/pages/TemplateBuilder/defaultPlaceholder.ts b/site/src/pages/TemplateBuilder/defaultPlaceholder.ts new file mode 100644 index 0000000000..9370ad64a5 --- /dev/null +++ b/site/src/pages/TemplateBuilder/defaultPlaceholder.ts @@ -0,0 +1,22 @@ +/** + * Coerces a module variable's `default` into placeholder text. + * + * `TemplateBuilderModuleVariable.default` is generated as + * `Record` because every `json.RawMessage` field maps to that + * type, but the actual wire value is a JSON scalar (string, number, or + * boolean). The parameter is typed `unknown` so the generated value is accepted + * without a cast. + * + * An empty-string default returns `undefined` so callers fall back to their + * existing Required/Optional hint. Object or array defaults also return + * `undefined` since they have no meaningful placeholder representation. + */ +export function defaultPlaceholder(value: unknown): string | undefined { + if (typeof value === "number" || typeof value === "boolean") { + return String(value); + } + if (typeof value === "string" && value !== "") { + return value; + } + return undefined; +}