diff --git a/src/components/entity/ha-entity-picker.ts b/src/components/entity/ha-entity-picker.ts index e8055a985b..397685a32b 100644 --- a/src/components/entity/ha-entity-picker.ts +++ b/src/components/entity/ha-entity-picker.ts @@ -403,7 +403,8 @@ export class HaEntityPicker extends LitElement { } public async open() { - this._picker?.open(); + await this.updateComplete; + await this._picker?.open(); } private _valueChanged(ev) { diff --git a/src/components/ha-area-picker.ts b/src/components/ha-area-picker.ts index 5cbd57a26e..d073c42211 100644 --- a/src/components/ha-area-picker.ts +++ b/src/components/ha-area-picker.ts @@ -1,15 +1,14 @@ -import { mdiTextureBox } from "@mdi/js"; -import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit"; +import { mdiPlus, mdiTextureBox } from "@mdi/js"; import type { HassEntity } from "home-assistant-js-websocket"; -import type { PropertyValues, TemplateResult } from "lit"; -import { LitElement, html } from "lit"; -import { customElement, property, query, state } from "lit/decorators"; +import type { TemplateResult } from "lit"; +import { LitElement, html, nothing } from "lit"; +import { customElement, property, query } from "lit/decorators"; import memoizeOne from "memoize-one"; import { fireEvent } from "../common/dom/fire_event"; +import { computeAreaName } from "../common/entity/compute_area_name"; import { computeDomain } from "../common/entity/compute_domain"; -import type { ScorableTextItem } from "../common/string/filter/sequence-matching"; -import { fuzzyFilterSort } from "../common/string/filter/sequence-matching"; -import type { AreaRegistryEntry } from "../data/area_registry"; +import { computeFloorName } from "../common/entity/compute_floor_name"; +import { getAreaContext } from "../common/entity/context/get_area_context"; import { createAreaRegistryEntry } from "../data/area_registry"; import type { DeviceEntityDisplayLookup, @@ -21,26 +20,15 @@ import { showAlertDialog } from "../dialogs/generic/show-dialog-box"; import { showAreaRegistryDetailDialog } from "../panels/config/areas/show-dialog-area-registry-detail"; import type { HomeAssistant, ValueChangedEvent } from "../types"; import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker"; -import "./ha-combo-box"; -import type { HaComboBox } from "./ha-combo-box"; import "./ha-combo-box-item"; +import "./ha-generic-picker"; +import type { HaGenericPicker } from "./ha-generic-picker"; import "./ha-icon-button"; +import type { PickerComboBoxItem } from "./ha-picker-combo-box"; +import type { PickerValueRenderer } from "./ha-picker-field"; import "./ha-svg-icon"; -type ScorableAreaRegistryEntry = ScorableTextItem & AreaRegistryEntry; - -const rowRenderer: ComboBoxLitRenderer = (item) => html` - - ${item.icon - ? html`` - : html``} - ${item.name} - -`; - const ADD_NEW_ID = "___ADD_NEW___"; -const NO_ITEMS_ID = "___NO_ITEMS___"; -const ADD_NEW_SUGGESTION_ID = "___ADD_NEW_SUGGESTION___"; @customElement("ha-area-picker") export class HaAreaPicker extends LitElement { @@ -99,41 +87,61 @@ export class HaAreaPicker extends LitElement { @property({ type: Boolean }) public required = false; - @state() private _opened?: boolean; - - @query("ha-combo-box", true) public comboBox!: HaComboBox; - - private _suggestion?: string; - - private _init = false; + @query("ha-generic-picker") private _picker?: HaGenericPicker; public async open() { await this.updateComplete; - await this.comboBox?.open(); + await this._picker?.open(); } - public async focus() { - await this.updateComplete; - await this.comboBox?.focus(); - } + private _valueRenderer: PickerValueRenderer = (value) => { + const area = this.hass.areas[value]; + + if (!area) { + return html` + + ${area} + `; + } + + const { floor } = getAreaContext(area, this.hass); + + const areaName = area ? computeAreaName(area) : undefined; + const floorName = floor ? computeFloorName(floor) : undefined; + + const icon = area.icon; + + return html` + ${icon + ? html`` + : html``} + ${areaName} + ${floorName + ? html`${floorName}` + : nothing} + `; + }; private _getAreas = memoizeOne( ( - areas: AreaRegistryEntry[], - devices: DeviceRegistryEntry[], - entities: EntityRegistryDisplayEntry[], + haAreas: HomeAssistant["areas"], + haDevices: HomeAssistant["devices"], + haEntities: HomeAssistant["entities"], includeDomains: this["includeDomains"], excludeDomains: this["excludeDomains"], includeDeviceClasses: this["includeDeviceClasses"], deviceFilter: this["deviceFilter"], entityFilter: this["entityFilter"], - noAdd: this["noAdd"], excludeAreas: this["excludeAreas"] - ): AreaRegistryEntry[] => { + ): PickerComboBoxItem[] => { let deviceEntityLookup: DeviceEntityDisplayLookup = {}; let inputDevices: DeviceRegistryEntry[] | undefined; let inputEntities: EntityRegistryDisplayEntry[] | undefined; + const areas = Object.values(haAreas); + const devices = Object.values(haDevices); + const entities = Object.values(haEntities); + if ( includeDomains || excludeDomains || @@ -263,203 +271,126 @@ export class HaAreaPicker extends LitElement { ); } - if (!outputAreas.length) { - outputAreas = [ - { - area_id: NO_ITEMS_ID, - floor_id: null, - name: this.hass.localize("ui.components.area-picker.no_areas"), - picture: null, - icon: null, - aliases: [], - labels: [], - temperature_entity_id: null, - humidity_entity_id: null, - created_at: 0, - modified_at: 0, - }, - ]; - } + const items = outputAreas.map((area) => { + const { floor } = getAreaContext(area, this.hass); + const floorName = floor ? computeFloorName(floor) : undefined; + const areaName = computeAreaName(area); + return { + id: area.area_id, + primary: areaName || area.area_id, + secondary: floorName, + icon: area.icon || undefined, + icon_path: area.icon ? undefined : mdiTextureBox, + sorting_label: areaName, + search_labels: [ + areaName, + floorName, + area.area_id, + ...area.aliases, + ].filter((v): v is string => Boolean(v)), + }; + }); - return noAdd - ? outputAreas - : [ - ...outputAreas, - { - area_id: ADD_NEW_ID, - floor_id: null, - name: this.hass.localize("ui.components.area-picker.add_new"), - picture: null, - icon: "mdi:plus", - aliases: [], - labels: [], - temperature_entity_id: null, - humidity_entity_id: null, - created_at: 0, - modified_at: 0, - }, - ]; + return items; } ); - protected updated(changedProps: PropertyValues) { - if ( - (!this._init && this.hass) || - (this._init && changedProps.has("_opened") && this._opened) - ) { - this._init = true; - const areas = this._getAreas( - Object.values(this.hass.areas), - Object.values(this.hass.devices), - Object.values(this.hass.entities), - this.includeDomains, - this.excludeDomains, - this.includeDeviceClasses, - this.deviceFilter, - this.entityFilter, - this.noAdd, - this.excludeAreas - ).map((area) => ({ - ...area, - strings: [area.area_id, ...area.aliases, area.name], - })); - this.comboBox.items = areas; - this.comboBox.filteredItems = areas; + private _getItems = () => + this._getAreas( + this.hass.areas, + this.hass.devices, + this.hass.entities, + this.includeDomains, + this.excludeDomains, + this.includeDeviceClasses, + this.deviceFilter, + this.entityFilter, + this.excludeAreas + ); + + private _allAreaNames = memoizeOne( + (areas: HomeAssistant["areas"]) => + Object.values(areas) + .map((area) => computeAreaName(area)?.toLowerCase()) + .filter(Boolean) as string[] + ); + + private _getAdditionalItems = ( + searchString?: string + ): PickerComboBoxItem[] => { + if (this.noAdd) { + return []; } - } + + const allAreas = this._allAreaNames(this.hass.areas); + + if (searchString && !allAreas.includes(searchString.toLowerCase())) { + return [ + { + id: ADD_NEW_ID + searchString, + primary: this.hass.localize( + "ui.components.area-picker.add_new_sugestion", + { + name: searchString, + } + ), + icon_path: mdiPlus, + }, + ]; + } + + return [ + { + id: ADD_NEW_ID, + primary: this.hass.localize("ui.components.area-picker.add_new"), + icon_path: mdiPlus, + }, + ]; + }; protected render(): TemplateResult { + const placeholder = + this.placeholder ?? this.hass.localize("ui.components.area-picker.area"); + return html` - - + `; } - private _filterChanged(ev: CustomEvent): void { - const target = ev.target as HaComboBox; - const filterString = ev.detail.value; - if (!filterString) { - this.comboBox.filteredItems = this.comboBox.items; - return; - } - - const filteredItems = fuzzyFilterSort( - filterString, - target.items?.filter( - (item) => ![NO_ITEMS_ID, ADD_NEW_ID].includes(item.label_id) - ) || [] - ); - if (filteredItems.length === 0) { - if (this.noAdd) { - this.comboBox.filteredItems = [ - { - area_id: NO_ITEMS_ID, - floor_id: null, - name: this.hass.localize("ui.components.area-picker.no_match"), - icon: null, - picture: null, - labels: [], - aliases: [], - temperature_entity_id: null, - humidity_entity_id: null, - created_at: 0, - modified_at: 0, - }, - ] as AreaRegistryEntry[]; - } else { - this._suggestion = filterString; - this.comboBox.filteredItems = [ - { - area_id: ADD_NEW_SUGGESTION_ID, - floor_id: null, - name: this.hass.localize( - "ui.components.area-picker.add_new_sugestion", - { name: this._suggestion } - ), - icon: "mdi:plus", - picture: null, - labels: [], - aliases: [], - temperature_entity_id: null, - humidity_entity_id: null, - created_at: 0, - modified_at: 0, - }, - ] as AreaRegistryEntry[]; - } - } else { - this.comboBox.filteredItems = filteredItems; - } - } - - private get _value() { - return this.value || ""; - } - - private _openedChanged(ev: ValueChangedEvent) { - this._opened = ev.detail.value; - } - - private _areaChanged(ev: ValueChangedEvent) { + private _valueChanged(ev: ValueChangedEvent) { ev.stopPropagation(); - let newValue = ev.detail.value; + const value = ev.detail.value; - if (newValue === NO_ITEMS_ID) { - newValue = ""; - this.comboBox.setInputValue(""); - return; - } - - if (![ADD_NEW_SUGGESTION_ID, ADD_NEW_ID].includes(newValue)) { - if (newValue !== this._value) { - this._setValue(newValue); + if (!value.startsWith(ADD_NEW_ID)) { + if (value !== this.value) { + this._setValue(value); } return; } - (ev.target as any).value = this._value; - this.hass.loadFragmentTranslation("config"); + const suggestedName = value.substring(ADD_NEW_ID.length); + showAreaRegistryDetailDialog(this, { - suggestedName: newValue === ADD_NEW_SUGGESTION_ID ? this._suggestion : "", + suggestedName: suggestedName, createEntry: async (values) => { try { const area = await createAreaRegistryEntry(this.hass, values); - const areas = [...Object.values(this.hass.areas), area]; - this.comboBox.filteredItems = this._getAreas( - areas, - Object.values(this.hass.devices)!, - Object.values(this.hass.entities)!, - this.includeDomains, - this.excludeDomains, - this.includeDeviceClasses, - this.deviceFilter, - this.entityFilter, - this.noAdd, - this.excludeAreas - ); - await this.updateComplete; - await this.comboBox.updateComplete; this._setValue(area.area_id); } catch (err: any) { showAlertDialog(this, { @@ -471,17 +402,12 @@ export class HaAreaPicker extends LitElement { } }, }); - - this._suggestion = undefined; - this.comboBox.setInputValue(""); } private _setValue(value?: string) { this.value = value; - setTimeout(() => { - fireEvent(this, "value-changed", { value }); - fireEvent(this, "change"); - }, 0); + fireEvent(this, "value-changed", { value }); + fireEvent(this, "change"); } }