Skip to content

Commit 3889d71

Browse files
authored
Improve addon configuration UI (#26997)
* Replace deeply nested ternary operators with if-return chain * Extract _convertSchemaElement from _convertSchema * Extract _convertSchemaElementToSelector from _convertSchemaElement * Add force parameter to _convertSchemaElementToSelector * Add UI editor for (lists of) nested dicts in addon configs * Render top-level dicts to expandable sections * Use correct translation keys for nested fields in addon configs * Restructure translation keys for nested addon config fields
1 parent 8872adf commit 3889d71

File tree

2 files changed

+121
-64
lines changed

2 files changed

+121
-64
lines changed

hassio/src/addon-view/config/hassio-addon-config.ts

Lines changed: 114 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ import "../../../../src/components/ha-alert";
1111
import "../../../../src/components/ha-button-menu";
1212
import "../../../../src/components/ha-card";
1313
import "../../../../src/components/ha-form/ha-form";
14-
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
14+
import type {
15+
HaFormSchema,
16+
HaFormDataContainer,
17+
} from "../../../../src/components/ha-form/types";
1518
import "../../../../src/components/ha-formfield";
1619
import "../../../../src/components/ha-icon-button";
1720
import "../../../../src/components/ha-list-item";
@@ -33,6 +36,7 @@ import { haStyle } from "../../../../src/resources/styles";
3336
import type { HomeAssistant } from "../../../../src/types";
3437
import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart";
3538
import { hassioStyle } from "../../resources/hassio-style";
39+
import type { ObjectSelector, Selector } from "../../../../src/data/selector";
3640

3741
const SUPPORTED_UI_TYPES = [
3842
"string",
@@ -78,77 +82,124 @@ class HassioAddonConfig extends LitElement {
7882

7983
@query("ha-yaml-editor") private _editor?: HaYamlEditor;
8084

81-
public computeLabel = (entry: HaFormSchema): string =>
82-
this.addon.translations[this.hass.language]?.configuration?.[entry.name]
83-
?.name ||
84-
this.addon.translations.en?.configuration?.[entry.name]?.name ||
85+
private _getTranslationEntry(
86+
language: string,
87+
entry: HaFormSchema,
88+
options?: { path?: string[] }
89+
) {
90+
let parent = this.addon.translations[language]?.configuration;
91+
if (!parent) return undefined;
92+
if (options?.path) {
93+
for (const key of options.path) {
94+
parent = parent[key]?.fields;
95+
if (!parent) return undefined;
96+
}
97+
}
98+
return parent[entry.name];
99+
}
100+
101+
public computeLabel = (
102+
entry: HaFormSchema,
103+
_data: HaFormDataContainer,
104+
options?: { path?: string[] }
105+
): string =>
106+
this._getTranslationEntry(this.hass.language, entry, options)?.name ||
107+
this._getTranslationEntry("en", entry, options)?.name ||
85108
entry.name;
86109

87-
public computeHelper = (entry: HaFormSchema): string =>
88-
this.addon.translations[this.hass.language]?.configuration?.[entry.name]
110+
public computeHelper = (
111+
entry: HaFormSchema,
112+
options?: { path?: string[] }
113+
): string =>
114+
this._getTranslationEntry(this.hass.language, entry, options)
89115
?.description ||
90-
this.addon.translations.en?.configuration?.[entry.name]?.description ||
116+
this._getTranslationEntry("en", entry, options)?.description ||
91117
"";
92118

93119
private _convertSchema = memoizeOne(
94120
// Convert supervisor schema to selectors
95-
(schema: Record<string, any>): HaFormSchema[] =>
96-
schema.map((entry) =>
97-
entry.type === "select"
98-
? {
99-
name: entry.name,
100-
required: entry.required,
101-
selector: { select: { options: entry.options } },
102-
}
103-
: entry.type === "string"
104-
? entry.multiple
105-
? {
106-
name: entry.name,
107-
required: entry.required,
108-
selector: {
109-
select: { options: [], multiple: true, custom_value: true },
110-
},
111-
}
112-
: {
113-
name: entry.name,
114-
required: entry.required,
115-
selector: {
116-
text: {
117-
type: entry.format
118-
? entry.format
119-
: MASKED_FIELDS.includes(entry.name)
120-
? "password"
121-
: "text",
122-
},
123-
},
124-
}
125-
: entry.type === "boolean"
126-
? {
127-
name: entry.name,
128-
required: entry.required,
129-
selector: { boolean: {} },
130-
}
131-
: entry.type === "schema"
132-
? {
133-
name: entry.name,
134-
required: entry.required,
135-
selector: { object: {} },
136-
}
137-
: entry.type === "float" || entry.type === "integer"
138-
? {
139-
name: entry.name,
140-
required: entry.required,
141-
selector: {
142-
number: {
143-
mode: "box",
144-
step: entry.type === "float" ? "any" : undefined,
145-
},
146-
},
147-
}
148-
: entry
149-
)
121+
(schema: readonly HaFormSchema[]): HaFormSchema[] =>
122+
this._convertSchemaElements(schema)
150123
);
151124

125+
private _convertSchemaElements(
126+
schema: readonly HaFormSchema[]
127+
): HaFormSchema[] {
128+
return schema.map((entry) => this._convertSchemaElement(entry));
129+
}
130+
131+
private _convertSchemaElement(entry: any): HaFormSchema {
132+
if (entry.type === "schema" && !entry.multiple) {
133+
return {
134+
name: entry.name,
135+
type: "expandable",
136+
required: entry.required,
137+
schema: this._convertSchemaElements(entry.schema),
138+
};
139+
}
140+
const selector = this._convertSchemaElementToSelector(entry, false);
141+
if (selector) {
142+
return {
143+
name: entry.name,
144+
required: entry.required,
145+
selector,
146+
};
147+
}
148+
return entry;
149+
}
150+
151+
private _convertSchemaElementToSelector(
152+
entry: any,
153+
force: boolean
154+
): Selector | null {
155+
if (entry.type === "select") {
156+
return { select: { options: entry.options } };
157+
}
158+
if (entry.type === "string") {
159+
return entry.multiple
160+
? { select: { options: [], multiple: true, custom_value: true } }
161+
: {
162+
text: {
163+
type: entry.format
164+
? entry.format
165+
: MASKED_FIELDS.includes(entry.name)
166+
? "password"
167+
: "text",
168+
},
169+
};
170+
}
171+
if (entry.type === "boolean") {
172+
return { boolean: {} };
173+
}
174+
if (entry.type === "schema") {
175+
const fields: NonNullable<ObjectSelector["object"]>["fields"] = {};
176+
for (const child_entry of entry.schema) {
177+
fields[child_entry.name] = {
178+
required: child_entry.required,
179+
selector: this._convertSchemaElementToSelector(child_entry, true)!,
180+
};
181+
}
182+
return {
183+
object: {
184+
multiple: entry.multiple,
185+
fields,
186+
},
187+
};
188+
}
189+
if (entry.type === "float" || entry.type === "integer") {
190+
return {
191+
number: {
192+
mode: "box",
193+
step: entry.type === "float" ? "any" : undefined,
194+
},
195+
};
196+
}
197+
if (force) {
198+
return { object: {} };
199+
}
200+
return null;
201+
}
202+
152203
private _filteredSchema = memoizeOne(
153204
(options: Record<string, unknown>, schema: HaFormSchema[]) =>
154205
schema.filter((entry) => entry.name in options || entry.required)

src/data/hassio/addon.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,15 @@ export type AddonState =
2929
| null;
3030
export type AddonRepository = "core" | "local" | string;
3131

32+
interface AddonFieldTranslation {
33+
name?: string;
34+
description?: string;
35+
fields?: Record<string, AddonFieldTranslation>;
36+
}
37+
3238
interface AddonTranslations {
3339
network?: Record<string, string>;
34-
configuration?: Record<string, { name?: string; description?: string }>;
40+
configuration?: Record<string, AddonFieldTranslation>;
3541
}
3642

3743
export interface HassioAddonInfo {

0 commit comments

Comments
 (0)