diff --git a/demo/src/stubs/area_registry.ts b/demo/src/stubs/area_registry.ts index 99c5ede11e..f45521b0d9 100644 --- a/demo/src/stubs/area_registry.ts +++ b/demo/src/stubs/area_registry.ts @@ -1,4 +1,4 @@ -import type { AreaRegistryEntry } from "../../../src/data/area_registry"; +import type { AreaRegistryEntry } from "../../../src/data/area/area_registry"; import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; export const mockAreaRegistry = ( diff --git a/gallery/src/pages/components/ha-form.ts b/gallery/src/pages/components/ha-form.ts index 508f63e1dd..dc2dc211ad 100644 --- a/gallery/src/pages/components/ha-form.ts +++ b/gallery/src/pages/components/ha-form.ts @@ -10,7 +10,7 @@ import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervis import { computeInitialHaFormData } from "../../../../src/components/ha-form/compute-initial-ha-form-data"; import "../../../../src/components/ha-form/ha-form"; import type { HaFormSchema } from "../../../../src/components/ha-form/types"; -import type { AreaRegistryEntry } from "../../../../src/data/area_registry"; +import type { AreaRegistryEntry } from "../../../../src/data/area/area_registry"; import type { DeviceRegistryEntry } from "../../../../src/data/device/device_registry"; import { getEntity } from "../../../../src/fake_data/entity"; import { provideHass } from "../../../../src/fake_data/provide_hass"; diff --git a/gallery/src/pages/components/ha-selector.ts b/gallery/src/pages/components/ha-selector.ts index 2f0fedf1e4..debcb1bcd8 100644 --- a/gallery/src/pages/components/ha-selector.ts +++ b/gallery/src/pages/components/ha-selector.ts @@ -11,7 +11,7 @@ import { mockLabelRegistry } from "../../../../demo/src/stubs/label_registry"; import "../../../../src/components/ha-formfield"; import "../../../../src/components/ha-selector/ha-selector"; import "../../../../src/components/ha-settings-row"; -import type { AreaRegistryEntry } from "../../../../src/data/area_registry"; +import type { AreaRegistryEntry } from "../../../../src/data/area/area_registry"; import type { BlueprintInput } from "../../../../src/data/blueprint"; import type { DeviceRegistryEntry } from "../../../../src/data/device/device_registry"; import type { FloorRegistryEntry } from "../../../../src/data/floor_registry"; diff --git a/src/common/areas/areas-floor-hierarchy.ts b/src/common/areas/areas-floor-hierarchy.ts index 33c1550f48..b44b273f8f 100644 --- a/src/common/areas/areas-floor-hierarchy.ts +++ b/src/common/areas/areas-floor-hierarchy.ts @@ -1,4 +1,4 @@ -import type { AreaRegistryEntry } from "../../data/area_registry"; +import type { AreaRegistryEntry } from "../../data/area/area_registry"; import type { FloorRegistryEntry } from "../../data/floor_registry"; export interface AreasFloorHierarchy { diff --git a/src/common/entity/compute_area_name.ts b/src/common/entity/compute_area_name.ts index fdd515ba9a..f5428e46ec 100644 --- a/src/common/entity/compute_area_name.ts +++ b/src/common/entity/compute_area_name.ts @@ -1,4 +1,4 @@ -import type { AreaRegistryEntry } from "../../data/area_registry"; +import type { AreaRegistryEntry } from "../../data/area/area_registry"; export const computeAreaName = (area: AreaRegistryEntry): string | undefined => area.name?.trim(); diff --git a/src/common/entity/context/get_area_context.ts b/src/common/entity/context/get_area_context.ts index 44531a0c07..adfdf82863 100644 --- a/src/common/entity/context/get_area_context.ts +++ b/src/common/entity/context/get_area_context.ts @@ -1,4 +1,4 @@ -import type { AreaRegistryEntry } from "../../../data/area_registry"; +import type { AreaRegistryEntry } from "../../../data/area/area_registry"; import type { FloorRegistryEntry } from "../../../data/floor_registry"; import type { HomeAssistant } from "../../../types"; diff --git a/src/common/entity/context/get_device_context.ts b/src/common/entity/context/get_device_context.ts index a41e917945..c03323bc77 100644 --- a/src/common/entity/context/get_device_context.ts +++ b/src/common/entity/context/get_device_context.ts @@ -1,4 +1,4 @@ -import type { AreaRegistryEntry } from "../../../data/area_registry"; +import type { AreaRegistryEntry } from "../../../data/area/area_registry"; import type { DeviceRegistryEntry } from "../../../data/device/device_registry"; import type { FloorRegistryEntry } from "../../../data/floor_registry"; import type { HomeAssistant } from "../../../types"; diff --git a/src/common/entity/context/get_entity_context.ts b/src/common/entity/context/get_entity_context.ts index 0edb352f86..2cc43e700c 100644 --- a/src/common/entity/context/get_entity_context.ts +++ b/src/common/entity/context/get_entity_context.ts @@ -1,5 +1,5 @@ import type { HassEntity } from "home-assistant-js-websocket"; -import type { AreaRegistryEntry } from "../../../data/area_registry"; +import type { AreaRegistryEntry } from "../../../data/area/area_registry"; import type { DeviceRegistryEntry } from "../../../data/device/device_registry"; import type { EntityRegistryDisplayEntry, diff --git a/src/components/ha-adaptive-dialog.ts b/src/components/ha-adaptive-dialog.ts index 6e5c631de9..cba11281ba 100644 --- a/src/components/ha-adaptive-dialog.ts +++ b/src/components/ha-adaptive-dialog.ts @@ -1,8 +1,8 @@ import { mdiClose } from "@mdi/js"; -import { css, html, LitElement } from "lit"; +import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; -import type { HomeAssistant } from "../types"; import { listenMediaQuery } from "../common/dom/media_query"; +import type { HomeAssistant } from "../types"; import "./ha-bottom-sheet"; import "./ha-dialog-header"; import "./ha-icon-button"; @@ -88,6 +88,9 @@ export class HaAdaptiveDialog extends LitElement { @property({ type: Boolean, attribute: "block-mode-change" }) public blockModeChange = false; + @property({ type: Boolean, attribute: "without-header" }) + public withoutHeader = false; + @state() private _mode: DialogSheetMode = "dialog"; private _unsubMediaQuery?: () => void; @@ -118,27 +121,33 @@ export class HaAdaptiveDialog extends LitElement { if (this._mode === "bottom-sheet") { return html` - - - - - ${this.headerTitle !== undefined - ? html` - ${this.headerTitle} - ` - : html``} - ${this.headerSubtitle !== undefined - ? html`${this.headerSubtitle}` - : html``} - - + ${!this.withoutHeader + ? html` + + + + ${this.headerTitle !== undefined + ? html` + ${this.headerTitle} + ` + : html``} + ${this.headerSubtitle !== undefined + ? html`${this.headerSubtitle}` + : html``} + + ` + : nothing} @@ -156,6 +165,7 @@ export class HaAdaptiveDialog extends LitElement { .headerSubtitle=${this.headerSubtitle} .headerSubtitlePosition=${this.headerSubtitlePosition} flexcontent + .withoutHeader=${this.withoutHeader} > @@ -137,183 +127,13 @@ export class HaAreaPicker extends LitElement { } ); - private _getAreas = memoizeOne( - ( - haAreas: HomeAssistant["areas"], - haDevices: HomeAssistant["devices"], - haEntities: HomeAssistant["entities"], - includeDomains: this["includeDomains"], - excludeDomains: this["excludeDomains"], - includeDeviceClasses: this["includeDeviceClasses"], - deviceFilter: this["deviceFilter"], - entityFilter: this["entityFilter"], - excludeAreas: this["excludeAreas"] - ): 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 || - includeDeviceClasses || - deviceFilter || - entityFilter - ) { - deviceEntityLookup = getDeviceEntityDisplayLookup(entities); - inputDevices = devices; - inputEntities = entities.filter((entity) => entity.area_id); - - if (includeDomains) { - inputDevices = inputDevices!.filter((device) => { - const devEntities = deviceEntityLookup[device.id]; - if (!devEntities || !devEntities.length) { - return false; - } - return deviceEntityLookup[device.id].some((entity) => - includeDomains.includes(computeDomain(entity.entity_id)) - ); - }); - inputEntities = inputEntities!.filter((entity) => - includeDomains.includes(computeDomain(entity.entity_id)) - ); - } - - if (excludeDomains) { - inputDevices = inputDevices!.filter((device) => { - const devEntities = deviceEntityLookup[device.id]; - if (!devEntities || !devEntities.length) { - return true; - } - return entities.every( - (entity) => - !excludeDomains.includes(computeDomain(entity.entity_id)) - ); - }); - inputEntities = inputEntities!.filter( - (entity) => - !excludeDomains.includes(computeDomain(entity.entity_id)) - ); - } - - if (includeDeviceClasses) { - inputDevices = inputDevices!.filter((device) => { - const devEntities = deviceEntityLookup[device.id]; - if (!devEntities || !devEntities.length) { - return false; - } - return deviceEntityLookup[device.id].some((entity) => { - const stateObj = this.hass.states[entity.entity_id]; - if (!stateObj) { - return false; - } - return ( - stateObj.attributes.device_class && - includeDeviceClasses.includes(stateObj.attributes.device_class) - ); - }); - }); - inputEntities = inputEntities!.filter((entity) => { - const stateObj = this.hass.states[entity.entity_id]; - return ( - stateObj.attributes.device_class && - includeDeviceClasses.includes(stateObj.attributes.device_class) - ); - }); - } - - if (deviceFilter) { - inputDevices = inputDevices!.filter((device) => - deviceFilter!(device) - ); - } - - if (entityFilter) { - inputDevices = inputDevices!.filter((device) => { - const devEntities = deviceEntityLookup[device.id]; - if (!devEntities || !devEntities.length) { - return false; - } - return deviceEntityLookup[device.id].some((entity) => { - const stateObj = this.hass.states[entity.entity_id]; - if (!stateObj) { - return false; - } - return entityFilter(stateObj); - }); - }); - inputEntities = inputEntities!.filter((entity) => { - const stateObj = this.hass.states[entity.entity_id]; - if (!stateObj) { - return false; - } - return entityFilter!(stateObj); - }); - } - } - - let outputAreas = areas; - - let areaIds: string[] | undefined; - - if (inputDevices) { - areaIds = inputDevices - .filter((device) => device.area_id) - .map((device) => device.area_id!); - } - - if (inputEntities) { - areaIds = (areaIds ?? []).concat( - inputEntities - .filter((entity) => entity.area_id) - .map((entity) => entity.area_id!) - ); - } - - if (areaIds) { - outputAreas = outputAreas.filter((area) => - areaIds!.includes(area.area_id) - ); - } - - if (excludeAreas) { - outputAreas = outputAreas.filter( - (area) => !excludeAreas!.includes(area.area_id) - ); - } - - const items = outputAreas.map((area) => { - const { floor } = getAreaContext(area, this.hass.floors); - 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, - search_labels: { - areaName: areaName || null, - floorName: floorName || null, - id: area.area_id, - aliases: area.aliases.join(" "), - }, - }; - }); - - return items; - } - ); - private _getItems = () => - this._getAreas( + this._getAreasMemoized( this.hass.areas, + this.hass.floors, this.hass.devices, this.hass.entities, + this.hass.states, this.includeDomains, this.excludeDomains, this.includeDeviceClasses, @@ -394,7 +214,7 @@ export class HaAreaPicker extends LitElement { .getAdditionalItems=${this._getAdditionalItems} .valueRenderer=${valueRenderer} .addButtonLabel=${this.addButtonLabel} - .searchKeys=${SEARCH_KEYS} + .searchKeys=${areaComboBoxKeys} .unknownItemText=${this.hass.localize( "ui.components.area-picker.unknown" )} diff --git a/src/components/ha-floor-picker.ts b/src/components/ha-floor-picker.ts index 89e18baa91..d3570f4c3e 100644 --- a/src/components/ha-floor-picker.ts +++ b/src/components/ha-floor-picker.ts @@ -8,7 +8,7 @@ import memoizeOne from "memoize-one"; import { fireEvent } from "../common/dom/fire_event"; import { computeDomain } from "../common/entity/compute_domain"; import { computeFloorName } from "../common/entity/compute_floor_name"; -import { updateAreaRegistryEntry } from "../data/area_registry"; +import { updateAreaRegistryEntry } from "../data/area/area_registry"; import type { DeviceEntityDisplayLookup, DeviceRegistryEntry, diff --git a/src/components/ha-picker-combo-box.ts b/src/components/ha-picker-combo-box.ts index 9ce5bcfd6c..2016ba4e6c 100644 --- a/src/components/ha-picker-combo-box.ts +++ b/src/components/ha-picker-combo-box.ts @@ -1,6 +1,6 @@ import type { LitVirtualizer } from "@lit-labs/virtualizer"; import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize"; -import { mdiMagnify, mdiMinusBoxOutline, mdiPlus } from "@mdi/js"; +import { mdiClose, mdiMagnify, mdiMinusBoxOutline, mdiPlus } from "@mdi/js"; import Fuse from "fuse.js"; import { css, html, LitElement, nothing } from "lit"; import { @@ -26,6 +26,8 @@ import "./chips/ha-chip-set"; import "./chips/ha-filter-chip"; import "./ha-combo-box-item"; import "./ha-icon"; +import "./ha-icon-button"; +import "./ha-svg-icon"; import "./ha-textfield"; import type { HaTextField } from "./ha-textfield"; @@ -147,7 +149,9 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) { @property({ attribute: "selected-section" }) public selectedSection?: string; - @query("lit-virtualizer") private _virtualizerElement?: LitVirtualizer; + @property({ type: Boolean, reflect: true }) public clearable = false; + + @query("lit-virtualizer") public virtualizerElement?: LitVirtualizer; @query("ha-textfield") private _searchFieldElement?: HaTextField; @@ -160,7 +164,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) { } protected get scrollableElement(): HTMLElement | null { - return this._virtualizerElement as HTMLElement | null; + return this.virtualizerElement as HTMLElement | null; } @state() private _sectionTitle?: string; @@ -207,8 +211,17 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) { return html` + .iconTrailing=${this.clearable && !!this._search} + > + + ${this._renderSectionButtons()} ${this.sections?.length ? html` @@ -244,6 +257,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) { @unpinned=${this._handleUnpinned} @scroll=${this._onScrollList} @focus=${this._focusList} + @blur=${this._resetSelectedItem} @visibilityChanged=${this._visibilityChanged} > @@ -276,18 +290,18 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) { @eventOptions({ passive: true }) private _visibilityChanged(ev) { if ( - this._virtualizerElement && + this.virtualizerElement && this.sectionTitleFunction && this.sections?.length ) { - const firstItem = this._virtualizerElement.items[ev.first]; - const secondItem = this._virtualizerElement.items[ev.first + 1]; + const firstItem = this.virtualizerElement.items[ev.first]; + const secondItem = this.virtualizerElement.items[ev.first + 1]; this._sectionTitle = this.sectionTitleFunction({ firstIndex: ev.first, lastIndex: ev.last, firstItem: firstItem as PickerComboBoxItem, secondItem: secondItem as PickerComboBoxItem, - itemsCount: this._virtualizerElement.items.length, + itemsCount: this.virtualizerElement.items.length, }); } } @@ -403,9 +417,22 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) { private _valueSelected = (ev: Event) => { ev.stopPropagation(); const value = (ev.currentTarget as any).value as string; + const index = Number((ev.currentTarget as any).index); const newValue = value?.trim(); - fireEvent(this, "value-changed", { value: newValue }); + this._fireSelectedEvents(newValue, index); + }; + + private _fireSelectedEvents(value: string, index: number) { + fireEvent(this, "value-changed", { value }); + fireEvent(this, "index-selected", { index }); + } + + private _clearSearch = () => { + if (this._searchFieldElement) { + this._searchFieldElement.value = ""; + this._searchFieldElement.dispatchEvent(new Event("input")); + } }; private _fuseIndex = memoizeOne( @@ -487,8 +514,8 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) { this._items = this._getItems(); // Reset scroll position when filter changes - if (this._virtualizerElement) { - this._virtualizerElement.scrollToIndex(0); + if (this.virtualizerElement) { + this.virtualizerElement.scrollToIndex(0); } } @@ -511,13 +538,13 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) { private _selectNextItem = (ev?: KeyboardEvent) => { ev?.stopPropagation(); ev?.preventDefault(); - if (!this._virtualizerElement) { + if (!this.virtualizerElement) { return; } this._searchFieldElement?.focus(); - const items = this._virtualizerElement.items as PickerComboBoxItem[]; + const items = this.virtualizerElement.items as PickerComboBoxItem[]; const maxItems = items.length - 1; @@ -551,14 +578,14 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) { private _selectPreviousItem = (ev: KeyboardEvent) => { ev.stopPropagation(); ev.preventDefault(); - if (!this._virtualizerElement) { + if (!this.virtualizerElement) { return; } if (this._selectedItemIndex > 0) { const nextIndex = this._selectedItemIndex - 1; - const items = this._virtualizerElement.items as PickerComboBoxItem[]; + const items = this.virtualizerElement.items as PickerComboBoxItem[]; if (!items[nextIndex]) { return; @@ -580,13 +607,13 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) { private _selectFirstItem = (ev: KeyboardEvent) => { ev.stopPropagation(); - if (!this._virtualizerElement || !this._virtualizerElement.items.length) { + if (!this.virtualizerElement || !this.virtualizerElement.items.length) { return; } const nextIndex = 0; - if (typeof this._virtualizerElement.items[nextIndex] === "string") { + if (typeof this.virtualizerElement.items[nextIndex] === "string") { this._selectedItemIndex = nextIndex + 1; } else { this._selectedItemIndex = nextIndex; @@ -597,13 +624,13 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) { private _selectLastItem = (ev: KeyboardEvent) => { ev.stopPropagation(); - if (!this._virtualizerElement || !this._virtualizerElement.items.length) { + if (!this.virtualizerElement || !this.virtualizerElement.items.length) { return; } - const nextIndex = this._virtualizerElement.items.length - 1; + const nextIndex = this.virtualizerElement.items.length - 1; - if (typeof this._virtualizerElement.items[nextIndex] === "string") { + if (typeof this.virtualizerElement.items[nextIndex] === "string") { this._selectedItemIndex = nextIndex - 1; } else { this._selectedItemIndex = nextIndex; @@ -613,14 +640,14 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) { }; private _scrollToSelectedItem = () => { - this._virtualizerElement + this.virtualizerElement ?.querySelector(".selected") ?.classList.remove("selected"); - this._virtualizerElement?.scrollToIndex(this._selectedItemIndex, "end"); + this.virtualizerElement?.scrollToIndex(this._selectedItemIndex, "end"); requestAnimationFrame(() => { - this._virtualizerElement + this.virtualizerElement ?.querySelector(`#list-item-${this._selectedItemIndex}`) ?.classList.add("selected"); }); @@ -628,12 +655,20 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) { private _pickSelectedItem = (ev: KeyboardEvent) => { ev.stopPropagation(); - const firstItem = this._virtualizerElement?.items[0] as PickerComboBoxItem; - - if (this._virtualizerElement?.items.length === 1) { - fireEvent(this, "value-changed", { - value: firstItem.id, + if ( + this.virtualizerElement?.items?.length !== undefined && + this.virtualizerElement.items.length < 4 && // it still can have a section title and a padding item + this.virtualizerElement.items.filter((item) => typeof item !== "string") + .length === 1 + ) { + ( + this.virtualizerElement?.items as (PickerComboBoxItem | string)[] + ).forEach((item, index) => { + if (typeof item !== "string") { + this._fireSelectedEvents(item.id, index); + } }); + return; } if (this._selectedItemIndex === -1) { @@ -643,16 +678,16 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) { // if filter button is focused ev.preventDefault(); - const item = this._virtualizerElement?.items[ + const item = this.virtualizerElement?.items[ this._selectedItemIndex ] as PickerComboBoxItem; if (item) { - fireEvent(this, "value-changed", { value: item.id }); + this._fireSelectedEvents(item.id, this._selectedItemIndex); } }; private _resetSelectedItem() { - this._virtualizerElement + this.virtualizerElement ?.querySelector(".selected") ?.classList.remove("selected"); this._selectedItemIndex = -1; @@ -662,11 +697,11 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) { typeof item === "string" ? item : item?.id; private _getInitialSelectedIndex() { - if (!this._virtualizerElement || this._search || !this.value) { + if (!this.virtualizerElement || this._search || !this.value) { return 0; } - const index = this._virtualizerElement.items.findIndex( + const index = this.virtualizerElement.items.findIndex( (item) => typeof item !== "string" && (item as PickerComboBoxItem).id === this.value @@ -691,6 +726,10 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) { flex: 1; } + :host([clearable]) { + --text-field-padding: 0 0 0 var(--ha-space-4); + } + ha-textfield { padding: 0 var(--ha-space-3); margin-bottom: var(--ha-space-3); @@ -792,8 +831,9 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) { .section-title, .title { + box-sizing: border-box; background-color: var(--ha-color-fill-neutral-quiet-resting); - padding: var(--ha-space-2) var(--ha-space-3); + padding: var(--ha-space-1) var(--ha-space-4); font-weight: var(--ha-font-weight-bold); color: var(--secondary-text-color); min-height: var(--ha-space-6); @@ -822,7 +862,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) { opacity: 0; position: absolute; top: 1px; - width: calc(100% - var(--ha-space-8)); + width: calc(100% - var(--ha-space-4)); } .section-title.show { @@ -846,4 +886,8 @@ declare global { interface HTMLElementTagNameMap { "ha-picker-combo-box": HaPickerComboBox; } + + interface HASSDomEvents { + "index-selected": { index: number }; + } } diff --git a/src/components/ha-wa-dialog.ts b/src/components/ha-wa-dialog.ts index 79034e32fc..9f546e1297 100644 --- a/src/components/ha-wa-dialog.ts +++ b/src/components/ha-wa-dialog.ts @@ -1,7 +1,7 @@ import "@home-assistant/webawesome/dist/components/dialog/dialog"; import type WaDialog from "@home-assistant/webawesome/dist/components/dialog/dialog"; import { mdiClose } from "@mdi/js"; -import { css, html, LitElement } from "lit"; +import { css, html, LitElement, nothing } from "lit"; import { customElement, eventOptions, @@ -106,6 +106,9 @@ export class HaWaDialog extends ScrollableFadeMixin(LitElement) { @property({ type: Boolean, reflect: true, attribute: "flexcontent" }) public flexContent = false; + @property({ type: Boolean, attribute: "without-header" }) + public withoutHeader = false; + @state() private _open = false; @@ -147,29 +150,35 @@ export class HaWaDialog extends ScrollableFadeMixin(LitElement) { @wa-after-show=${this._handleAfterShow} @wa-after-hide=${this._handleAfterHide} > - - - - - - ${this.headerTitle !== undefined - ? html` - ${this.headerTitle} - ` - : html``} - ${this.headerSubtitle !== undefined - ? html`${this.headerSubtitle}` - : html``} - - - + ${!this.withoutHeader + ? html` + + + + + ${this.headerTitle !== undefined + ? html` + ${this.headerTitle} + ` + : html``} + ${this.headerSubtitle !== undefined + ? html`${this.headerSubtitle}` + : html``} + + + ` + : nothing}
diff --git a/src/components/target-picker/ha-target-picker-item-row.ts b/src/components/target-picker/ha-target-picker-item-row.ts index b1886a1844..2d69622da7 100644 --- a/src/components/target-picker/ha-target-picker-item-row.ts +++ b/src/components/target-picker/ha-target-picker-item-row.ts @@ -20,7 +20,7 @@ import { computeDomain } from "../../common/entity/compute_domain"; import { computeEntityName } from "../../common/entity/compute_entity_name"; import { getEntityContext } from "../../common/entity/context/get_entity_context"; import { computeRTL } from "../../common/util/compute_rtl"; -import type { AreaRegistryEntry } from "../../data/area_registry"; +import type { AreaRegistryEntry } from "../../data/area/area_registry"; import { getConfigEntry } from "../../data/config_entries"; import { labelsContext } from "../../data/context"; import type { DeviceRegistryEntry } from "../../data/device/device_registry"; diff --git a/src/data/area/area_picker.ts b/src/data/area/area_picker.ts new file mode 100644 index 0000000000..6c856d48f1 --- /dev/null +++ b/src/data/area/area_picker.ts @@ -0,0 +1,204 @@ +import { mdiTextureBox } from "@mdi/js"; +import { computeAreaName } from "../../common/entity/compute_area_name"; +import { computeDomain } from "../../common/entity/compute_domain"; +import { computeFloorName } from "../../common/entity/compute_floor_name"; +import { getAreaContext } from "../../common/entity/context/get_area_context"; +import type { HaDevicePickerDeviceFilterFunc } from "../../components/device/ha-device-picker"; +import type { PickerComboBoxItem } from "../../components/ha-picker-combo-box"; +import type { FuseWeightedKey } from "../../resources/fuseMultiTerm"; +import type { HomeAssistant } from "../../types"; +import { + getDeviceEntityDisplayLookup, + type DeviceEntityDisplayLookup, + type DeviceRegistryEntry, +} from "../device/device_registry"; +import type { HaEntityPickerEntityFilterFunc } from "../entity/entity"; +import type { EntityRegistryDisplayEntry } from "../entity/entity_registry"; + +export const getAreas = ( + haAreas: HomeAssistant["areas"], + haFloors: HomeAssistant["floors"], + haDevices: HomeAssistant["devices"], + haEntities: HomeAssistant["entities"], + haStates: HomeAssistant["states"], + includeDomains?: string[], + excludeDomains?: string[], + includeDeviceClasses?: string[], + deviceFilter?: HaDevicePickerDeviceFilterFunc, + entityFilter?: HaEntityPickerEntityFilterFunc, + excludeAreas?: string[], + idPrefix = "" +): 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 || + includeDeviceClasses || + deviceFilter || + entityFilter + ) { + deviceEntityLookup = getDeviceEntityDisplayLookup(entities); + inputDevices = devices; + inputEntities = entities.filter((entity) => entity.area_id); + + if (includeDomains) { + inputDevices = inputDevices!.filter((device) => { + const devEntities = deviceEntityLookup[device.id]; + if (!devEntities || !devEntities.length) { + return false; + } + return deviceEntityLookup[device.id].some((entity) => + includeDomains.includes(computeDomain(entity.entity_id)) + ); + }); + inputEntities = inputEntities!.filter((entity) => + includeDomains.includes(computeDomain(entity.entity_id)) + ); + } + + if (excludeDomains) { + inputDevices = inputDevices!.filter((device) => { + const devEntities = deviceEntityLookup[device.id]; + if (!devEntities || !devEntities.length) { + return true; + } + return entities.every( + (entity) => !excludeDomains.includes(computeDomain(entity.entity_id)) + ); + }); + inputEntities = inputEntities!.filter( + (entity) => !excludeDomains.includes(computeDomain(entity.entity_id)) + ); + } + + if (includeDeviceClasses) { + inputDevices = inputDevices!.filter((device) => { + const devEntities = deviceEntityLookup[device.id]; + if (!devEntities || !devEntities.length) { + return false; + } + return deviceEntityLookup[device.id].some((entity) => { + const stateObj = haStates[entity.entity_id]; + if (!stateObj) { + return false; + } + return ( + stateObj.attributes.device_class && + includeDeviceClasses.includes(stateObj.attributes.device_class) + ); + }); + }); + inputEntities = inputEntities!.filter((entity) => { + const stateObj = haStates[entity.entity_id]; + return ( + stateObj.attributes.device_class && + includeDeviceClasses.includes(stateObj.attributes.device_class) + ); + }); + } + + if (deviceFilter) { + inputDevices = inputDevices!.filter((device) => deviceFilter!(device)); + } + + if (entityFilter) { + inputDevices = inputDevices!.filter((device) => { + const devEntities = deviceEntityLookup[device.id]; + if (!devEntities || !devEntities.length) { + return false; + } + return deviceEntityLookup[device.id].some((entity) => { + const stateObj = haStates[entity.entity_id]; + if (!stateObj) { + return false; + } + return entityFilter(stateObj); + }); + }); + inputEntities = inputEntities!.filter((entity) => { + const stateObj = haStates[entity.entity_id]; + if (!stateObj) { + return false; + } + return entityFilter!(stateObj); + }); + } + } + + let outputAreas = areas; + + let areaIds: string[] | undefined; + + if (inputDevices) { + areaIds = inputDevices + .filter((device) => device.area_id) + .map((device) => device.area_id!); + } + + if (inputEntities) { + areaIds = (areaIds ?? []).concat( + inputEntities + .filter((entity) => entity.area_id) + .map((entity) => entity.area_id!) + ); + } + + if (areaIds) { + outputAreas = outputAreas.filter((area) => areaIds!.includes(area.area_id)); + } + + if (excludeAreas) { + outputAreas = outputAreas.filter( + (area) => !excludeAreas!.includes(area.area_id) + ); + } + + const items = outputAreas.map((area) => { + const { floor } = getAreaContext(area, haFloors); + const floorName = floor ? computeFloorName(floor) : undefined; + const areaName = computeAreaName(area); + return { + id: `${idPrefix}${area.area_id}`, + primary: areaName || area.area_id, + secondary: floorName, + icon: area.icon || undefined, + icon_path: area.icon ? undefined : mdiTextureBox, + search_labels: { + areaId: area.area_id, + aliases: area.aliases.join(" "), + }, + }; + }); + + return items; +}; + +export const areaComboBoxKeys: FuseWeightedKey[] = [ + { + name: "primary", + weight: 10, + }, + { + name: "search_labels.aliases", + weight: 8, + }, + { + name: "secondary", + weight: 6, + }, + { + name: "search_labels.domain", + weight: 4, + }, + { + name: "search_labels.areaId", + weight: 2, + }, +]; diff --git a/src/data/area_registry.ts b/src/data/area/area_registry.ts similarity index 90% rename from src/data/area_registry.ts rename to src/data/area/area_registry.ts index e5174e0869..afa86d0f31 100644 --- a/src/data/area_registry.ts +++ b/src/data/area/area_registry.ts @@ -1,12 +1,12 @@ -import type { HomeAssistant } from "../types"; -import type { DeviceRegistryEntry } from "./device/device_registry"; +import type { HomeAssistant } from "../../types"; +import type { DeviceRegistryEntry } from "../device/device_registry"; import type { EntityRegistryDisplayEntry, EntityRegistryEntry, -} from "./entity/entity_registry"; -import type { RegistryEntry } from "./registry"; +} from "../entity/entity_registry"; +import type { RegistryEntry } from "../registry"; -export { subscribeAreaRegistry } from "./ws-area_registry"; +export { subscribeAreaRegistry } from "../ws-area_registry"; export interface AreaRegistryEntry extends RegistryEntry { aliases: string[]; diff --git a/src/data/area_floor_picker.ts b/src/data/area_floor_picker.ts index 984ba51a41..61a4d68366 100644 --- a/src/data/area_floor_picker.ts +++ b/src/data/area_floor_picker.ts @@ -6,7 +6,7 @@ import type { HaDevicePickerDeviceFilterFunc } from "../components/device/ha-dev import type { PickerComboBoxItem } from "../components/ha-picker-combo-box"; import type { FuseWeightedKey } from "../resources/fuseMultiTerm"; import type { HomeAssistant } from "../types"; -import type { AreaRegistryEntry } from "./area_registry"; +import type { AreaRegistryEntry } from "./area/area_registry"; import { getDeviceEntityDisplayLookup, type DeviceEntityDisplayLookup, diff --git a/src/data/floor_registry.ts b/src/data/floor_registry.ts index 65cf336856..71a8d9f4fd 100644 --- a/src/data/floor_registry.ts +++ b/src/data/floor_registry.ts @@ -1,5 +1,5 @@ import type { HomeAssistant } from "../types"; -import type { AreaRegistryEntry } from "./area_registry"; +import type { AreaRegistryEntry } from "./area/area_registry"; import type { RegistryEntry } from "./registry"; export { subscribeAreaRegistry } from "./ws-area_registry"; diff --git a/src/data/quick_bar.ts b/src/data/quick_bar.ts new file mode 100644 index 0000000000..edc3d5b849 --- /dev/null +++ b/src/data/quick_bar.ts @@ -0,0 +1,327 @@ +import { + mdiKeyboard, + mdiNavigationVariant, + mdiPuzzle, + mdiReload, + mdiServerNetwork, + mdiStorePlus, +} from "@mdi/js"; +import { canShowPage } from "../common/config/can_show_page"; +import { componentsWithService } from "../common/config/components_with_service"; +import { isComponentLoaded } from "../common/config/is_component_loaded"; +import type { PickerComboBoxItem } from "../components/ha-picker-combo-box"; +import type { PageNavigation } from "../layouts/hass-tabs-subpage"; +import { configSections } from "../panels/config/ha-panel-config"; +import type { HomeAssistant } from "../types"; +import type { HassioAddonInfo } from "./hassio/addon"; +import { domainToName } from "./integration"; +import { getPanelIcon, getPanelNameTranslationKey } from "./panel"; +import type { FuseWeightedKey } from "../resources/fuseMultiTerm"; + +export interface NavigationComboBoxItem extends PickerComboBoxItem { + path: string; + image?: string; + iconColor?: string; +} + +export interface BaseNavigationCommand { + path: string; + primary: string; + icon_path?: string; + iconPath?: string; + iconColor?: string; + image?: string; +} + +export interface ActionCommandComboBoxItem extends PickerComboBoxItem { + action: string; + domain?: string; +} + +export interface NavigationInfo extends PageNavigation { + primary: string; +} + +const generateNavigationPanelCommands = ( + localize: HomeAssistant["localize"], + panels: HomeAssistant["panels"], + addons?: HassioAddonInfo[] +): BaseNavigationCommand[] => + Object.entries(panels) + .filter( + ([panelKey]) => panelKey !== "_my_redirect" && panelKey !== "hassio" + ) + .map(([_panelKey, panel]) => { + const translationKey = getPanelNameTranslationKey(panel); + const icon = getPanelIcon(panel) || "mdi:view-dashboard"; + + const primary = localize(translationKey) || panel.title || panel.url_path; + + let image: string | undefined; + + if (addons) { + const addon = addons.find(({ slug }) => slug === panel.url_path); + if (addon) { + image = addon.icon + ? `/api/hassio/addons/${addon.slug}/icon` + : undefined; + } + } + + return { + primary, + icon, + image, + path: `/${panel.url_path}`, + }; + }); + +const getNavigationInfoFromConfig = ( + localize: HomeAssistant["localize"], + page: PageNavigation +): NavigationInfo | undefined => { + const path = page.path.substring(1); + + let name = path.substring(path.indexOf("/") + 1); + name = name.indexOf("/") > -1 ? name.substring(0, name.indexOf("/")) : name; + + const caption = + (name && localize(`ui.dialogs.quick-bar.commands.navigation.${name}`)) || + // @ts-expect-error + (page.translationKey && localize(page.translationKey)); + + if (caption) { + return { ...page, primary: caption }; + } + + return undefined; +}; + +const generateNavigationConfigSectionCommands = ( + hass: HomeAssistant +): BaseNavigationCommand[] => { + if (!hass.user?.is_admin) { + return []; + } + + const items: NavigationInfo[] = []; + + Object.values(configSections).forEach((sectionPages) => { + sectionPages.forEach((page) => { + if (!canShowPage(hass, page)) { + return; + } + + const info = getNavigationInfoFromConfig(hass.localize, page); + + if (!info) { + return; + } + // Add to list, but only if we do not already have an entry for the same path and component + if (items.some((e) => e.path === info.path)) { + return; + } + + items.push(info); + }); + }); + + return items; +}; + +const finalizeNavigationCommands = ( + localize: HomeAssistant["localize"], + items: BaseNavigationCommand[] +): NavigationComboBoxItem[] => + items.map((item, index) => { + const secondary = localize( + "ui.dialogs.quick-bar.commands.types.navigation" + ); + return { + id: `navigation_${index}_${item.path}`, + icon_path: item.iconPath || mdiNavigationVariant, + secondary, + sorting_label: `${item.primary}_${secondary}`, + ...item, + }; + }); + +export const generateNavigationCommands = ( + hass: HomeAssistant, + addons?: HassioAddonInfo[] +): NavigationComboBoxItem[] => { + const panelItems = generateNavigationPanelCommands( + hass.localize, + hass.panels, + addons + ); + const sectionItems = generateNavigationConfigSectionCommands(hass); + const supervisorItems: BaseNavigationCommand[] = []; + if (hass.user?.is_admin && isComponentLoaded(hass, "hassio")) { + supervisorItems.push({ + path: "/hassio/store", + icon_path: mdiStorePlus, + primary: hass.localize( + "ui.dialogs.quick-bar.commands.navigation.addon_store" + ), + }); + supervisorItems.push({ + path: "/hassio/dashboard", + icon_path: mdiPuzzle, + primary: hass.localize( + "ui.dialogs.quick-bar.commands.navigation.addon_dashboard" + ), + }); + if (addons) { + for (const addon of addons.filter((a) => a.version)) { + supervisorItems.push({ + path: `/hassio/addon/${addon.slug}`, + image: addon.icon + ? `/api/hassio/addons/${addon.slug}/icon` + : undefined, + primary: hass.localize( + "ui.dialogs.quick-bar.commands.navigation.addon_info", + { addon: addon.name } + ), + }); + } + } + } + + const additionalItems = [ + { + path: "", + primary: hass.localize( + "ui.dialogs.quick-bar.commands.navigation.shortcuts" + ), + icon_path: mdiKeyboard, + }, + ]; + + return finalizeNavigationCommands(hass.localize, [ + ...panelItems, + ...sectionItems, + ...supervisorItems, + ...additionalItems, + ]); +}; + +const generateReloadCommands = ( + hass: HomeAssistant +): ActionCommandComboBoxItem[] => { + // Get all domains that have a direct "reload" service + const reloadableDomains = componentsWithService(hass, "reload"); + + const commands = reloadableDomains.map((domain) => ({ + primary: + hass.localize(`ui.dialogs.quick-bar.commands.reload.${domain}`) || + hass.localize("ui.dialogs.quick-bar.commands.reload.reload", { + domain: domainToName(hass.localize, domain), + }), + domain, + action: "reload", + icon_path: mdiReload, + secondary: hass.localize(`ui.dialogs.quick-bar.commands.types.reload`), + })); + + // Add "frontend.reload_themes" + commands.push({ + primary: hass.localize("ui.dialogs.quick-bar.commands.reload.themes"), + domain: "frontend", + action: "reload_themes", + icon_path: mdiReload, + secondary: hass.localize("ui.dialogs.quick-bar.commands.types.reload"), + }); + + // Add "homeassistant.reload_core_config" + commands.push({ + primary: hass.localize("ui.dialogs.quick-bar.commands.reload.core"), + domain: "homeassistant", + action: "reload_core_config", + icon_path: mdiReload, + secondary: hass.localize("ui.dialogs.quick-bar.commands.types.reload"), + }); + + // Add "homeassistant.reload_all" + commands.push({ + primary: hass.localize("ui.dialogs.quick-bar.commands.reload.all"), + domain: "homeassistant", + action: "reload_all", + icon_path: mdiReload, + secondary: hass.localize("ui.dialogs.quick-bar.commands.types.reload"), + }); + + return commands.map((command, index) => ({ + ...command, + id: `command_${index}_${command.primary}`, + sorting_label: `${command.primary}_${command.secondary}_${command.domain}`, + })); +}; + +const generateServerControlCommands = ( + hass: HomeAssistant +): ActionCommandComboBoxItem[] => { + const serverActions = ["restart", "stop"] as const; + + return serverActions.map((action, index) => { + const primary = hass.localize( + "ui.dialogs.quick-bar.commands.server_control.perform_action", + { + action: hass.localize( + `ui.dialogs.quick-bar.commands.server_control.${action}` + ), + } + ); + + const secondary = hass.localize( + "ui.dialogs.quick-bar.commands.types.server_control" + ); + + return { + id: `server_control_${index}_${action}`, + primary, + domain: "homeassistant", + icon_path: mdiServerNetwork, + secondary, + sorting_label: `${primary}_${secondary}_${action}`, + action, + }; + }); +}; + +export const generateActionCommands = ( + hass: HomeAssistant +): ActionCommandComboBoxItem[] => [ + ...generateReloadCommands(hass), + ...generateServerControlCommands(hass), +]; + +export const commandComboBoxKeys: FuseWeightedKey[] = [ + { + name: "primary", + weight: 10, + }, + { + name: "domain", + weight: 8, + }, + { + name: "secondary", + weight: 6, + }, +]; + +export const navigateComboBoxKeys: FuseWeightedKey[] = [ + { + name: "primary", + weight: 10, + }, + { + name: "path", + weight: 8, + }, + { + name: "secondary", + weight: 6, + }, +]; diff --git a/src/data/target.ts b/src/data/target.ts index 25d3f741b1..975ba5d9c1 100644 --- a/src/data/target.ts +++ b/src/data/target.ts @@ -3,8 +3,8 @@ import { computeDomain } from "../common/entity/compute_domain"; import type { HaDevicePickerDeviceFilterFunc } from "../components/device/ha-device-picker"; import type { PickerComboBoxItem } from "../components/ha-picker-combo-box"; import type { HomeAssistant } from "../types"; +import type { AreaRegistryEntry } from "./area/area_registry"; import type { FloorComboBoxItem } from "./area_floor_picker"; -import type { AreaRegistryEntry } from "./area_registry"; import type { DevicePickerItem } from "./device/device_picker"; import type { DeviceRegistryEntry } from "./device/device_registry"; import type { HaEntityPickerEntityFilterFunc } from "./entity/entity"; diff --git a/src/data/ws-area_registry.ts b/src/data/ws-area_registry.ts index aaeee08392..a6113796b3 100644 --- a/src/data/ws-area_registry.ts +++ b/src/data/ws-area_registry.ts @@ -2,7 +2,7 @@ import type { Connection } from "home-assistant-js-websocket"; import { createCollection } from "home-assistant-js-websocket"; import type { Store } from "home-assistant-js-websocket/dist/store"; import { debounce } from "../common/util/debounce"; -import type { AreaRegistryEntry } from "./area_registry"; +import type { AreaRegistryEntry } from "./area/area_registry"; const fetchAreaRegistry = (conn: Connection) => conn.sendMessagePromise({ diff --git a/src/dialogs/quick-bar/ha-quick-bar.ts b/src/dialogs/quick-bar/ha-quick-bar.ts index 1c2fda3896..77298fe1b5 100644 --- a/src/dialogs/quick-bar/ha-quick-bar.ts +++ b/src/dialogs/quick-bar/ha-quick-bar.ts @@ -1,1140 +1,747 @@ -import type { ListItem } from "@material/mwc-list/mwc-list-item"; -import { - mdiClose, - mdiConsoleLine, - mdiDevices, - mdiEarth, - mdiKeyboard, - mdiMagnify, - mdiReload, - mdiServerNetwork, -} from "@mdi/js"; +import { mdiDevices } from "@mdi/js"; import Fuse from "fuse.js"; -import type { TemplateResult } from "lit"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property, query, state } from "lit/decorators"; -import { ifDefined } from "lit/directives/if-defined"; -import { styleMap } from "lit/directives/style-map"; import memoizeOne from "memoize-one"; -import { canShowPage } from "../../common/config/can_show_page"; -import { componentsWithService } from "../../common/config/components_with_service"; import { isComponentLoaded } from "../../common/config/is_component_loaded"; import { fireEvent } from "../../common/dom/fire_event"; -import { computeAreaName } from "../../common/entity/compute_area_name"; -import { computeDeviceNameDisplay } from "../../common/entity/compute_device_name"; -import { computeDomain } from "../../common/entity/compute_domain"; -import { entityUseDeviceName } from "../../common/entity/compute_entity_name"; -import { computeStateName } from "../../common/entity/compute_state_name"; -import { getDeviceContext } from "../../common/entity/context/get_device_context"; import { navigate } from "../../common/navigate"; import { caseInsensitiveStringCompare } from "../../common/string/compare"; -import type { ScorableTextItem } from "../../common/string/filter/sequence-matching"; -import { computeRTL } from "../../common/util/compute_rtl"; -import { debounce } from "../../common/util/debounce"; -import "../../components/ha-button"; -import "../../components/ha-icon-button"; -import "../../components/ha-label"; -import "../../components/ha-list"; -import "../../components/ha-md-list-item"; +import "../../components/entity/state-badge"; +import "../../components/ha-adaptive-dialog"; +import "../../components/ha-combo-box-item"; +import "../../components/ha-domain-icon"; +import "../../components/ha-icon"; +import "../../components/ha-picker-combo-box"; +import type { + HaPickerComboBox, + PickerComboBoxItem, +} from "../../components/ha-picker-combo-box"; import "../../components/ha-spinner"; -import "../../components/ha-textfield"; +import "../../components/ha-svg-icon"; import "../../components/ha-tip"; -import { getConfigEntries } from "../../data/config_entries"; -import { fetchHassioAddonsInfo } from "../../data/hassio/addon"; -import { domainToName } from "../../data/integration"; -import { getPanelNameTranslationKey } from "../../data/panel"; -import type { PageNavigation } from "../../layouts/hass-tabs-subpage"; -import { configSections } from "../../panels/config/ha-panel-config"; -import { multiTermSortedSearch } from "../../resources/fuseMultiTerm"; +import { areaComboBoxKeys, getAreas } from "../../data/area/area_picker"; +import { getConfigEntries, type ConfigEntry } from "../../data/config_entries"; import { - haStyleDialog, - haStyleDialogFixedTop, - haStyleScrollbar, -} from "../../resources/styles"; -import { loadVirtualizer } from "../../resources/virtualizer"; + deviceComboBoxKeys, + getDevices, + type DevicePickerItem, +} from "../../data/device/device_picker"; +import { + entityComboBoxKeys, + getEntities, + type EntityComboBoxItem, +} from "../../data/entity/entity_picker"; +import { + fetchHassioAddonsInfo, + type HassioAddonInfo, +} from "../../data/hassio/addon"; +import { + commandComboBoxKeys, + generateActionCommands, + generateNavigationCommands, + navigateComboBoxKeys, + type ActionCommandComboBoxItem, + type NavigationComboBoxItem, +} from "../../data/quick_bar"; +import { + multiTermSortedSearch, + type FuseWeightedKey, +} from "../../resources/fuseMultiTerm"; import type { HomeAssistant } from "../../types"; -import { brandsUrl } from "../../util/brands-url"; +import { isIosApp } from "../../util/is_ios"; import { showConfirmationDialog } from "../generic/show-dialog-box"; import { showShortcutsDialog } from "../shortcuts/show-shortcuts-dialog"; -import { QuickBarMode, type QuickBarParams } from "./show-dialog-quick-bar"; +import type { QuickBarParams, QuickBarSection } from "./show-dialog-quick-bar"; -const SEARCH_KEYS = [ - { name: "primaryText", weight: 10 }, - { name: "altText", weight: 8 }, - { name: "friendlyName", weight: 8 }, - { name: "area", weight: 6 }, - { name: "translatedDomain", weight: 5 }, - { name: "entityId", weight: 4 }, // for technical search -]; - -interface QuickBarItem extends ScorableTextItem { - id: string; - primaryText: string; - iconPath?: string; - action(data?: any): void; -} - -interface CommandItem extends QuickBarItem { - categoryKey: "reload" | "navigation" | "server_control"; - categoryText: string; -} - -interface EntityItem extends QuickBarItem { - altText: string; - icon?: TemplateResult; - translatedDomain: string; - entityId: string; - friendlyName: string; -} - -interface DeviceItem extends QuickBarItem { - deviceId: string; - domain?: string; - translatedDomain?: string; - area?: string; -} - -const isCommandItem = (item: QuickBarItem): item is CommandItem => - (item as CommandItem).categoryKey !== undefined; - -const isDeviceItem = (item: QuickBarItem): item is DeviceItem => - (item as DeviceItem).deviceId !== undefined; - -interface QuickBarNavigationItem extends CommandItem { - path: string; -} - -type NavigationInfo = PageNavigation & Pick; - -type BaseNavigationCommand = Pick< - QuickBarNavigationItem, - "primaryText" | "path" ->; +const SEPARATOR = "________"; @customElement("ha-quick-bar") export class QuickBar extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @state() private _commandItems?: CommandItem[]; - - @state() private _entityItems?: EntityItem[]; - - @state() private _deviceItems?: DeviceItem[]; - - @state() private _filter = ""; - - @state() private _search = ""; - @state() private _open = false; - @state() private _opened = false; - - @state() private _narrow = false; + @state() private _loading = true; @state() private _hint?: string; - @state() private _mode = QuickBarMode.Entity; + @state() private _selectedSection?: QuickBarSection; - @query("ha-textfield", false) private _filterInputField?: HTMLElement; + @state() private _opened = false; - private _focusSet = false; + @query("ha-picker-combo-box") private _comboBox?: HaPickerComboBox; - private _focusListElement?: ListItem | null; + private get _showEntityId() { + return this.hass.userData?.showEntityIdPicker; + } + private _configEntryLookup: Record = {}; + + private _addons?: HassioAddonInfo[]; + + private _translationsLoaded = false; + + // #region lifecycle public async showDialog(params: QuickBarParams) { - this._mode = params.mode || QuickBarMode.Entity; + if (!this._translationsLoaded) { + this._fetchTranslations(); + this._translationsLoaded = true; + } + this._initialize(); + this._selectedSection = params.mode; this._hint = params.hint; - this._narrow = matchMedia( - "all and (max-width: 450px), all and (max-height: 500px)" - ).matches; - this._initializeItemsIfNeeded(); this._open = true; } + private async _fetchTranslations() { + await this.hass.loadBackendTranslation("title"); + } + + private async _initialize() { + try { + const configEntries = await getConfigEntries(this.hass); + this._configEntryLookup = Object.fromEntries( + configEntries.map((entry) => [entry.entry_id, entry]) + ); + } catch (err) { + // eslint-disable-next-line no-console + console.error("Error fetching config entries for quick bar", err); + } + + if (this.hass.user?.is_admin && isComponentLoaded(this.hass, "hassio")) { + try { + const hassioAddonsInfo = await fetchHassioAddonsInfo(this.hass); + this._addons = hassioAddonsInfo.addons; + } catch (err) { + // eslint-disable-next-line no-console + console.error("Error fetching hassio addons for quick bar", err); + } + } + + this._loading = false; + } + + private _dialogOpened = async () => { + this._opened = true; + requestAnimationFrame(() => { + if (this.hass && isIosApp(this.hass)) { + this.hass.auth.external!.fireMessage({ + type: "focus_element", + payload: { + element_id: "combo-box", + }, + }); + return; + } + this._comboBox?.focus(); + }); + }; + + // be sure to reload ha-picker-combo-box when adaptive-dialog mode changes + private _showTriggered = () => { + this._opened = false; + }; + public closeDialog() { this._open = false; + return true; + } + + private _dialogClosed = () => { + this._selectedSection = undefined; this._opened = false; - this._focusSet = false; - this._filter = ""; - this._search = ""; - this._entityItems = undefined; - this._commandItems = undefined; + this._open = false; fireEvent(this, "dialog-closed", { dialog: this.localName }); - } + }; - protected willUpdate() { - if (!this.hasUpdated) { - loadVirtualizer(); - } - } + // #endregion lifecycle - private _getItems = memoizeOne( - ( - mode: QuickBarMode, - commandItems, - entityItems, - deviceItems, - filter: string - ) => { - let items = entityItems; - - if (mode === QuickBarMode.Command) { - items = commandItems; - } else if (mode === QuickBarMode.Device) { - items = deviceItems; - } - - if (items && filter && filter !== " ") { - return this._filterItems(items, filter); - } - return items; - } - ); + // #region render protected render() { if (!this._open) { return nothing; } - const items: QuickBarItem[] | undefined = this._getItems( - this._mode, - this._commandItems, - this._entityItems, - this._deviceItems, - this._filter - ); - - const translationKey = - this._mode === QuickBarMode.Device - ? "filter_placeholder_devices" - : "filter_placeholder"; - const placeholder = this.hass.localize( - `ui.dialogs.quick-bar.${translationKey}` - ); - - const commandMode = this._mode === QuickBarMode.Command; - const deviceMode = this._mode === QuickBarMode.Device; - const icon = commandMode - ? mdiConsoleLine - : deviceMode - ? mdiDevices - : mdiMagnify; - const searchPrefix = commandMode ? ">" : deviceMode ? "#" : ""; + const sections = [ + { + id: "navigate", + label: this.hass.localize("ui.dialogs.quick-bar.navigate_title"), + }, + ...(this.hass.user?.is_admin + ? [ + "separator" as const, + { + id: "command", + label: this.hass.localize("ui.dialogs.quick-bar.commands_title"), + }, + ] + : []), + "separator" as const, + { + id: "entity", + label: this.hass.localize("ui.components.target-picker.type.entities"), + }, + ...(this.hass.user?.is_admin + ? [ + { + id: "device", + label: this.hass.localize( + "ui.components.target-picker.type.devices" + ), + }, + { + id: "area", + label: this.hass.localize( + "ui.components.target-picker.type.areas" + ), + }, + ] + : []), + ]; return html` - -
- - - ${this._search || this._narrow - ? html` -
- ${this._search && - html``} - ${this._narrow - ? html` - - ${this.hass!.localize("ui.common.close")} - - ` - : ""} -
- ` - : ""} -
-
- ${!items - ? html`` - : items.length === 0 - ? html` -
- ${this.hass.localize("ui.dialogs.quick-bar.nothing_found")} -
- ` - : html` - - ${this._opened - ? html` - ` - : ""} - - `} + ${!this._loading && this._opened + ? html`` + : nothing} ${this._hint - ? html`${this._hint}` - : ""} -
+ ? html`${this._hint}` + : nothing} + `; } - private async _initializeItemsIfNeeded() { - if (this._mode === QuickBarMode.Command) { - this._commandItems = - this._commandItems || (await this._generateCommandItems()); - } else if (this._mode === QuickBarMode.Device) { - this._deviceItems = - this._deviceItems || (await this._generateDeviceItems()); - } else { - this._entityItems = - this._entityItems || (await this._generateEntityItems()); - } - } - - private _handleOpened() { - this._opened = true; - } - - private async _handleRangeChanged(e) { - if (this._focusSet) { - return; - } - if (e.firstVisible > -1) { - this._focusSet = true; - await this.updateComplete; - this._setFocusFirstListItem(); - } - } - - private _renderItem = (item: QuickBarItem, index: number) => { + private _renderRow = ( + item: + | NavigationComboBoxItem + | ActionCommandComboBoxItem + | EntityComboBoxItem + | DevicePickerItem + ) => { if (!item) { return nothing; } - if (isDeviceItem(item)) { - return this._renderDeviceItem(item, index); - } + const iconPath = item.icon_path || mdiDevices; - if (isCommandItem(item)) { - return this._renderCommandItem(item, index); - } - - return this._renderEntityItem(item as EntityItem, index); + return html` + + ${"stateObj" in item && item.stateObj + ? html` + + ` + : "domain" in item && item.domain + ? html` + + ` + : "image" in item && item.image + ? html` + ${item.primary + ` + : item.icon + ? html`` + : "iconColor" in item && item.iconColor + ? html` +
+ +
+ ` + : html` + + `} + ${item.primary} + ${item.secondary + ? html`${item.secondary}` + : nothing} + ${"stateObj" in item && !!this._showEntityId + ? html` + + ${item.stateObj?.entity_id} + + ` + : nothing} + ${"domain_name" in item && + (!("stateObj" in item) || !this._showEntityId) + ? html` +
+ ${(item as EntityComboBoxItem).domain_name} +
+ ` + : nothing} +
+ `; }; - private _renderDeviceItem(item: DeviceItem, index?: number) { - return html` - - ${item.domain - ? html`` - : nothing} - ${item.primaryText} - ${item.area - ? html` ${item.area} ` - : nothing} - ${item.translatedDomain - ? html`
- ${item.translatedDomain} -
` - : nothing} -
- `; - } - - private _renderEntityItem(item: EntityItem, index?: number) { - const showEntityId = this.hass.userData?.showEntityIdPicker; - - return html` - - ${item.iconPath - ? html` - - ` - : html`${item.icon}`} - ${item.primaryText} - ${item.altText - ? html` ${item.altText} ` - : nothing} - ${item.entityId && showEntityId - ? html` - ${item.entityId} - ` - : nothing} - ${item.translatedDomain && !showEntityId - ? html`
- ${item.translatedDomain} -
` - : nothing} -
- `; - } - - private _renderCommandItem(item: CommandItem, index?: number) { - return html` - - - - ${item.iconPath - ? html` - - ` - : nothing} - ${item.categoryText} - - - - ${item.primaryText} - - `; - } - - private async _processItemAndCloseDialog(item: QuickBarItem, index: number) { - this._addSpinnerToCommandItem(index); - - await item.action(); - this.closeDialog(); - } - - private _handleInputKeyDown(ev: KeyboardEvent) { - if (ev.code === "Enter") { - const firstItem = this._getItemAtIndex(0); - if (!firstItem || firstItem.style.display === "none") { - return; - } - this._processItemAndCloseDialog((firstItem as any).item, 0); - } else if (ev.code === "ArrowDown") { - ev.preventDefault(); - this._getItemAtIndex(0)?.focus(); - this._focusSet = true; - this._focusListElement = this._getItemAtIndex(0); - } - } - - private _getItemAtIndex(index: number): ListItem | null { - return this.renderRoot.querySelector(`ha-md-list-item[index="${index}"]`); - } - - private _addSpinnerToCommandItem(index: number): void { - const div = document.createElement("div"); - div.slot = "meta"; + private _getRowSpinner = memoizeOne(() => { const spinner = document.createElement("ha-spinner"); spinner.size = "small"; - div.appendChild(spinner); - this._getItemAtIndex(index)?.appendChild(div); - } + spinner.style.marginRight = "16px"; + spinner.style.position = "absolute"; + spinner.style.right = "0"; + return spinner; + }); - private _handleSearchChange(ev: CustomEvent): void { - const newFilter = (ev.currentTarget as any).value; - const oldMode = this._mode; - const oldSearch = this._search; - let newMode: QuickBarMode; - let newSearch: string; - - if (newFilter.startsWith(">")) { - newMode = QuickBarMode.Command; - newSearch = newFilter.substring(1); - } else if (newFilter.startsWith("#")) { - newMode = QuickBarMode.Device; - newSearch = newFilter.substring(1); - } else { - newMode = QuickBarMode.Entity; - newSearch = newFilter; + private _sectionTitleFunction = ({ + firstIndex, + lastIndex, + firstItem, + secondItem, + itemsCount, + }: { + firstIndex: number; + lastIndex: number; + firstItem: PickerComboBoxItem | string; + secondItem: PickerComboBoxItem | string; + itemsCount: number; + }) => { + if ( + firstItem === undefined || + secondItem === undefined || + typeof firstItem === "string" || + (typeof secondItem === "string" && secondItem !== "padding") || + (firstIndex === 0 && lastIndex === itemsCount - 1) + ) { + return undefined; } - if (oldMode === newMode && oldSearch === newSearch) { - return; - } + const type = + "action" in firstItem + ? this.hass.localize("ui.dialogs.quick-bar.commands_title") + : "path" in firstItem + ? this.hass.localize("ui.dialogs.quick-bar.navigate_title") + : "stateObj" in firstItem + ? this.hass.localize("ui.components.target-picker.type.entities") + : "domain" in firstItem + ? this.hass.localize("ui.components.target-picker.type.devices") + : this.hass.localize("ui.components.target-picker.type.areas"); - this._mode = newMode; - this._search = newSearch; + return type; + }; - if (this._hint) { - this._hint = undefined; - } + // #endregion render - if (oldMode !== this._mode) { - this._focusSet = false; - this._initializeItemsIfNeeded(); - this._filter = this._search; - } else { - if (this._focusSet && this._focusListElement) { - this._focusSet = false; - // @ts-ignore - this._focusListElement.rippleHandlers.endFocus(); - } - this._debouncedSetFilter(this._search); - } - } + // #region data - private _clearSearch() { - this._search = ""; - this._filter = ""; - } - - private _debouncedSetFilter = debounce((filter: string) => { - this._filter = filter; - }, 100); - - private _setFocusFirstListItem() { - // @ts-ignore - this._getItemAtIndex(0)?.rippleHandlers.startFocus(); - this._focusListElement = this._getItemAtIndex(0); - } - - private _handleListItemKeyDown(ev: KeyboardEvent) { - const isSingleCharacter = ev.key.length === 1; - const index = (ev.target as HTMLElement).getAttribute("index"); - const isFirstListItem = index === "0"; - this._focusListElement = ev.target as ListItem; - if (ev.key === "ArrowDown") { - this._getItemAtIndex(Number(index) + 1)?.focus(); - } - if (ev.key === "ArrowUp") { - if (isFirstListItem) { - this._filterInputField?.focus(); - } else { - this._getItemAtIndex(Number(index) - 1)?.focus(); - } - } - if (ev.key === "Enter" || ev.key === " ") { - this._processItemAndCloseDialog( - (ev.target as any).item, - Number((ev.target as HTMLElement).getAttribute("index")) - ); - } - if (ev.key === "Backspace" || isSingleCharacter) { - (ev.currentTarget as HTMLElement).scrollTop = 0; - this._filterInputField?.focus(); - } - } - - private _handleItemClick(ev) { - const listItem = ev.target.closest("ha-md-list-item"); - this._processItemAndCloseDialog( - listItem.item, - Number(listItem.getAttribute("index")) + private _getItems = (searchString: string, section: string) => { + this._selectedSection = section as QuickBarSection | undefined; + return this._getItemsMemoized( + this._configEntryLookup, + searchString, + this._selectedSection ); - } + }; - private async _generateDeviceItems(): Promise { - const configEntries = await getConfigEntries(this.hass); - const configEntryLookup = Object.fromEntries( - configEntries.map((entry) => [entry.entry_id, entry]) - ); + private _getItemsMemoized = memoizeOne( + ( + configEntryLookup: Record, + filter?: string, + section?: QuickBarSection + ) => { + const items: (string | PickerComboBoxItem)[] = []; - return Object.values(this.hass.devices) - .filter((device) => !device.disabled_by) - .map((device) => { - const deviceName = computeDeviceNameDisplay(device, this.hass); + if (!section || section === "navigate") { + let navigateItems = this._generateNavigationCommandsMemoized( + this.hass, + this._addons + ).sort(this._sortBySortingLabel); - const { area } = getDeviceContext(device, this.hass); + if (filter) { + navigateItems = this._filterGroup( + "navigate", + navigateItems, + filter, + navigateComboBoxKeys + ) as NavigationComboBoxItem[]; + } - const areaName = area ? computeAreaName(area) : undefined; + if (!section && navigateItems.length) { + // show group title + items.push(this.hass.localize("ui.dialogs.quick-bar.navigate_title")); + } - const deviceItem = { - id: device.id, - primaryText: deviceName, - deviceId: device.id, - area: areaName, - action: () => navigate(`/config/devices/device/${device.id}`), - }; + items.push(...navigateItems); + } - const configEntry = device.primary_config_entry - ? configEntryLookup[device.primary_config_entry] - : undefined; - - const domain = configEntry?.domain; - const translatedDomain = domain - ? domainToName(this.hass.localize, domain) - : undefined; - - return { - ...deviceItem, - domain, - translatedDomain, - strings: [deviceName, areaName, domain, domainToName].filter( - Boolean - ) as string[], - }; - }) - .sort((a, b) => - caseInsensitiveStringCompare( - a.primaryText, - b.primaryText, - this.hass.locale.language - ) - ); - } - - private async _generateEntityItems(): Promise { - const isRTL = computeRTL(this.hass); - - await this.hass.loadBackendTranslation("title"); - - return Object.keys(this.hass.states) - .map((entityId) => { - const stateObj = this.hass.states[entityId]; - - const friendlyName = computeStateName(stateObj); // Keep this for search - - const useDeviceName = entityUseDeviceName( - stateObj, - this.hass.entities, - this.hass.devices + if (this.hass.user?.is_admin && (!section || section === "command")) { + let commandItems = this._generateActionCommandsMemoized(this.hass).sort( + this._sortBySortingLabel ); - const name = this.hass.formatEntityName( - stateObj, - useDeviceName ? { type: "device" } : { type: "entity" } + if (filter) { + commandItems = this._filterGroup( + "command", + commandItems, + filter, + commandComboBoxKeys + ) as ActionCommandComboBoxItem[]; + } + + if (!section && commandItems.length) { + // show group title + items.push(this.hass.localize("ui.dialogs.quick-bar.commands_title")); + } + + items.push(...commandItems); + } + + if (!section || section === "entity") { + let entityItems = this._getEntitiesMemoized(this.hass).sort( + this._sortBySortingLabel ); - const primary = name || entityId; + if (filter) { + entityItems = this._filterGroup( + "entity", + entityItems, + filter, + entityComboBoxKeys + ) as EntityComboBoxItem[]; + } - const secondary = this.hass.formatEntityName( - stateObj, - useDeviceName - ? [{ type: "area" }] - : [{ type: "area" }, { type: "device" }], - { - separator: isRTL ? " ◂ " : " ▸ ", - } - ); + if (!section && entityItems.length) { + // show group title + items.push( + this.hass.localize("ui.components.target-picker.type.entities") + ); + } - const translatedDomain = domainToName( - this.hass.localize, - computeDomain(entityId) - ); + items.push(...entityItems); + } - const entityItem = { - id: `entity-${entityId}`, - primaryText: primary, - altText: secondary, - icon: html` - - `, - translatedDomain: translatedDomain, - entityId: entityId, - friendlyName: friendlyName, - action: () => fireEvent(this, "hass-more-info", { entityId }), - }; + if (this.hass.user?.is_admin && (!section || section === "device")) { + let deviceItems = this._getDevicesMemoized( + this.hass, + configEntryLookup + ).sort(this._sortBySortingLabel); - return { - ...entityItem, - strings: [entityItem.primaryText, entityItem.altText], - }; - }) - .sort((a, b) => - caseInsensitiveStringCompare( - a.primaryText, - b.primaryText, - this.hass.locale.language - ) - ); - } + if (filter) { + deviceItems = this._filterGroup( + "device", + deviceItems, + filter, + deviceComboBoxKeys + ); + } - private async _generateCommandItems(): Promise { - return [ - ...(await this._generateReloadCommands()), - ...this._generateServerControlCommands(), - ...(await this._generateNavigationCommands()), - ].sort((a, b) => - caseInsensitiveStringCompare( - a.strings.join(" "), - b.strings.join(" "), - this.hass.locale.language + if (!section && deviceItems.length) { + // show group title + items.push( + this.hass.localize("ui.components.target-picker.type.devices") + ); + } + + items.push(...deviceItems); + } + + if (this.hass.user?.is_admin && (!section || section === "area")) { + let areaItems = this._getAreasMemoized(this.hass); + + if (filter) { + areaItems = this._filterGroup( + "area", + areaItems, + filter, + areaComboBoxKeys + ); + } + + if (!section && areaItems.length) { + // show group title + items.push( + this.hass.localize("ui.components.target-picker.type.areas") + ); + } + + items.push(...areaItems); + } + + return items; + } + ); + + private _getEntitiesMemoized = memoizeOne((hass: HomeAssistant) => + getEntities( + hass, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + `entity${SEPARATOR}` + ) + ); + + private _getDevicesMemoized = memoizeOne( + (hass: HomeAssistant, configEntryLookup: Record) => + getDevices( + hass, + configEntryLookup, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + `device${SEPARATOR}` ) + ); + + private _getAreasMemoized = memoizeOne((hass: HomeAssistant) => + getAreas( + hass.areas, + hass.floors, + hass.devices, + hass.entities, + hass.states, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + `area${SEPARATOR}` + ) + ); + + private _generateNavigationCommandsMemoized = memoizeOne( + generateNavigationCommands + ); + + private _generateActionCommandsMemoized = memoizeOne(generateActionCommands); + + private _createFuseIndex = (states, keys: FuseWeightedKey[]) => + Fuse.createIndex(keys, states); + + private _fuseIndexes = { + entity: memoizeOne((states: PickerComboBoxItem[]) => + this._createFuseIndex(states, entityComboBoxKeys) + ), + device: memoizeOne((states: PickerComboBoxItem[]) => + this._createFuseIndex(states, deviceComboBoxKeys) + ), + area: memoizeOne((states: PickerComboBoxItem[]) => + this._createFuseIndex(states, areaComboBoxKeys) + ), + command: memoizeOne((states: PickerComboBoxItem[]) => + this._createFuseIndex(states, commandComboBoxKeys) + ), + navigate: memoizeOne((states: PickerComboBoxItem[]) => + this._createFuseIndex(states, navigateComboBoxKeys) + ), + }; + + private _filterGroup( + type: QuickBarSection, + items: PickerComboBoxItem[], + searchTerm: string, + weightedKeys: FuseWeightedKey[] + ) { + const fuseIndex = this._fuseIndexes[type](items); + + return multiTermSortedSearch( + items, + searchTerm, + weightedKeys, + (item: PickerComboBoxItem) => item.id, + fuseIndex ); } - private async _generateReloadCommands(): Promise { - // Get all domains that have a direct "reload" service - const reloadableDomains = componentsWithService(this.hass, "reload"); - - const localize = await this.hass.loadBackendTranslation( - "title", - reloadableDomains + private _sortBySortingLabel = (entityA, entityB) => + caseInsensitiveStringCompare( + (entityA as PickerComboBoxItem).sorting_label!, + (entityB as PickerComboBoxItem).sorting_label!, + this.hass.locale.language ); - const commands = reloadableDomains.map((domain) => ({ - primaryText: - this.hass.localize(`ui.dialogs.quick-bar.commands.reload.${domain}`) || - this.hass.localize("ui.dialogs.quick-bar.commands.reload.reload", { - domain: domainToName(localize, domain), - }), - action: () => this.hass.callService(domain, "reload"), - iconPath: mdiReload, - categoryText: this.hass.localize( - `ui.dialogs.quick-bar.commands.types.reload` - ), - })); + // #endregion data - // Add "frontend.reload_themes" - commands.push({ - primaryText: this.hass.localize( - "ui.dialogs.quick-bar.commands.reload.themes" - ), - action: () => this.hass.callService("frontend", "reload_themes"), - iconPath: mdiReload, - categoryText: this.hass.localize( - "ui.dialogs.quick-bar.commands.types.reload" - ), - }); + // #region interaction - // Add "homeassistant.reload_core_config" - commands.push({ - primaryText: this.hass.localize( - "ui.dialogs.quick-bar.commands.reload.core" - ), - action: () => - this.hass.callService("homeassistant", "reload_core_config"), - iconPath: mdiReload, - categoryText: this.hass.localize( - "ui.dialogs.quick-bar.commands.types.reload" - ), - }); + private async _handleItemSelected(ev: CustomEvent<{ index: number }>) { + if (this._comboBox && this._comboBox.virtualizerElement) { + const index = ev.detail.index; + const item = this._comboBox.virtualizerElement.items[ + index + ] as PickerComboBoxItem; - // Add "homeassistant.reload_all" - commands.push({ - primaryText: this.hass.localize( - "ui.dialogs.quick-bar.commands.reload.all" - ), - action: () => this.hass.callService("homeassistant", "reload_all"), - iconPath: mdiReload, - categoryText: this.hass.localize( - "ui.dialogs.quick-bar.commands.types.reload" - ), - }); + // entity selected + if (item && "stateObj" in item) { + this.closeDialog(); + fireEvent(this, "hass-more-info", { + entityId: item.search_labels!.entityId, + }); + return; + } - return commands.map((command, index) => ({ - ...command, - id: `command_${index}_${command.primaryText}`, - categoryKey: "reload", - strings: [`${command.categoryText} ${command.primaryText}`], - })); - } + // device selected + if (item && item.id.startsWith(`device${SEPARATOR}`)) { + this.closeDialog(); + navigate(`/config/devices/device/${item.id.split(SEPARATOR)[1]}`); + return; + } - private _generateServerControlCommands(): CommandItem[] { - const serverActions = ["restart", "stop"] as const; + // area selected + if (item && item.id.startsWith(`area${SEPARATOR}`)) { + this.closeDialog(); + navigate(`/config/areas/area/${item.id.split(SEPARATOR)[1]}`); + return; + } - return serverActions.map((action, index) => { - const categoryKey: CommandItem["categoryKey"] = "server_control"; - - const item = { - id: `server_control_${index}_${action}`, - primaryText: this.hass.localize( - "ui.dialogs.quick-bar.commands.server_control.perform_action", - { - action: this.hass.localize( - `ui.dialogs.quick-bar.commands.server_control.${action}` - ), - } - ), - iconPath: mdiServerNetwork, - categoryText: this.hass.localize( - `ui.dialogs.quick-bar.commands.types.${categoryKey}` - ), - categoryKey, - action: async () => { + // command selected + if (item && "action" in item) { + const actionItem = item as ActionCommandComboBoxItem; + if (actionItem.action === "restart" || actionItem.action === "stop") { const confirmed = await showConfirmationDialog(this, { title: this.hass.localize( - `ui.dialogs.restart.${action}.confirm_title` + `ui.dialogs.restart.${actionItem.action}.confirm_title` ), text: this.hass.localize( - `ui.dialogs.restart.${action}.confirm_description` + `ui.dialogs.restart.${actionItem.action}.confirm_description` ), confirmText: this.hass.localize( - `ui.dialogs.restart.${action}.confirm_action` + `ui.dialogs.restart.${actionItem.action}.confirm_action` ), destructive: true, }); if (!confirmed) { return; } - this.hass.callService("homeassistant", action); - }, - }; - return { - ...item, - strings: [`${item.categoryText} ${item.primaryText}`], - }; - }); - } + this.hass.callService(actionItem.domain!, actionItem.action); + this.closeDialog(); + return; + } - private async _generateNavigationCommands(): Promise { - const panelItems = this._generateNavigationPanelCommands(); - const sectionItems = this._generateNavigationConfigSectionCommands(); - const supervisorItems: BaseNavigationCommand[] = []; - if (isComponentLoaded(this.hass, "hassio")) { - const addonsInfo = await fetchHassioAddonsInfo(this.hass); - supervisorItems.push({ - path: "/hassio/store", - primaryText: this.hass.localize( - "ui.dialogs.quick-bar.commands.navigation.addon_store" - ), - }); - supervisorItems.push({ - path: "/hassio/dashboard", - primaryText: this.hass.localize( - "ui.dialogs.quick-bar.commands.navigation.addon_dashboard" - ), - }); - for (const addon of addonsInfo.addons.filter((a) => a.version)) { - supervisorItems.push({ - path: `/hassio/addon/${addon.slug}`, - primaryText: this.hass.localize( - "ui.dialogs.quick-bar.commands.navigation.addon_info", - { addon: addon.name } - ), - }); + const element = this._comboBox.virtualizerElement.querySelector( + `#list-item-${index}` + ) as HTMLDivElement | null; + + if (element) { + element.style.backgroundColor = + "var(--ha-color-fill-primary-normal-resting)"; + element.prepend(this._getRowSpinner()); + } + + await this.hass.callService(actionItem.domain!, actionItem.action); + + this.closeDialog(); + return; + } + + // navigation selected + if (item && "path" in item) { + this.closeDialog(); + + if (!item.path) { + showShortcutsDialog(this); + return; + } + + navigate((item as NavigationComboBoxItem).path); } } - - const additionalItems = [ - { - path: "", - primaryText: this.hass.localize( - "ui.dialogs.quick-bar.commands.navigation.shortcuts" - ), - action: () => showShortcutsDialog(this), - iconPath: mdiKeyboard, - }, - ]; - - return this._finalizeNavigationCommands([ - ...panelItems, - ...sectionItems, - ...supervisorItems, - ...additionalItems, - ]); } - private _generateNavigationPanelCommands(): BaseNavigationCommand[] { - return Object.keys(this.hass.panels) - .filter( - (panelKey) => panelKey !== "_my_redirect" && panelKey !== "hassio" - ) - .map((panelKey) => { - const panel = this.hass.panels[panelKey]; - const translationKey = getPanelNameTranslationKey(panel); + // #endregion interaction - const primaryText = - this.hass.localize(translationKey) || panel.title || panel.url_path; + // #region styles - return { - primaryText, - path: `/${panel.url_path}`, - }; - }); - } - - private _generateNavigationConfigSectionCommands(): BaseNavigationCommand[] { - const items: NavigationInfo[] = []; - - for (const sectionKey of Object.keys(configSections)) { - for (const page of configSections[sectionKey]) { - if (!canShowPage(this.hass, page)) { - continue; - } - - const info = this._getNavigationInfoFromConfig(page); - - if (!info) { - continue; - } - // Add to list, but only if we do not already have an entry for the same path and component - if (items.some((e) => e.path === info.path)) { - continue; - } - - items.push(info); - } - } - - return items; - } - - private _getNavigationInfoFromConfig( - page: PageNavigation - ): NavigationInfo | undefined { - const path = page.path.substring(1); - - let name = path.substring(path.indexOf("/") + 1); - name = name.indexOf("/") > -1 ? name.substring(0, name.indexOf("/")) : name; - - const caption = - (name && - this.hass.localize( - `ui.dialogs.quick-bar.commands.navigation.${name}` - )) || - // @ts-expect-error - (page.translationKey && this.hass.localize(page.translationKey)); - - if (caption) { - return { ...page, primaryText: caption }; - } - - return undefined; - } - - private _finalizeNavigationCommands( - items: BaseNavigationCommand[] - ): CommandItem[] { - return items.map((item, index) => { - const categoryKey: CommandItem["categoryKey"] = "navigation"; - - const navItem = { - id: `navigation_${index}_${item.path}`, - iconPath: mdiEarth, - categoryText: this.hass.localize( - `ui.dialogs.quick-bar.commands.types.${categoryKey}` - ), - action: () => navigate(item.path), - ...item, - }; - - return { - ...navItem, - strings: [`${navItem.categoryText} ${navItem.primaryText}`], - categoryKey, - }; - }); - } - - private _fuseIndex = memoizeOne((items: QuickBarItem[]) => - Fuse.createIndex(SEARCH_KEYS, items) - ); - - private _filterItems = memoizeOne( - (items: QuickBarItem[], filter: string): QuickBarItem[] => { - const index = this._fuseIndex(items); - - return multiTermSortedSearch( - items, - filter, - SEARCH_KEYS, - (item) => item.id, - index + static styles = css` + :host { + --dialog-surface-margin-top: var(--ha-space-10); + --ha-dialog-min-height: 620px; + --ha-bottom-sheet-height: calc( + 100vh - max(var(--safe-area-inset-top), 48px) ); + --ha-bottom-sheet-height: calc( + 100dvh - max(var(--safe-area-inset-top), 48px) + ); + --ha-bottom-sheet-max-height: calc( + 100vh - max(var(--safe-area-inset-top), 48px) + ); + --ha-bottom-sheet-max-height: calc( + 100dvh - max(var(--safe-area-inset-top), 48px) + ); + --dialog-content-padding: 0; + --safe-area-inset-bottom: 0px; } - ); - static get styles() { - return [ - haStyleScrollbar, - haStyleDialog, - haStyleDialogFixedTop, - css` - ha-list { - position: relative; - --mdc-list-vertical-padding: 0; - } - .heading { - display: flex; - align-items: center; - --mdc-theme-primary: var(--primary-text-color); - } + ha-tip { + display: flex; + justify-content: center; + align-items: center; + color: var(--secondary-text-color); + gap: var(--ha-space-1); + } - .heading ha-textfield { - flex-grow: 1; - } + ha-tip a { + color: var(--primary-color); + } - ha-dialog { - --dialog-z-index: 9; - --dialog-content-padding: 0; - } + @media all and (max-width: 450px), all and (max-height: 690px) { + ha-tip { + display: none; + } + } + `; - @media (min-width: 800px) { - ha-dialog { - --mdc-dialog-max-width: 800px; - --mdc-dialog-min-width: 500px; - --mdc-dialog-max-height: calc( - 100vh - var(--ha-space-18) - var(--safe-area-inset-y) - ); - } - } - - @media all and (max-width: 450px), all and (max-height: 500px) { - ha-textfield { - --mdc-shape-small: 0; - } - } - - @media all and (max-width: 450px), all and (max-height: 690px) { - .hint { - display: none; - } - } - - ha-svg-icon.prefix { - color: var(--primary-text-color); - } - - ha-textfield ha-icon-button { - --mdc-icon-button-size: 24px; - color: var(--primary-text-color); - } - - .command-category { - --ha-label-icon-color: #585858; - --ha-label-text-color: #212121; - } - - .command-category.reload { - --ha-label-background-color: #cddc39; - } - - .command-category.navigation { - --ha-label-background-color: var(--light-primary-color); - } - - .command-category.server_control { - --ha-label-background-color: var(--warning-color); - } - - span.command-text { - margin-left: var(--ha-space-2); - margin-inline-start: var(--ha-space-2); - margin-inline-end: initial; - direction: var(--direction); - } - - ha-md-list-item { - width: 100%; - } - - /* Fixed height for items because we are use virtualizer */ - ha-md-list-item.two-line { - --md-list-item-one-line-container-height: 64px; - --md-list-item-two-line-container-height: 64px; - --md-list-item-top-space: var(--ha-space-2); - --md-list-item-bottom-space: var(--ha-space-2); - } - - ha-md-list-item.three-line { - width: 100%; - --md-list-item-one-line-container-height: 72px; - --md-list-item-two-line-container-height: 72px; - --md-list-item-three-line-container-height: 72px; - --md-list-item-top-space: var(--ha-space-2); - --md-list-item-bottom-space: var(--ha-space-2); - } - - ha-md-list-item .code { - font-family: var(--ha-font-family-code); - font-size: var(--ha-font-size-xs); - } - - ha-md-list-item .domain { - font-size: var(--ha-font-size-s); - font-weight: var(--ha-font-weight-normal); - line-height: var(--ha-line-height-normal); - align-self: flex-end; - max-width: 30%; - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; - } - - ha-md-list-item img { - width: 32px; - height: 32px; - } - - ha-tip { - padding: var(--ha-space-5); - } - - .nothing-found { - padding: var(--ha-space-4) 0; - text-align: center; - } - - div[slot="trailingIcon"] { - display: flex; - align-items: center; - } - - lit-virtualizer { - contain: size layout !important; - } - `, - ]; - } + // #endregion styles } declare global { diff --git a/src/dialogs/quick-bar/show-dialog-quick-bar.ts b/src/dialogs/quick-bar/show-dialog-quick-bar.ts index 2609ac8b35..b83818ad9d 100644 --- a/src/dialogs/quick-bar/show-dialog-quick-bar.ts +++ b/src/dialogs/quick-bar/show-dialog-quick-bar.ts @@ -1,14 +1,15 @@ import { fireEvent } from "../../common/dom/fire_event"; -export const enum QuickBarMode { - Command = "command", - Device = "device", - Entity = "entity", -} +export type QuickBarSection = + | "entity" + | "device" + | "area" + | "navigate" + | "command"; export interface QuickBarParams { entityFilter?: string; - mode?: QuickBarMode; + mode?: QuickBarSection; hint?: string; } diff --git a/src/dialogs/shortcuts/dialog-shortcuts.ts b/src/dialogs/shortcuts/dialog-shortcuts.ts index ced2f6e05a..4e75a02f00 100644 --- a/src/dialogs/shortcuts/dialog-shortcuts.ts +++ b/src/dialogs/shortcuts/dialog-shortcuts.ts @@ -1,11 +1,10 @@ -import { css, html, LitElement, nothing } from "lit"; +import { css, html, LitElement } from "lit"; import { customElement, property, state } from "lit/decorators"; import { fireEvent } from "../../common/dom/fire_event"; import type { LocalizeKeys } from "../../common/translations/localize"; import "../../components/ha-alert"; -import { createCloseHeading } from "../../components/ha-dialog"; import "../../components/ha-svg-icon"; -import { haStyleDialog } from "../../resources/styles"; +import "../../components/ha-wa-dialog"; import type { HomeAssistant } from "../../types"; import { isMac } from "../../util/is_mac"; @@ -38,6 +37,10 @@ const _SHORTCUTS: Section[] = [ { textTranslationKey: "ui.dialogs.shortcuts.searching.on_any_page", }, + { + shortcut: [CTRL_CMD, "K"], + descriptionTranslationKey: "ui.dialogs.shortcuts.searching.search", + }, { shortcut: ["C"], descriptionTranslationKey: @@ -165,17 +168,22 @@ const _SHORTCUTS: Section[] = [ class DialogShortcuts extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @state() private _opened = false; + @state() private _open = false; public async showDialog(): Promise { - this._opened = true; + this._open = true; } - public async closeDialog(): Promise { - this._opened = false; + private _dialogClosed() { + this._open = false; fireEvent(this, "dialog-closed", { dialog: this.localName }); } + public async closeDialog() { + this._open = false; + return true; + } + private _renderShortcut( shortcutKeys: ShortcutString[], descriptionKey: LocalizeKeys @@ -202,20 +210,11 @@ class DialogShortcuts extends LitElement { } protected render() { - if (!this._opened) { - return nothing; - } - return html` -
${_SHORTCUTS.map( @@ -238,7 +237,7 @@ class DialogShortcuts extends LitElement { )}
- + ${this.hass.localize("ui.dialogs.shortcuts.enable_shortcuts_hint", { user_profile: html`${this.hass.localize( @@ -247,25 +246,12 @@ class DialogShortcuts extends LitElement { >`, })} -
+ `; } static styles = [ - haStyleDialog, css` - ha-dialog { - --dialog-z-index: 15; - } - - h3:first-of-type { - margin-top: 0; - } - - .content { - margin-bottom: 24px; - } - .shortcut { display: flex; flex-direction: row; @@ -287,6 +273,10 @@ class DialogShortcuts extends LitElement { ha-svg-icon { width: 12px; } + + ha-alert a { + color: var(--primary-color); + } `, ]; } diff --git a/src/panels/config/areas/dialog-area-registry-detail.ts b/src/panels/config/areas/dialog-area-registry-detail.ts index 6071e66e15..60ff671fe7 100644 --- a/src/panels/config/areas/dialog-area-registry-detail.ts +++ b/src/panels/config/areas/dialog-area-registry-detail.ts @@ -21,8 +21,8 @@ import "../../../components/ha-wa-dialog"; import type { AreaRegistryEntry, AreaRegistryEntryMutableParams, -} from "../../../data/area_registry"; -import { deleteAreaRegistryEntry } from "../../../data/area_registry"; +} from "../../../data/area/area_registry"; +import { deleteAreaRegistryEntry } from "../../../data/area/area_registry"; import { SENSOR_DEVICE_CLASS_HUMIDITY, SENSOR_DEVICE_CLASS_TEMPERATURE, diff --git a/src/panels/config/areas/dialog-areas-floors-order.ts b/src/panels/config/areas/dialog-areas-floors-order.ts index b0eec9ca6e..c4b5278d1e 100644 --- a/src/panels/config/areas/dialog-areas-floors-order.ts +++ b/src/panels/config/areas/dialog-areas-floors-order.ts @@ -14,16 +14,16 @@ import "../../../components/ha-button"; import "../../../components/ha-dialog-footer"; import "../../../components/ha-floor-icon"; import "../../../components/ha-icon"; -import "../../../components/ha-wa-dialog"; import "../../../components/ha-md-list"; import "../../../components/ha-md-list-item"; import "../../../components/ha-sortable"; import "../../../components/ha-svg-icon"; -import type { AreaRegistryEntry } from "../../../data/area_registry"; +import "../../../components/ha-wa-dialog"; +import type { AreaRegistryEntry } from "../../../data/area/area_registry"; import { reorderAreaRegistryEntries, updateAreaRegistryEntry, -} from "../../../data/area_registry"; +} from "../../../data/area/area_registry"; import { reorderFloorRegistryEntries } from "../../../data/floor_registry"; import { haStyle, haStyleDialog } from "../../../resources/styles"; import type { HomeAssistant } from "../../../types"; diff --git a/src/panels/config/areas/dialog-floor-registry-detail.ts b/src/panels/config/areas/dialog-floor-registry-detail.ts index 231a652d9d..1354b0a587 100644 --- a/src/panels/config/areas/dialog-floor-registry-detail.ts +++ b/src/panels/config/areas/dialog-floor-registry-detail.ts @@ -18,7 +18,7 @@ import "../../../components/ha-settings-row"; import "../../../components/ha-svg-icon"; import "../../../components/ha-textfield"; import "../../../components/ha-wa-dialog"; -import { updateAreaRegistryEntry } from "../../../data/area_registry"; +import { updateAreaRegistryEntry } from "../../../data/area/area_registry"; import type { FloorRegistryEntry, FloorRegistryEntryMutableParams, diff --git a/src/panels/config/areas/ha-config-area-page.ts b/src/panels/config/areas/ha-config-area-page.ts index 6264010e15..3b20d744ef 100644 --- a/src/panels/config/areas/ha-config-area-page.ts +++ b/src/panels/config/areas/ha-config-area-page.ts @@ -23,11 +23,11 @@ import "../../../components/ha-icon-next"; import "../../../components/ha-list"; import "../../../components/ha-list-item"; import "../../../components/ha-tooltip"; -import type { AreaRegistryEntry } from "../../../data/area_registry"; +import type { AreaRegistryEntry } from "../../../data/area/area_registry"; import { deleteAreaRegistryEntry, updateAreaRegistryEntry, -} from "../../../data/area_registry"; +} from "../../../data/area/area_registry"; import type { AutomationEntity } from "../../../data/automation"; import { fullEntitiesContext } from "../../../data/context"; import type { DeviceRegistryEntry } from "../../../data/device/device_registry"; diff --git a/src/panels/config/areas/ha-config-areas-dashboard.ts b/src/panels/config/areas/ha-config-areas-dashboard.ts index 31a40a765a..502df07cc5 100644 --- a/src/panels/config/areas/ha-config-areas-dashboard.ts +++ b/src/panels/config/areas/ha-config-areas-dashboard.ts @@ -31,12 +31,12 @@ import "../../../components/ha-list-item"; import "../../../components/ha-sortable"; import type { HaSortableOptions } from "../../../components/ha-sortable"; import "../../../components/ha-svg-icon"; -import type { AreaRegistryEntry } from "../../../data/area_registry"; +import type { AreaRegistryEntry } from "../../../data/area/area_registry"; import { createAreaRegistryEntry, reorderAreaRegistryEntries, updateAreaRegistryEntry, -} from "../../../data/area_registry"; +} from "../../../data/area/area_registry"; import type { FloorRegistryEntry } from "../../../data/floor_registry"; import { createFloorRegistryEntry, diff --git a/src/panels/config/areas/show-dialog-area-registry-detail.ts b/src/panels/config/areas/show-dialog-area-registry-detail.ts index ca12d2b87a..f7e79b864a 100644 --- a/src/panels/config/areas/show-dialog-area-registry-detail.ts +++ b/src/panels/config/areas/show-dialog-area-registry-detail.ts @@ -2,7 +2,7 @@ import { fireEvent } from "../../../common/dom/fire_event"; import type { AreaRegistryEntry, AreaRegistryEntryMutableParams, -} from "../../../data/area_registry"; +} from "../../../data/area/area_registry"; export interface AreaRegistryDetailDialogParams { entry?: AreaRegistryEntry; diff --git a/src/panels/config/automation/add-automation-element-dialog.ts b/src/panels/config/automation/add-automation-element-dialog.ts index 72099734c2..f42d331856 100644 --- a/src/panels/config/automation/add-automation-element-dialog.ts +++ b/src/panels/config/automation/add-automation-element-dialog.ts @@ -57,11 +57,11 @@ import { ACTION_COLLECTIONS, ACTION_ICONS, } from "../../../data/action"; -import type { FloorComboBoxItem } from "../../../data/area_floor_picker"; import { getAreaDeviceLookup, getAreaEntityLookup, -} from "../../../data/area_registry"; +} from "../../../data/area/area_registry"; +import type { FloorComboBoxItem } from "../../../data/area_floor_picker"; import { DYNAMIC_PREFIX, getValueFromDynamic, diff --git a/src/panels/config/automation/add-automation-element/ha-automation-add-from-target.ts b/src/panels/config/automation/add-automation-element/ha-automation-add-from-target.ts index 7caa9a148e..e2cf97b4a2 100644 --- a/src/panels/config/automation/add-automation-element/ha-automation-add-from-target.ts +++ b/src/panels/config/automation/add-automation-element/ha-automation-add-from-target.ts @@ -28,6 +28,10 @@ import "../../../../components/ha-md-list-item"; import "../../../../components/ha-section-title"; import "../../../../components/ha-state-icon"; import "../../../../components/ha-svg-icon"; +import { + getAreaDeviceLookup, + getAreaEntityLookup, +} from "../../../../data/area/area_registry"; import { getAreasNestedInFloors, type AreaFloorValue, @@ -35,10 +39,6 @@ import { type FloorNestedComboBoxItem, type UnassignedAreasFloorComboBoxItem, } from "../../../../data/area_floor_picker"; -import { - getAreaDeviceLookup, - getAreaEntityLookup, -} from "../../../../data/area_registry"; import { getConfigEntries, type ConfigEntry, diff --git a/src/panels/config/automation/ha-automation-picker.ts b/src/panels/config/automation/ha-automation-picker.ts index ea7d764ad5..3eb99ea442 100644 --- a/src/panels/config/automation/ha-automation-picker.ts +++ b/src/panels/config/automation/ha-automation-picker.ts @@ -67,7 +67,7 @@ import type { HaMdMenuItem } from "../../../components/ha-md-menu-item"; import "../../../components/ha-sub-menu"; import "../../../components/ha-svg-icon"; import "../../../components/ha-tooltip"; -import { createAreaRegistryEntry } from "../../../data/area_registry"; +import { createAreaRegistryEntry } from "../../../data/area/area_registry"; import type { AutomationEntity } from "../../../data/automation"; import { deleteAutomation, diff --git a/src/panels/config/dashboard/ha-config-dashboard.ts b/src/panels/config/dashboard/ha-config-dashboard.ts index c45bc75806..0d49a2d2d1 100644 --- a/src/panels/config/dashboard/ha-config-dashboard.ts +++ b/src/panels/config/dashboard/ha-config-dashboard.ts @@ -33,10 +33,7 @@ import { checkForEntityUpdates, filterUpdateEntitiesParameterized, } from "../../../data/update"; -import { - QuickBarMode, - showQuickBar, -} from "../../../dialogs/quick-bar/show-dialog-quick-bar"; +import { showQuickBar } from "../../../dialogs/quick-bar/show-dialog-quick-bar"; import { showRestartDialog } from "../../../dialogs/restart/show-dialog-restart"; import { showShortcutsDialog } from "../../../dialogs/shortcuts/show-shortcuts-dialog"; import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; @@ -375,7 +372,6 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) { }; showQuickBar(this, { - mode: QuickBarMode.Command, hint: this.hass.enableShortcuts ? this.hass.localize("ui.dialogs.quick-bar.key_c_tip", params) : undefined, diff --git a/src/panels/config/devices/ha-config-devices-dashboard.ts b/src/panels/config/devices/ha-config-devices-dashboard.ts index 5d6a9eb5f3..7bac8f3f4a 100644 --- a/src/panels/config/devices/ha-config-devices-dashboard.ts +++ b/src/panels/config/devices/ha-config-devices-dashboard.ts @@ -54,7 +54,7 @@ import "../../../components/ha-icon-button"; import "../../../components/ha-md-divider"; import "../../../components/ha-md-menu-item"; import "../../../components/ha-sub-menu"; -import { createAreaRegistryEntry } from "../../../data/area_registry"; +import { createAreaRegistryEntry } from "../../../data/area/area_registry"; import type { ConfigEntry, SubEntry } from "../../../data/config_entries"; import { getSubEntries, sortConfigEntries } from "../../../data/config_entries"; import { fullEntitiesContext } from "../../../data/context"; diff --git a/src/panels/config/scene/ha-scene-dashboard.ts b/src/panels/config/scene/ha-scene-dashboard.ts index a30425673d..6bc77d2120 100644 --- a/src/panels/config/scene/ha-scene-dashboard.ts +++ b/src/panels/config/scene/ha-scene-dashboard.ts @@ -61,7 +61,7 @@ import "../../../components/ha-state-icon"; import "../../../components/ha-sub-menu"; import "../../../components/ha-svg-icon"; import "../../../components/ha-tooltip"; -import { createAreaRegistryEntry } from "../../../data/area_registry"; +import { createAreaRegistryEntry } from "../../../data/area/area_registry"; import type { CategoryRegistryEntry } from "../../../data/category_registry"; import { createCategoryRegistryEntry, diff --git a/src/panels/config/script/ha-script-picker.ts b/src/panels/config/script/ha-script-picker.ts index e01c185347..338ba15c19 100644 --- a/src/panels/config/script/ha-script-picker.ts +++ b/src/panels/config/script/ha-script-picker.ts @@ -62,7 +62,7 @@ import "../../../components/ha-md-menu-item"; import "../../../components/ha-sub-menu"; import "../../../components/ha-svg-icon"; import "../../../components/ha-tooltip"; -import { createAreaRegistryEntry } from "../../../data/area_registry"; +import { createAreaRegistryEntry } from "../../../data/area/area_registry"; import type { CategoryRegistryEntry } from "../../../data/category_registry"; import { createCategoryRegistryEntry, diff --git a/src/panels/lovelace/card-features/hui-area-controls-card-feature.ts b/src/panels/lovelace/card-features/hui-area-controls-card-feature.ts index 3e15bc3987..846c89cf3b 100644 --- a/src/panels/lovelace/card-features/hui-area-controls-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-area-controls-card-feature.ts @@ -16,7 +16,7 @@ import "../../../components/ha-control-button"; import "../../../components/ha-control-button-group"; import "../../../components/ha-domain-icon"; import "../../../components/ha-svg-icon"; -import type { AreaRegistryEntry } from "../../../data/area_registry"; +import type { AreaRegistryEntry } from "../../../data/area/area_registry"; import { forwardHaptic } from "../../../data/haptics"; import { computeCssVariable } from "../../../resources/css-variables"; import type { HomeAssistant } from "../../../types"; diff --git a/src/panels/lovelace/hui-root.ts b/src/panels/lovelace/hui-root.ts index b1df8a08db..8313958693 100644 --- a/src/panels/lovelace/hui-root.ts +++ b/src/panels/lovelace/hui-root.ts @@ -51,7 +51,7 @@ import "../../components/ha-svg-icon"; import "../../components/ha-tab-group"; import "../../components/ha-tab-group-tab"; import "../../components/ha-tooltip"; -import { createAreaRegistryEntry } from "../../data/area_registry"; +import { createAreaRegistryEntry } from "../../data/area/area_registry"; import type { LovelacePanelConfig } from "../../data/lovelace"; import type { LovelaceConfig, @@ -72,10 +72,7 @@ import { showConfirmationDialog, } from "../../dialogs/generic/show-dialog-box"; import { showMoreInfoDialog } from "../../dialogs/more-info/show-ha-more-info-dialog"; -import { - QuickBarMode, - showQuickBar, -} from "../../dialogs/quick-bar/show-dialog-quick-bar"; +import { showQuickBar } from "../../dialogs/quick-bar/show-dialog-quick-bar"; import { showShortcutsDialog } from "../../dialogs/shortcuts/show-shortcuts-dialog"; import { showVoiceCommandDialog } from "../../dialogs/voice-command-dialog/show-ha-voice-command-dialog"; import { haStyle } from "../../resources/styles"; @@ -299,13 +296,11 @@ class HUIRoot extends LitElement { }, { icon: mdiMagnify, - key: "ui.panel.lovelace.menu.search_entities", + key: "ui.panel.lovelace.menu.search_home_assistant", buttonAction: this._showQuickBar, overflowAction: this._handleShowQuickBar, visible: !this._editMode && !this.hass.kioskMode, overflow: this.narrow, - suffix: - this.hass.enableShortcuts && !isMobileClient ? "(E)" : undefined, }, { icon: mdiCommentProcessingOutline, @@ -912,7 +907,6 @@ class HUIRoot extends LitElement { }; showQuickBar(this, { - mode: QuickBarMode.Entity, hint: this.hass.enableShortcuts ? this.hass.localize("ui.tips.key_e_tip", params) : undefined, diff --git a/src/panels/lovelace/strategies/areas/editor/hui-areas-dashboard-strategy-editor.ts b/src/panels/lovelace/strategies/areas/editor/hui-areas-dashboard-strategy-editor.ts index 8a8ffff4b5..4a8e61d0a2 100644 --- a/src/panels/lovelace/strategies/areas/editor/hui-areas-dashboard-strategy-editor.ts +++ b/src/panels/lovelace/strategies/areas/editor/hui-areas-dashboard-strategy-editor.ts @@ -13,11 +13,12 @@ import "../../../../../components/ha-entities-display-editor"; import "../../../../../components/ha-icon"; import "../../../../../components/ha-icon-button"; import "../../../../../components/ha-icon-button-prev"; +import type { DisplayItem } from "../../../../../components/ha-items-display-editor"; import "../../../../../components/ha-svg-icon"; import { updateAreaRegistryEntry, type AreaRegistryEntry, -} from "../../../../../data/area_registry"; +} from "../../../../../data/area/area_registry"; import { haCardSizeLarge, haCardSizeSmall, @@ -33,7 +34,6 @@ import { AREA_STRATEGY_GROUPS, getAreaGroupedEntities, } from "../helpers/areas-strategy-helper"; -import type { DisplayItem } from "../../../../../components/ha-items-display-editor"; @customElement("hui-areas-dashboard-strategy-editor") export class HuiAreasDashboardStrategyEditor diff --git a/src/panels/lovelace/strategies/areas/helpers/areas-strategy-helper.ts b/src/panels/lovelace/strategies/areas/helpers/areas-strategy-helper.ts index e194e09ee1..c1d5714b92 100644 --- a/src/panels/lovelace/strategies/areas/helpers/areas-strategy-helper.ts +++ b/src/panels/lovelace/strategies/areas/helpers/areas-strategy-helper.ts @@ -4,7 +4,7 @@ import type { EntityFilterFunc } from "../../../../../common/entity/entity_filte import { generateEntityFilter } from "../../../../../common/entity/entity_filter"; import { stripPrefixFromEntityName } from "../../../../../common/entity/strip_prefix_from_entity_name"; import { orderCompare } from "../../../../../common/string/compare"; -import type { AreaRegistryEntry } from "../../../../../data/area_registry"; +import type { AreaRegistryEntry } from "../../../../../data/area/area_registry"; import type { FloorRegistryEntry } from "../../../../../data/floor_registry"; import type { LovelaceCardConfig } from "../../../../../data/lovelace/config/card"; import type { HomeAssistant } from "../../../../../types"; diff --git a/src/panels/lovelace/strategies/home/home-overview-view-strategy.ts b/src/panels/lovelace/strategies/home/home-overview-view-strategy.ts index 6426053a2b..b92b7d4d1f 100644 --- a/src/panels/lovelace/strategies/home/home-overview-view-strategy.ts +++ b/src/panels/lovelace/strategies/home/home-overview-view-strategy.ts @@ -7,7 +7,7 @@ import { generateEntityFilter, } from "../../../../common/entity/entity_filter"; import { floorDefaultIcon } from "../../../../components/ha-floor-icon"; -import type { AreaRegistryEntry } from "../../../../data/area_registry"; +import type { AreaRegistryEntry } from "../../../../data/area/area_registry"; import { getEnergyPreferences } from "../../../../data/energy"; import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card"; import type { diff --git a/src/state/connection-mixin.ts b/src/state/connection-mixin.ts index b42a071126..f12085ff95 100644 --- a/src/state/connection-mixin.ts +++ b/src/state/connection-mixin.ts @@ -10,7 +10,7 @@ import { import { fireEvent } from "../common/dom/fire_event"; import { computeStateName } from "../common/entity/compute_state_name"; import { promiseTimeout } from "../common/util/promise-timeout"; -import { subscribeAreaRegistry } from "../data/area_registry"; +import { subscribeAreaRegistry } from "../data/area/area_registry"; import { broadcastConnectionStatus } from "../data/connection-status"; import { subscribeDeviceRegistry } from "../data/device/device_registry"; import { diff --git a/src/state/quick-bar-mixin.ts b/src/state/quick-bar-mixin.ts index f9bb66deeb..f0dd705560 100644 --- a/src/state/quick-bar-mixin.ts +++ b/src/state/quick-bar-mixin.ts @@ -1,22 +1,22 @@ import type { PropertyValues } from "lit"; import memoizeOne from "memoize-one"; import { isComponentLoaded } from "../common/config/is_component_loaded"; +import { canOverrideAlphanumericInput } from "../common/dom/can-override-input"; import { mainWindow } from "../common/dom/get_main_window"; -import type { QuickBarParams } from "../dialogs/quick-bar/show-dialog-quick-bar"; -import { - QuickBarMode, - showQuickBar, +import { ShortcutManager } from "../common/keyboard/shortcuts"; +import { extractSearchParamsObject } from "../common/url/search-params"; +import type { + QuickBarParams, + QuickBarSection, } from "../dialogs/quick-bar/show-dialog-quick-bar"; +import { showQuickBar } from "../dialogs/quick-bar/show-dialog-quick-bar"; +import { showShortcutsDialog } from "../dialogs/shortcuts/show-shortcuts-dialog"; +import { showVoiceCommandDialog } from "../dialogs/voice-command-dialog/show-ha-voice-command-dialog"; +import type { Redirects } from "../panels/my/ha-panel-my"; import type { Constructor, HomeAssistant } from "../types"; import { storeState } from "../util/ha-pref-storage"; import { showToast } from "../util/toast"; import type { HassElement } from "./hass-element"; -import { ShortcutManager } from "../common/keyboard/shortcuts"; -import { extractSearchParamsObject } from "../common/url/search-params"; -import { showVoiceCommandDialog } from "../dialogs/voice-command-dialog/show-ha-voice-command-dialog"; -import { canOverrideAlphanumericInput } from "../common/dom/can-override-input"; -import { showShortcutsDialog } from "../dialogs/shortcuts/show-shortcuts-dialog"; -import type { Redirects } from "../panels/my/ha-panel-my"; declare global { interface HASSDomEvents { @@ -39,13 +39,13 @@ export default >(superClass: T) => mainWindow.addEventListener("hass-quick-bar-trigger", (ev) => { switch (ev.detail.key) { case "e": - this._showQuickBar(ev.detail); + this._showQuickBar(ev.detail, "entity"); break; case "c": - this._showQuickBar(ev.detail, QuickBarMode.Command); + this._showQuickBar(ev.detail, "command"); break; case "d": - this._showQuickBar(ev.detail, QuickBarMode.Device); + this._showQuickBar(ev.detail, "device"); break; case "m": this._createMyLink(ev.detail); @@ -65,19 +65,21 @@ export default >(superClass: T) => const shortcutManager = new ShortcutManager(); shortcutManager.add({ // Those are for latin keyboards that have e, c, m keys - e: { handler: (ev) => this._showQuickBar(ev) }, - c: { handler: (ev) => this._showQuickBar(ev, QuickBarMode.Command) }, + e: { handler: (ev) => this._showQuickBar(ev, "entity") }, + c: { handler: (ev) => this._showQuickBar(ev, "command") }, m: { handler: (ev) => this._createMyLink(ev) }, a: { handler: (ev) => this._showVoiceCommandDialog(ev) }, - d: { handler: (ev) => this._showQuickBar(ev, QuickBarMode.Device) }, + d: { handler: (ev) => this._showQuickBar(ev, "device") }, + "$mod+k": { handler: (ev) => this._showQuickBar(ev) }, // Workaround see https://github.com/jamiebuilds/tinykeys/issues/130 "Shift+?": { handler: (ev) => this._showShortcutDialog(ev) }, // Those are fallbacks for non-latin keyboards that don't have e, c, m keys (qwerty-based shortcuts) - KeyE: { handler: (ev) => this._showQuickBar(ev) }, - KeyC: { handler: (ev) => this._showQuickBar(ev, QuickBarMode.Command) }, + KeyE: { handler: (ev) => this._showQuickBar(ev, "entity") }, + KeyC: { handler: (ev) => this._showQuickBar(ev, "command") }, KeyM: { handler: (ev) => this._createMyLink(ev) }, KeyA: { handler: (ev) => this._showVoiceCommandDialog(ev) }, - KeyD: { handler: (ev) => this._showQuickBar(ev, QuickBarMode.Device) }, + KeyD: { handler: (ev) => this._showQuickBar(ev, "device") }, + "$mod+KeyK": { handler: (ev) => this._showQuickBar(ev) }, }); } @@ -102,10 +104,7 @@ export default >(superClass: T) => showVoiceCommandDialog(this, this.hass!, { pipeline_id: "last_used" }); } - private _showQuickBar( - e: KeyboardEvent, - mode: QuickBarMode = QuickBarMode.Entity - ) { + private _showQuickBar(e: KeyboardEvent, mode?: QuickBarSection) { if (!this._canShowQuickBar(e)) { return; } diff --git a/src/translations/en.json b/src/translations/en.json index 315668041e..562c976d9d 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1338,6 +1338,8 @@ "text": "Home Assistant is running in safe mode, custom integrations and frontend modules are not available. Restart Home Assistant to exit safe mode." }, "quick-bar": { + "commands_title": "Commands", + "navigate_title": "Navigate", "commands": { "reload": { "all": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::all%]", @@ -1423,7 +1425,7 @@ }, "filter_placeholder": "Search entities", "filter_placeholder_devices": "Search devices", - "title": "Quick search", + "title": "Search Home Assistant", "key_c_tip": "[%key:ui::tips::key_c_tip%]", "nothing_found": "Nothing found!" }, @@ -2148,6 +2150,7 @@ "title": "Searching", "on_any_page": "On any page", "on_pages_with_tables": "On pages with tables", + "search": "Search Home Assistant", "search_command": "search command", "search_entities": "search entities", "search_devices": "search devices", @@ -7485,6 +7488,7 @@ "search_entities": "Search entities", "assist": "Assist", "assist_tooltip": "Assist", + "search_home_assistant": "Search Home Assistant", "reload_resources": "Reload resources", "exit_edit_mode": "Done", "close": "Close", diff --git a/src/types.ts b/src/types.ts index 1ffcbb824f..80f523820e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -14,7 +14,7 @@ import type { EntityNameOptions, } from "./common/entity/compute_entity_name_display"; import type { LocalizeFunc } from "./common/translations/localize"; -import type { AreaRegistryEntry } from "./data/area_registry"; +import type { AreaRegistryEntry } from "./data/area/area_registry"; import type { DeviceRegistryEntry } from "./data/device/device_registry"; import type { EntityRegistryDisplayEntry } from "./data/entity/entity_registry"; import type { FloorRegistryEntry } from "./data/floor_registry"; diff --git a/test/common/entity/compute_area_name.test.ts b/test/common/entity/compute_area_name.test.ts index cd02371ad2..f4b2eff3cf 100644 --- a/test/common/entity/compute_area_name.test.ts +++ b/test/common/entity/compute_area_name.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; import { computeAreaName } from "../../../src/common/entity/compute_area_name"; -import type { AreaRegistryEntry } from "../../../src/data/area_registry"; +import type { AreaRegistryEntry } from "../../../src/data/area/area_registry"; describe("computeAreaName", () => { it("returns the trimmed name if present", () => { diff --git a/test/common/entity/context/context-mock.ts b/test/common/entity/context/context-mock.ts index e662d22bd5..7180c0483a 100644 --- a/test/common/entity/context/context-mock.ts +++ b/test/common/entity/context/context-mock.ts @@ -1,5 +1,5 @@ import type { HassEntity } from "home-assistant-js-websocket"; -import type { AreaRegistryEntry } from "../../../../src/data/area_registry"; +import type { AreaRegistryEntry } from "../../../../src/data/area/area_registry"; import type { DeviceRegistryEntry } from "../../../../src/data/device/device_registry"; import type { EntityRegistryDisplayEntry,