From 2ab5da6d8465198f4b624cc238083fabcb364445 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 27 Oct 2022 19:17:42 +0200 Subject: [PATCH] Allow null selector (#14212) --- .../ha-form/compute-initial-ha-form-data.ts | 12 ++-- .../ha-selector/ha-selector-area.ts | 14 ++-- .../ha-selector/ha-selector-attribute.ts | 6 +- .../ha-selector/ha-selector-config-entry.ts | 2 +- .../ha-selector/ha-selector-device.ts | 13 ++-- .../ha-selector/ha-selector-duration.ts | 2 +- .../ha-selector/ha-selector-entity.ts | 20 ++++-- .../ha-selector/ha-selector-file.ts | 2 +- .../ha-selector/ha-selector-icon.ts | 4 +- .../ha-selector/ha-selector-location.ts | 6 +- .../ha-selector/ha-selector-number.ts | 30 ++++---- .../ha-selector/ha-selector-select.ts | 26 +++---- .../ha-selector/ha-selector-state.ts | 4 +- .../ha-selector/ha-selector-target.ts | 12 ++-- .../ha-selector/ha-selector-text.ts | 4 +- .../ha-selector/ha-selector-ui-action.ts | 2 +- src/data/selector.ts | 68 +++++++++---------- 17 files changed, 121 insertions(+), 106 deletions(-) diff --git a/src/components/ha-form/compute-initial-ha-form-data.ts b/src/components/ha-form/compute-initial-ha-form-data.ts index 96c1ae58fb..ebcec7414d 100644 --- a/src/components/ha-form/compute-initial-ha-form-data.ts +++ b/src/components/ha-form/compute-initial-ha-form-data.ts @@ -39,11 +39,11 @@ export const computeInitialHaFormData = ( const selector: Selector = field.selector; if ("device" in selector) { - data[field.name] = selector.device.multiple ? [] : ""; + data[field.name] = selector.device?.multiple ? [] : ""; } else if ("entity" in selector) { - data[field.name] = selector.entity.multiple ? [] : ""; + data[field.name] = selector.entity?.multiple ? [] : ""; } else if ("area" in selector) { - data[field.name] = selector.area.multiple ? [] : ""; + data[field.name] = selector.area?.multiple ? [] : ""; } else if ("boolean" in selector) { data[field.name] = false; } else if ( @@ -56,9 +56,9 @@ export const computeInitialHaFormData = ( ) { data[field.name] = ""; } else if ("number" in selector) { - data[field.name] = selector.number.min ?? 0; + data[field.name] = selector.number?.min ?? 0; } else if ("select" in selector) { - if (selector.select.options.length) { + if (selector.select?.options.length) { data[field.name] = selector.select.options[0][0]; } } else if ("duration" in selector) { @@ -75,7 +75,7 @@ export const computeInitialHaFormData = ( } else if ("color_rgb" in selector) { data[field.name] = [0, 0, 0]; } else if ("color_temp" in selector) { - data[field.name] = selector.color_temp.min_mireds ?? 153; + data[field.name] = selector.color_temp?.min_mireds ?? 153; } else if ( "action" in selector || "media" in selector || diff --git a/src/components/ha-selector/ha-selector-area.ts b/src/components/ha-selector/ha-selector-area.ts index c6f1be6efd..704a36ed2c 100644 --- a/src/components/ha-selector/ha-selector-area.ts +++ b/src/components/ha-selector/ha-selector-area.ts @@ -55,8 +55,8 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) { protected updated(changedProperties: PropertyValues): void { if ( changedProperties.has("selector") && - (this.selector.area.device?.integration || - this.selector.area.entity?.integration) && + (this.selector.area?.device?.integration || + this.selector.area?.entity?.integration) && !this._entitySources ) { fetchEntitySourcesWithCache(this.hass).then((sources) => { @@ -67,14 +67,14 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) { protected render(): TemplateResult { if ( - (this.selector.area.device?.integration || - this.selector.area.entity?.integration) && + (this.selector.area?.device?.integration || + this.selector.area?.entity?.integration) && !this._entitySources ) { return html``; } - if (!this.selector.area.multiple) { + if (!this.selector.area?.multiple) { return html` { - if (!this.selector.area.entity) { + if (!this.selector.area?.entity) { return true; } @@ -118,7 +118,7 @@ export class HaAreaSelector extends SubscribeMixin(LitElement) { }; private _filterDevices = (device: DeviceRegistryEntry): boolean => { - if (!this.selector.area.device) { + if (!this.selector.area?.device) { return true; } diff --git a/src/components/ha-selector/ha-selector-attribute.ts b/src/components/ha-selector/ha-selector-attribute.ts index da06508e2f..da194bdf22 100644 --- a/src/components/ha-selector/ha-selector-attribute.ts +++ b/src/components/ha-selector/ha-selector-attribute.ts @@ -30,9 +30,9 @@ export class HaSelectorAttribute extends SubscribeMixin(LitElement) { return html` `; } diff --git a/src/components/ha-selector/ha-selector-device.ts b/src/components/ha-selector/ha-selector-device.ts index bfb0928524..e56808b766 100644 --- a/src/components/ha-selector/ha-selector-device.ts +++ b/src/components/ha-selector/ha-selector-device.ts @@ -53,7 +53,7 @@ export class HaDeviceSelector extends SubscribeMixin(LitElement) { super.updated(changedProperties); if ( changedProperties.has("selector") && - this.selector.device.integration && + this.selector.device?.integration && !this._entitySources ) { fetchEntitySourcesWithCache(this.hass).then((sources) => { @@ -63,11 +63,11 @@ export class HaDeviceSelector extends SubscribeMixin(LitElement) { } protected render() { - if (this.selector.device.integration && !this._entitySources) { + if (this.selector.device?.integration && !this._entitySources) { return html``; } - if (!this.selector.device.multiple) { + if (!this.selector.device?.multiple) { return html` `; } diff --git a/src/components/ha-selector/ha-selector-entity.ts b/src/components/ha-selector/ha-selector-entity.ts index 2c7062171c..b56ecf1723 100644 --- a/src/components/ha-selector/ha-selector-entity.ts +++ b/src/components/ha-selector/ha-selector-entity.ts @@ -30,14 +30,14 @@ export class HaEntitySelector extends LitElement { @property({ type: Boolean }) public required = true; protected render() { - if (!this.selector.entity.multiple) { + if (!this.selector.entity?.multiple) { return html` { @@ -73,8 +73,16 @@ export class HaEntitySelector extends LitElement { } } - private _filterEntities = (entity: HassEntity): boolean => - filterSelectorEntities(this.selector.entity, entity, this._entitySources); + private _filterEntities = (entity: HassEntity): boolean => { + if (!this.selector?.entity) { + return true; + } + return filterSelectorEntities( + this.selector.entity, + entity, + this._entitySources + ); + }; } declare global { diff --git a/src/components/ha-selector/ha-selector-file.ts b/src/components/ha-selector/ha-selector-file.ts index 589e43cf9d..f46ca12696 100644 --- a/src/components/ha-selector/ha-selector-file.ts +++ b/src/components/ha-selector/ha-selector-file.ts @@ -32,7 +32,7 @@ export class HaFileSelector extends LitElement { return html` `; diff --git a/src/components/ha-selector/ha-selector-location.ts b/src/components/ha-selector/ha-selector-location.ts index 05a2b3b451..b4b0c6524b 100644 --- a/src/components/ha-selector/ha-selector-location.ts +++ b/src/components/ha-selector/ha-selector-location.ts @@ -43,7 +43,7 @@ export class HaLocationSelector extends LitElement { value?: LocationSelectorValue ): MarkerLocation[] => { const computedStyles = getComputedStyle(this); - const zoneRadiusColor = selector.location.radius + const zoneRadiusColor = selector.location?.radius ? computedStyles.getPropertyValue("--zone-radius-color") || computedStyles.getPropertyValue("--accent-color") : undefined; @@ -52,10 +52,10 @@ export class HaLocationSelector extends LitElement { id: "location", latitude: value?.latitude || this.hass.config.latitude, longitude: value?.longitude || this.hass.config.longitude, - radius: selector.location.radius ? value?.radius || 1000 : undefined, + radius: selector.location?.radius ? value?.radius || 1000 : undefined, radius_color: zoneRadiusColor, icon: - selector.location.icon || selector.location.radius + selector.location?.icon || selector.location?.radius ? "mdi:map-marker-radius" : "mdi:map-marker", location_editable: true, diff --git a/src/components/ha-selector/ha-selector-number.ts b/src/components/ha-selector/ha-selector-number.ts index 343490af31..4edf7e2eb2 100644 --- a/src/components/ha-selector/ha-selector-number.ts +++ b/src/components/ha-selector/ha-selector-number.ts @@ -27,7 +27,7 @@ export class HaNumberSelector extends LitElement { @property({ type: Boolean }) public disabled = false; protected render() { - const isBox = this.selector.number.mode === "box"; + const isBox = this.selector.number?.mode === "box"; return html`
@@ -37,10 +37,10 @@ export class HaNumberSelector extends LitElement { ? html`${this.label}${this.required ? " *" : ""}` : ""} @@ -80,7 +82,7 @@ export class HaNumberSelector extends LitElement { } private get _value() { - return this.value ?? (this.selector.number.min || 0); + return this.value ?? (this.selector.number?.min || 0); } private _handleInputChange(ev) { @@ -88,7 +90,7 @@ export class HaNumberSelector extends LitElement { const value = ev.target.value === "" || isNaN(ev.target.value) ? this.required - ? this.selector.number.min || 0 + ? this.selector.number?.min || 0 : undefined : Number(ev.target.value); if (this.value === value) { diff --git a/src/components/ha-selector/ha-selector-select.ts b/src/components/ha-selector/ha-selector-select.ts index c454d339cd..fddb300c7b 100644 --- a/src/components/ha-selector/ha-selector-select.ts +++ b/src/components/ha-selector/ha-selector-select.ts @@ -9,6 +9,7 @@ import type { HomeAssistant } from "../../types"; import "../ha-checkbox"; import "../ha-chip"; import "../ha-chip-set"; +import "../ha-combo-box"; import type { HaComboBox } from "../ha-combo-box"; import "../ha-formfield"; import "../ha-radio"; @@ -36,12 +37,13 @@ export class HaSelectSelector extends LitElement { private _filter = ""; protected render() { - const options = this.selector.select.options.map((option) => - typeof option === "object" ? option : { value: option, label: option } - ); + const options = + this.selector.select?.options.map((option) => + typeof option === "object" ? option : { value: option, label: option } + ) || []; - if (!this.selector.select.custom_value && this._mode === "list") { - if (!this.selector.select.multiple) { + if (!this.selector.select?.custom_value && this._mode === "list") { + if (!this.selector.select?.multiple) { return html`
${this.label} @@ -82,7 +84,7 @@ export class HaSelectSelector extends LitElement { `; } - if (this.selector.select.multiple) { + if (this.selector.select?.multiple) { const value = !this.value || this.value === "" ? [] : (this.value as string[]); @@ -123,7 +125,7 @@ export class HaSelectSelector extends LitElement { `; } - if (this.selector.select.custom_value) { + if (this.selector.select?.custom_value) { if ( this.value !== undefined && !options.find((option) => option.value === this.value) @@ -178,8 +180,8 @@ export class HaSelectSelector extends LitElement { private get _mode(): "list" | "dropdown" { return ( - this.selector.select.mode || - (this.selector.select.options.length < 6 ? "list" : "dropdown") + this.selector.select?.mode || + ((this.selector.select?.options?.length || 0) < 6 ? "list" : "dropdown") ); } @@ -243,7 +245,7 @@ export class HaSelectSelector extends LitElement { return; } - if (!this.selector.select.multiple) { + if (!this.selector.select?.multiple) { fireEvent(this, "value-changed", { value: newValue, }); @@ -271,14 +273,14 @@ export class HaSelectSelector extends LitElement { this._filter = ev?.detail.value || ""; const filteredItems = this.comboBox.items?.filter((item) => { - if (this.selector.select.multiple && this.value?.includes(item.value)) { + if (this.selector.select?.multiple && this.value?.includes(item.value)) { return false; } const label = item.label || item.value; return label.toLowerCase().includes(this._filter?.toLowerCase()); }); - if (this._filter && this.selector.select.custom_value) { + if (this._filter && this.selector.select?.custom_value) { filteredItems?.unshift({ label: this._filter, value: this._filter }); } diff --git a/src/components/ha-selector/ha-selector-state.ts b/src/components/ha-selector/ha-selector-state.ts index b72b0349bd..a28628ed86 100644 --- a/src/components/ha-selector/ha-selector-state.ts +++ b/src/components/ha-selector/ha-selector-state.ts @@ -30,9 +30,9 @@ export class HaSelectorState extends SubscribeMixin(LitElement) { return html` { @@ -76,8 +76,8 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) { protected render(): TemplateResult { if ( - (this.selector.target.device?.integration || - this.selector.target.entity?.integration) && + (this.selector.target?.device?.integration || + this.selector.target?.entity?.integration) && !this._entitySources ) { return html``; @@ -94,7 +94,7 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) { } private _filterEntities = (entity: HassEntity): boolean => { - if (!this.selector.target.entity) { + if (!this.selector.target?.entity) { return true; } @@ -106,7 +106,7 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) { }; private _filterDevices = (device: DeviceRegistryEntry): boolean => { - if (!this.selector.target.device) { + if (!this.selector.target?.device) { return true; } diff --git a/src/components/ha-selector/ha-selector-text.ts b/src/components/ha-selector/ha-selector-text.ts index 1f5190dabe..a9d3de47d4 100644 --- a/src/components/ha-selector/ha-selector-text.ts +++ b/src/components/ha-selector/ha-selector-text.ts @@ -39,7 +39,7 @@ export class HaTextSelector extends LitElement { .disabled=${this.disabled} @input=${this._handleChange} autocapitalize="none" - .autocomplete=${this.selector.text.autocomplete} + .autocomplete=${this.selector.text?.autocomplete} spellcheck="false" .required=${this.required} autogrow @@ -59,7 +59,7 @@ export class HaTextSelector extends LitElement { html`
` : this.selector.text?.suffix} .required=${this.required} - .autocomplete=${this.selector.text.autocomplete} + .autocomplete=${this.selector.text?.autocomplete} > ${this.selector.text?.type === "password" ? html` diff --git a/src/data/selector.ts b/src/data/selector.ts index c86922162d..061b246695 100644 --- a/src/data/selector.ts +++ b/src/data/selector.ts @@ -36,26 +36,26 @@ export type Selector = export interface ActionSelector { // eslint-disable-next-line @typescript-eslint/ban-types - action: {}; + action: {} | null; } export interface AddonSelector { addon: { name?: string; slug?: string; - }; + } | null; } export interface SelectorDevice { - integration?: DeviceSelector["device"]["integration"]; - manufacturer?: DeviceSelector["device"]["manufacturer"]; - model?: DeviceSelector["device"]["model"]; + integration?: NonNullable["integration"]; + manufacturer?: NonNullable["manufacturer"]; + model?: NonNullable["model"]; } export interface SelectorEntity { - integration?: EntitySelector["entity"]["integration"]; - domain?: EntitySelector["entity"]["domain"]; - device_class?: EntitySelector["entity"]["device_class"]; + integration?: NonNullable["integration"]; + domain?: NonNullable["domain"]; + device_class?: NonNullable["device_class"]; } export interface AreaSelector { @@ -63,47 +63,47 @@ export interface AreaSelector { entity?: SelectorEntity; device?: SelectorDevice; multiple?: boolean; - }; + } | null; } export interface AttributeSelector { attribute: { entity_id?: string; hide_attributes?: readonly string[]; - }; + } | null; } export interface BooleanSelector { // eslint-disable-next-line @typescript-eslint/ban-types - boolean: {}; + boolean: {} | null; } export interface ColorRGBSelector { // eslint-disable-next-line @typescript-eslint/ban-types - color_rgb: {}; + color_rgb: {} | null; } export interface ColorTempSelector { color_temp: { min_mireds?: number; max_mireds?: number; - }; + } | null; } export interface ConfigEntrySelector { config_entry: { integration?: string; - }; + } | null; } export interface DateSelector { // eslint-disable-next-line @typescript-eslint/ban-types - date: {}; + date: {} | null; } export interface DateTimeSelector { // eslint-disable-next-line @typescript-eslint/ban-types - datetime: {}; + datetime: {} | null; } export interface DeviceSelector { @@ -113,13 +113,13 @@ export interface DeviceSelector { model?: string; entity?: SelectorEntity; multiple?: boolean; - }; + } | null; } export interface DurationSelector { duration: { enable_day?: boolean; - }; + } | null; } export interface EntitySelector { @@ -130,24 +130,24 @@ export interface EntitySelector { multiple?: boolean; include_entities?: string[]; exclude_entities?: string[]; - }; + } | null; } export interface FileSelector { file: { accept: string; - }; + } | null; } export interface IconSelector { icon: { placeholder?: string; fallbackPath?: string; - }; + } | null; } export interface LocationSelector { - location: { radius?: boolean; icon?: string }; + location: { radius?: boolean; icon?: string } | null; } export interface LocationSelectorValue { @@ -158,7 +158,7 @@ export interface LocationSelectorValue { export interface MediaSelector { // eslint-disable-next-line @typescript-eslint/ban-types - media: {}; + media: {} | null; } export interface MediaSelectorValue { @@ -176,7 +176,7 @@ export interface MediaSelectorValue { export interface NavigationSelector { // eslint-disable-next-line @typescript-eslint/ban-types - navigation: {}; + navigation: {} | null; } export interface NumberSelector { @@ -186,12 +186,12 @@ export interface NumberSelector { step?: number; mode?: "box" | "slider"; unit_of_measurement?: string; - }; + } | null; } export interface ObjectSelector { // eslint-disable-next-line @typescript-eslint/ban-types - object: {}; + object: {} | null; } export interface SelectOption { @@ -206,14 +206,14 @@ export interface SelectSelector { custom_value?: boolean; mode?: "list" | "dropdown"; options: readonly string[] | readonly SelectOption[]; - }; + } | null; } export interface StateSelector { state: { entity_id?: string; attribute?: string; - }; + } | null; } export interface StringSelector { @@ -235,34 +235,34 @@ export interface StringSelector { | "color"; suffix?: string; autocomplete?: string; - }; + } | null; } export interface TargetSelector { target: { entity?: SelectorEntity; device?: SelectorDevice; - }; + } | null; } export interface TemplateSelector { // eslint-disable-next-line @typescript-eslint/ban-types - template: {}; + template: {} | null; } export interface ThemeSelector { // eslint-disable-next-line @typescript-eslint/ban-types - theme: {}; + theme: {} | null; } export interface TimeSelector { // eslint-disable-next-line @typescript-eslint/ban-types - time: {}; + time: {} | null; } export interface UiActionSelector { "ui-action": { actions?: UiAction[]; - }; + } | null; } export const filterSelectorDevices = (