diff --git a/src/panels/config/apps/app-view/config/supervisor-app-config.ts b/src/panels/config/apps/app-view/config/supervisor-app-config.ts index 1f4f7d97875d..d5fa4e9aae7b 100644 --- a/src/panels/config/apps/app-view/config/supervisor-app-config.ts +++ b/src/panels/config/apps/app-view/config/supervisor-app-config.ts @@ -37,6 +37,7 @@ import { haStyle } from "../../../../../resources/styles"; import type { HomeAssistant } from "../../../../../types"; import { supervisorAppsStyle } from "../../resources/supervisor-apps-style"; import { suggestSupervisorAppRestart } from "../dialogs/suggestSupervisorAppRestart"; +import { stringSchemaEntrySelector } from "../util/string-schema-selector"; const SUPPORTED_UI_TYPES = [ "string", @@ -53,8 +54,6 @@ const secretTag = defineScalarTag("!secret", { const ADDON_YAML_SCHEMA = YAML11_SCHEMA.withTags(secretTag); -const MASKED_FIELDS = ["password", "secret", "token"]; - @customElement("supervisor-app-config") class SupervisorAppConfig extends DirtyStateProviderMixin< Record @@ -181,17 +180,7 @@ class SupervisorAppConfig extends DirtyStateProviderMixin< return { select: { options: entry.options, multiple: entry.multiple } }; } if (entry.type === "string") { - return entry.multiple - ? { select: { options: [], multiple: true, custom_value: true } } - : { - text: { - type: entry.format - ? entry.format - : MASKED_FIELDS.includes(entry.name) - ? "password" - : "text", - }, - }; + return stringSchemaEntrySelector(entry); } if (entry.type === "boolean") { return { boolean: {} }; diff --git a/src/panels/config/apps/app-view/util/string-schema-selector.ts b/src/panels/config/apps/app-view/util/string-schema-selector.ts new file mode 100644 index 000000000000..1d3b3963f59a --- /dev/null +++ b/src/panels/config/apps/app-view/util/string-schema-selector.ts @@ -0,0 +1,37 @@ +import type { Selector } from "../../../../../data/selector"; + +export const MASKED_FIELDS = ["password", "secret", "token"]; + +export interface StringSchemaEntry { + name: string; + multiple?: boolean; + format?: "email" | "password" | "url"; + options?: string[]; +} + +export const stringSchemaEntrySelector = ( + entry: StringSchemaEntry +): Selector => { + if (entry.multiple) { + return { + select: { + options: entry.options ?? [], + multiple: true, + custom_value: true, + }, + }; + } + // Single-value fields keep the text selector even when options are + // available: the single-value custom_value picker cannot store an empty + // string (clearing emits undefined, dropping the key on save), which + // would silently change stored-value semantics versus the text field. + return { + text: { + type: entry.format + ? entry.format + : MASKED_FIELDS.includes(entry.name) + ? "password" + : "text", + }, + }; +}; diff --git a/test/panels/config/apps/string-schema-selector.test.ts b/test/panels/config/apps/string-schema-selector.test.ts new file mode 100644 index 000000000000..6d2a6ea0a049 --- /dev/null +++ b/test/panels/config/apps/string-schema-selector.test.ts @@ -0,0 +1,65 @@ +import { describe, expect, it } from "vitest"; +import { stringSchemaEntrySelector } from "../../../../src/panels/config/apps/app-view/util/string-schema-selector"; + +describe("stringSchemaEntrySelector", () => { + it("renders multi-value fields as a multi-select with supervisor options", () => { + expect( + stringSchemaEntrySelector({ + name: "enabled_shares", + multiple: true, + options: ["addons", "backup", "config"], + }) + ).toEqual({ + select: { + options: ["addons", "backup", "config"], + multiple: true, + custom_value: true, + }, + }); + }); + + it("renders multi-value fields without options as an empty multi-select", () => { + expect( + stringSchemaEntrySelector({ name: "veto_files", multiple: true }) + ).toEqual({ + select: { options: [], multiple: true, custom_value: true }, + }); + }); + + it("keeps single-value fields as text even when options are present", () => { + expect( + stringSchemaEntrySelector({ + name: "share_on_port", + options: ["443", "8443", "10000"], + }) + ).toEqual({ text: { type: "text" } }); + }); + + it("keeps masked fields as password text even when options are present", () => { + expect( + stringSchemaEntrySelector({ name: "token", options: ["a", "b"] }) + ).toEqual({ text: { type: "password" } }); + }); + + it("keeps format-typed fields as text even when options are present", () => { + expect( + stringSchemaEntrySelector({ + name: "contact", + format: "email", + options: ["a@b.c"], + }) + ).toEqual({ text: { type: "email" } }); + }); + + it("renders plain single-value fields without options as text", () => { + expect(stringSchemaEntrySelector({ name: "workgroup" })).toEqual({ + text: { type: "text" }, + }); + }); + + it("renders masked fields without options as password text", () => { + expect(stringSchemaEntrySelector({ name: "password" })).toEqual({ + text: { type: "password" }, + }); + }); +});