mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat(site): use template/module config variable's default value as its field placeholder if present (#26880)
closes DEVEX-543
Previously, templates'/modules' config variables' input fields would
just display Required/Optional as placeholder text. Now we show the
field's default value as a placeholder if defined
([example](https://github.com/coder/coder/blob/496c0576ca6bfbf3ba4b8d470cd320549f7fc6d1/coderd/templatebuilder/modules/aider/module.json#L30)),
falling back to Required/Optional if there's no default defined.
599b607194 co-authored with Claude Code
This commit is contained in:
@@ -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 = <ConfigurationFieldLabel variable={variable} />;
|
||||
|
||||
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,
|
||||
|
||||
@@ -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<SelectFieldDefinition> = ({
|
||||
<div className="!col-end-1 flex flex-col gap-2">
|
||||
<Label htmlFor={id}>
|
||||
{label}
|
||||
{required && (
|
||||
{required ? (
|
||||
<>
|
||||
{" "}
|
||||
<span className="text-sm font-bold text-content-destructive">
|
||||
*
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<OptionalIndicator />
|
||||
)}
|
||||
</Label>
|
||||
{description && (
|
||||
@@ -170,13 +173,15 @@ const RadioField: FC<RadioFieldDefinition> = ({
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label id={labelId}>
|
||||
{label}
|
||||
{required && (
|
||||
{required ? (
|
||||
<>
|
||||
{" "}
|
||||
<span className="text-sm font-bold text-content-destructive">
|
||||
*
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<OptionalIndicator />
|
||||
)}
|
||||
</Label>
|
||||
{description && (
|
||||
@@ -293,13 +298,15 @@ const SwitchGroupField: FC<SwitchGroupFieldDefinition> = ({
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label id={labelId}>
|
||||
{label}
|
||||
{required && (
|
||||
{required ? (
|
||||
<>
|
||||
{" "}
|
||||
<span className="text-sm font-bold text-content-destructive">
|
||||
*
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<OptionalIndicator />
|
||||
)}
|
||||
</Label>
|
||||
{description && (
|
||||
@@ -335,3 +342,23 @@ export const ConfigurationFieldContainer: FC<PropsWithChildren> = ({
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const OptionalIndicator: FC = () => {
|
||||
return (
|
||||
<>
|
||||
{" "}
|
||||
<span className="text-content-secondary">(optional)</span>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const ConfigurationFieldLabel: FC<{
|
||||
variable: TemplateBuilderModuleVariable;
|
||||
}> = ({ variable }) => {
|
||||
return (
|
||||
<>
|
||||
{variable.name}
|
||||
{!variable.required && <OptionalIndicator />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 = <ConfigurationFieldLabel variable={variable} />;
|
||||
|
||||
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,
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Coerces a module variable's `default` into placeholder text.
|
||||
*
|
||||
* `TemplateBuilderModuleVariable.default` is generated as
|
||||
* `Record<string, string>` 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;
|
||||
}
|
||||
Reference in New Issue
Block a user