mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-27 19:26:36 +00:00
Improve device picker UI and search (#25401)
* Improve device picker * Improve device search quick bar * Fix types * Fix selected device in the dropdown * Move filter to picker * Rename filters * Use generic picker * Update src/dialogs/quick-bar/ha-quick-bar.ts --------- Co-authored-by: Bram Kragten <mail@bramkragten.nl>
This commit is contained in:
parent
eda9abc3c5
commit
15fd4134d0
@ -1,33 +1,28 @@
|
|||||||
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||||
import type { HassEntity } from "home-assistant-js-websocket";
|
import type { HassEntity } from "home-assistant-js-websocket";
|
||||||
import type { PropertyValues, TemplateResult } from "lit";
|
import { html, LitElement, nothing, type PropertyValues } from "lit";
|
||||||
import { LitElement, html, nothing } from "lit";
|
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import { computeDeviceNameDisplay } from "../../common/entity/compute_device_name";
|
import { computeAreaName } from "../../common/entity/compute_area_name";
|
||||||
|
import {
|
||||||
|
computeDeviceName,
|
||||||
|
computeDeviceNameDisplay,
|
||||||
|
} from "../../common/entity/compute_device_name";
|
||||||
import { computeDomain } from "../../common/entity/compute_domain";
|
import { computeDomain } from "../../common/entity/compute_domain";
|
||||||
import { stringCompare } from "../../common/string/compare";
|
import { getDeviceContext } from "../../common/entity/context/get_device_context";
|
||||||
import type { ScorableTextItem } from "../../common/string/filter/sequence-matching";
|
import { getConfigEntries, type ConfigEntry } from "../../data/config_entries";
|
||||||
import { fuzzyFilterSort } from "../../common/string/filter/sequence-matching";
|
import {
|
||||||
import type {
|
getDeviceEntityDisplayLookup,
|
||||||
DeviceEntityDisplayLookup,
|
type DeviceEntityDisplayLookup,
|
||||||
DeviceRegistryEntry,
|
type DeviceRegistryEntry,
|
||||||
} from "../../data/device_registry";
|
} from "../../data/device_registry";
|
||||||
import { getDeviceEntityDisplayLookup } from "../../data/device_registry";
|
import { domainToName } from "../../data/integration";
|
||||||
import type { EntityRegistryDisplayEntry } from "../../data/entity_registry";
|
import type { HomeAssistant } from "../../types";
|
||||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
import { brandsUrl } from "../../util/brands-url";
|
||||||
import "../ha-combo-box";
|
import "../ha-generic-picker";
|
||||||
import type { HaComboBox } from "../ha-combo-box";
|
import type { HaGenericPicker } from "../ha-generic-picker";
|
||||||
import "../ha-combo-box-item";
|
import type { PickerComboBoxItem } from "../ha-picker-combo-box";
|
||||||
|
|
||||||
interface Device {
|
|
||||||
name: string;
|
|
||||||
area: string;
|
|
||||||
id: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
type ScorableDevice = ScorableTextItem & Device;
|
|
||||||
|
|
||||||
export type HaDevicePickerDeviceFilterFunc = (
|
export type HaDevicePickerDeviceFilterFunc = (
|
||||||
device: DeviceRegistryEntry
|
device: DeviceRegistryEntry
|
||||||
@ -35,25 +30,35 @@ export type HaDevicePickerDeviceFilterFunc = (
|
|||||||
|
|
||||||
export type HaDevicePickerEntityFilterFunc = (entity: HassEntity) => boolean;
|
export type HaDevicePickerEntityFilterFunc = (entity: HassEntity) => boolean;
|
||||||
|
|
||||||
const rowRenderer: ComboBoxLitRenderer<Device> = (item) => html`
|
interface DevicePickerItem extends PickerComboBoxItem {
|
||||||
<ha-combo-box-item type="button">
|
domain?: string;
|
||||||
<span slot="headline">${item.name}</span>
|
domain_name?: string;
|
||||||
${item.area
|
}
|
||||||
? html`<span slot="supporting-text">${item.area}</span>`
|
|
||||||
: nothing}
|
|
||||||
</ha-combo-box-item>
|
|
||||||
`;
|
|
||||||
|
|
||||||
@customElement("ha-device-picker")
|
@customElement("ha-device-picker")
|
||||||
export class HaDevicePicker extends LitElement {
|
export class HaDevicePicker extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
// eslint-disable-next-line lit/no-native-attributes
|
||||||
|
@property({ type: Boolean }) public autofocus = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public disabled = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public required = false;
|
||||||
|
|
||||||
@property() public label?: string;
|
@property() public label?: string;
|
||||||
|
|
||||||
@property() public value?: string;
|
@property() public value?: string;
|
||||||
|
|
||||||
@property() public helper?: string;
|
@property() public helper?: string;
|
||||||
|
|
||||||
|
@property() public placeholder?: string;
|
||||||
|
|
||||||
|
@property({ type: String, attribute: "search-label" })
|
||||||
|
public searchLabel?: string;
|
||||||
|
|
||||||
|
@property({ attribute: false, type: Array }) public createDomains?: string[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show only devices with entities from specific domains.
|
* Show only devices with entities from specific domains.
|
||||||
* @type {Array}
|
* @type {Array}
|
||||||
@ -92,38 +97,52 @@ export class HaDevicePicker extends LitElement {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public entityFilter?: HaDevicePickerEntityFilterFunc;
|
public entityFilter?: HaDevicePickerEntityFilterFunc;
|
||||||
|
|
||||||
@property({ type: Boolean }) public disabled = false;
|
@property({ attribute: "hide-clear-icon", type: Boolean })
|
||||||
|
public hideClearIcon = false;
|
||||||
|
|
||||||
@property({ type: Boolean }) public required = false;
|
@query("ha-generic-picker") private _picker?: HaGenericPicker;
|
||||||
|
|
||||||
@state() private _opened?: boolean;
|
@state() private _configEntryLookup: Record<string, ConfigEntry> = {};
|
||||||
|
|
||||||
@query("ha-combo-box", true) public comboBox!: HaComboBox;
|
protected firstUpdated(_changedProperties: PropertyValues): void {
|
||||||
|
super.firstUpdated(_changedProperties);
|
||||||
|
this._loadConfigEntries();
|
||||||
|
}
|
||||||
|
|
||||||
private _init = false;
|
private async _loadConfigEntries() {
|
||||||
|
const configEntries = await getConfigEntries(this.hass);
|
||||||
|
this._configEntryLookup = Object.fromEntries(
|
||||||
|
configEntries.map((entry) => [entry.entry_id, entry])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getItems = () =>
|
||||||
|
this._getDevices(
|
||||||
|
this.hass.devices,
|
||||||
|
this.hass.entities,
|
||||||
|
this._configEntryLookup,
|
||||||
|
this.includeDomains,
|
||||||
|
this.excludeDomains,
|
||||||
|
this.includeDeviceClasses,
|
||||||
|
this.deviceFilter,
|
||||||
|
this.entityFilter,
|
||||||
|
this.excludeDevices
|
||||||
|
);
|
||||||
|
|
||||||
private _getDevices = memoizeOne(
|
private _getDevices = memoizeOne(
|
||||||
(
|
(
|
||||||
devices: DeviceRegistryEntry[],
|
haDevices: HomeAssistant["devices"],
|
||||||
areas: HomeAssistant["areas"],
|
haEntities: HomeAssistant["entities"],
|
||||||
entities: EntityRegistryDisplayEntry[],
|
configEntryLookup: Record<string, ConfigEntry>,
|
||||||
includeDomains: this["includeDomains"],
|
includeDomains: this["includeDomains"],
|
||||||
excludeDomains: this["excludeDomains"],
|
excludeDomains: this["excludeDomains"],
|
||||||
includeDeviceClasses: this["includeDeviceClasses"],
|
includeDeviceClasses: this["includeDeviceClasses"],
|
||||||
deviceFilter: this["deviceFilter"],
|
deviceFilter: this["deviceFilter"],
|
||||||
entityFilter: this["entityFilter"],
|
entityFilter: this["entityFilter"],
|
||||||
excludeDevices: this["excludeDevices"]
|
excludeDevices: this["excludeDevices"]
|
||||||
): ScorableDevice[] => {
|
): DevicePickerItem[] => {
|
||||||
if (!devices.length) {
|
const devices = Object.values(haDevices);
|
||||||
return [
|
const entities = Object.values(haEntities);
|
||||||
{
|
|
||||||
id: "no_devices",
|
|
||||||
area: "",
|
|
||||||
name: this.hass.localize("ui.components.device-picker.no_devices"),
|
|
||||||
strings: [],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
let deviceEntityLookup: DeviceEntityDisplayLookup = {};
|
let deviceEntityLookup: DeviceEntityDisplayLookup = {};
|
||||||
|
|
||||||
@ -214,133 +233,158 @@ export class HaDevicePicker extends LitElement {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const outputDevices = inputDevices.map((device) => {
|
const outputDevices = inputDevices.map<DevicePickerItem>((device) => {
|
||||||
const name = computeDeviceNameDisplay(
|
const deviceName = computeDeviceNameDisplay(
|
||||||
device,
|
device,
|
||||||
this.hass,
|
this.hass,
|
||||||
deviceEntityLookup[device.id]
|
deviceEntityLookup[device.id]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { area } = getDeviceContext(device, this.hass);
|
||||||
|
|
||||||
|
const areaName = area ? computeAreaName(area) : undefined;
|
||||||
|
|
||||||
|
const configEntry = device.primary_config_entry
|
||||||
|
? configEntryLookup?.[device.primary_config_entry]
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const domain = configEntry?.domain;
|
||||||
|
const domainName = domain
|
||||||
|
? domainToName(this.hass.localize, domain)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: device.id,
|
id: device.id,
|
||||||
name:
|
label: "",
|
||||||
name ||
|
primary:
|
||||||
|
deviceName ||
|
||||||
this.hass.localize("ui.components.device-picker.unnamed_device"),
|
this.hass.localize("ui.components.device-picker.unnamed_device"),
|
||||||
area:
|
secondary: areaName,
|
||||||
device.area_id && areas[device.area_id]
|
domain: configEntry?.domain,
|
||||||
? areas[device.area_id].name
|
domain_name: domainName,
|
||||||
: this.hass.localize("ui.components.device-picker.no_area"),
|
search_labels: [deviceName, areaName, domain, domainName].filter(
|
||||||
strings: [name || ""],
|
Boolean
|
||||||
|
) as string[],
|
||||||
|
sorting_label: deviceName || "zzz",
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
if (!outputDevices.length) {
|
|
||||||
return [
|
return outputDevices;
|
||||||
{
|
|
||||||
id: "no_devices",
|
|
||||||
area: "",
|
|
||||||
name: this.hass.localize("ui.components.device-picker.no_match"),
|
|
||||||
strings: [],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
if (outputDevices.length === 1) {
|
|
||||||
return outputDevices;
|
|
||||||
}
|
|
||||||
return outputDevices.sort((a, b) =>
|
|
||||||
stringCompare(a.name || "", b.name || "", this.hass.locale.language)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
public async open() {
|
private _valueRenderer = memoizeOne(
|
||||||
await this.updateComplete;
|
(configEntriesLookup: Record<string, ConfigEntry>) => (value: string) => {
|
||||||
await this.comboBox?.open();
|
const deviceId = value;
|
||||||
}
|
const device = this.hass.devices[deviceId];
|
||||||
|
|
||||||
public async focus() {
|
if (!device) {
|
||||||
await this.updateComplete;
|
return html`<span slot="headline">${deviceId}</span>`;
|
||||||
await this.comboBox?.focus();
|
}
|
||||||
}
|
|
||||||
|
|
||||||
protected updated(changedProps: PropertyValues) {
|
const { area } = getDeviceContext(device, this.hass);
|
||||||
if (
|
|
||||||
(!this._init && this.hass) ||
|
const deviceName = device ? computeDeviceName(device) : undefined;
|
||||||
(this._init && changedProps.has("_opened") && this._opened)
|
const areaName = area ? computeAreaName(area) : undefined;
|
||||||
) {
|
|
||||||
this._init = true;
|
const primary = deviceName;
|
||||||
const devices = this._getDevices(
|
const secondary = areaName;
|
||||||
Object.values(this.hass.devices),
|
|
||||||
this.hass.areas,
|
const configEntry = device.primary_config_entry
|
||||||
Object.values(this.hass.entities),
|
? configEntriesLookup[device.primary_config_entry]
|
||||||
this.includeDomains,
|
: undefined;
|
||||||
this.excludeDomains,
|
|
||||||
this.includeDeviceClasses,
|
return html`
|
||||||
this.deviceFilter,
|
${configEntry
|
||||||
this.entityFilter,
|
? html`<img
|
||||||
this.excludeDevices
|
slot="start"
|
||||||
);
|
alt=""
|
||||||
this.comboBox.items = devices;
|
crossorigin="anonymous"
|
||||||
this.comboBox.filteredItems = devices;
|
referrerpolicy="no-referrer"
|
||||||
|
src=${brandsUrl({
|
||||||
|
domain: configEntry.domain,
|
||||||
|
type: "icon",
|
||||||
|
darkOptimized: this.hass.themes?.darkMode,
|
||||||
|
})}
|
||||||
|
/>`
|
||||||
|
: nothing}
|
||||||
|
<span slot="headline">${primary}</span>
|
||||||
|
<span slot="supporting-text">${secondary}</span>
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
}
|
);
|
||||||
|
|
||||||
|
private _rowRenderer: ComboBoxLitRenderer<DevicePickerItem> = (item) => html`
|
||||||
|
<ha-combo-box-item type="button">
|
||||||
|
${item.domain
|
||||||
|
? html`
|
||||||
|
<img
|
||||||
|
slot="start"
|
||||||
|
alt=""
|
||||||
|
crossorigin="anonymous"
|
||||||
|
referrerpolicy="no-referrer"
|
||||||
|
src=${brandsUrl({
|
||||||
|
domain: item.domain,
|
||||||
|
type: "icon",
|
||||||
|
darkOptimized: this.hass.themes.darkMode,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
|
||||||
|
<span slot="headline">${item.primary}</span>
|
||||||
|
${item.secondary
|
||||||
|
? html`<span slot="supporting-text">${item.secondary}</span>`
|
||||||
|
: nothing}
|
||||||
|
${item.domain_name
|
||||||
|
? html`
|
||||||
|
<div slot="trailing-supporting-text" class="domain">
|
||||||
|
${item.domain_name}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
</ha-combo-box-item>
|
||||||
|
`;
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
const placeholder =
|
||||||
|
this.placeholder ??
|
||||||
|
this.hass.localize("ui.components.device-picker.placeholder");
|
||||||
|
const notFoundLabel = this.hass.localize(
|
||||||
|
"ui.components.device-picker.no_match"
|
||||||
|
);
|
||||||
|
|
||||||
|
const valueRenderer = this._valueRenderer(this._configEntryLookup);
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
|
||||||
return html`
|
return html`
|
||||||
<ha-combo-box
|
<ha-generic-picker
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.label=${this.label === undefined && this.hass
|
.autofocus=${this.autofocus}
|
||||||
? this.hass.localize("ui.components.device-picker.device")
|
.label=${this.label}
|
||||||
: this.label}
|
.searchLabel=${this.searchLabel}
|
||||||
.value=${this._value}
|
.notFoundLabel=${notFoundLabel}
|
||||||
.helper=${this.helper}
|
.placeholder=${placeholder}
|
||||||
.renderer=${rowRenderer}
|
.value=${this.value}
|
||||||
.disabled=${this.disabled}
|
.rowRenderer=${this._rowRenderer}
|
||||||
.required=${this.required}
|
.getItems=${this._getItems}
|
||||||
item-id-path="id"
|
.hideClearIcon=${this.hideClearIcon}
|
||||||
item-value-path="id"
|
.valueRenderer=${valueRenderer}
|
||||||
item-label-path="name"
|
@value-changed=${this._valueChanged}
|
||||||
@opened-changed=${this._openedChanged}
|
>
|
||||||
@value-changed=${this._deviceChanged}
|
</ha-generic-picker>
|
||||||
@filter-changed=${this._filterChanged}
|
|
||||||
></ha-combo-box>
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private get _value() {
|
public async open() {
|
||||||
return this.value || "";
|
await this.updateComplete;
|
||||||
|
await this._picker?.open();
|
||||||
}
|
}
|
||||||
|
|
||||||
private _filterChanged(ev: CustomEvent): void {
|
private _valueChanged(ev) {
|
||||||
const target = ev.target as HaComboBox;
|
|
||||||
const filterString = ev.detail.value.toLowerCase();
|
|
||||||
target.filteredItems = filterString.length
|
|
||||||
? fuzzyFilterSort<ScorableDevice>(filterString, target.items || [])
|
|
||||||
: target.items;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _deviceChanged(ev: ValueChangedEvent<string>) {
|
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
let newValue = ev.detail.value;
|
const value = ev.detail.value;
|
||||||
|
|
||||||
if (newValue === "no_devices") {
|
|
||||||
newValue = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newValue !== this._value) {
|
|
||||||
this._setValue(newValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _openedChanged(ev: ValueChangedEvent<boolean>) {
|
|
||||||
this._opened = ev.detail.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _setValue(value: string) {
|
|
||||||
this.value = value;
|
this.value = value;
|
||||||
setTimeout(() => {
|
fireEvent(this, "value-changed", { value });
|
||||||
fireEvent(this, "value-changed", { value });
|
|
||||||
fireEvent(this, "change");
|
|
||||||
}, 0);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { css, html, LitElement, nothing } from "lit";
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import type { ValueChangedEvent, HomeAssistant } from "../../types";
|
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||||
import "./ha-device-picker";
|
import "./ha-device-picker";
|
||||||
import type {
|
import type {
|
||||||
HaDevicePickerDeviceFilterFunc,
|
HaDevicePickerDeviceFilterFunc,
|
||||||
|
@ -212,6 +212,10 @@ export class HaPickerComboBox extends LitElement {
|
|||||||
this.comboBox.setTextFieldValue("");
|
this.comboBox.setTextFieldValue("");
|
||||||
const newValue = ev.detail.value?.trim();
|
const newValue = ev.detail.value?.trim();
|
||||||
|
|
||||||
|
if (newValue === NO_MATCHING_ITEMS_FOUND_ID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (newValue !== this._value) {
|
if (newValue !== this._value) {
|
||||||
this._setValue(newValue);
|
this._setValue(newValue);
|
||||||
}
|
}
|
||||||
|
@ -419,7 +419,10 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
|||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
id="input"
|
id="input"
|
||||||
.type=${"device_id"}
|
.type=${"device_id"}
|
||||||
.label=${this.hass.localize(
|
.placeholder=${this.hass.localize(
|
||||||
|
"ui.components.target-picker.add_device_id"
|
||||||
|
)}
|
||||||
|
.searchLabel=${this.hass.localize(
|
||||||
"ui.components.target-picker.add_device_id"
|
"ui.components.target-picker.add_device_id"
|
||||||
)}
|
)}
|
||||||
.deviceFilter=${this.deviceFilter}
|
.deviceFilter=${this.deviceFilter}
|
||||||
|
@ -28,6 +28,7 @@ import {
|
|||||||
import { computeDomain } from "../../common/entity/compute_domain";
|
import { computeDomain } from "../../common/entity/compute_domain";
|
||||||
import { computeEntityName } from "../../common/entity/compute_entity_name";
|
import { computeEntityName } from "../../common/entity/compute_entity_name";
|
||||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||||
|
import { getDeviceContext } from "../../common/entity/context/get_device_context";
|
||||||
import { getEntityContext } from "../../common/entity/context/get_entity_context";
|
import { getEntityContext } from "../../common/entity/context/get_entity_context";
|
||||||
import { navigate } from "../../common/navigate";
|
import { navigate } from "../../common/navigate";
|
||||||
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
||||||
@ -41,6 +42,7 @@ import "../../components/ha-md-list-item";
|
|||||||
import "../../components/ha-spinner";
|
import "../../components/ha-spinner";
|
||||||
import "../../components/ha-textfield";
|
import "../../components/ha-textfield";
|
||||||
import "../../components/ha-tip";
|
import "../../components/ha-tip";
|
||||||
|
import { getConfigEntries } from "../../data/config_entries";
|
||||||
import { fetchHassioAddonsInfo } from "../../data/hassio/addon";
|
import { fetchHassioAddonsInfo } from "../../data/hassio/addon";
|
||||||
import { domainToName } from "../../data/integration";
|
import { domainToName } from "../../data/integration";
|
||||||
import { getPanelNameTranslationKey } from "../../data/panel";
|
import { getPanelNameTranslationKey } from "../../data/panel";
|
||||||
@ -50,6 +52,7 @@ import { HaFuse } from "../../resources/fuse";
|
|||||||
import { haStyleDialog, haStyleScrollbar } from "../../resources/styles";
|
import { haStyleDialog, haStyleScrollbar } from "../../resources/styles";
|
||||||
import { loadVirtualizer } from "../../resources/virtualizer";
|
import { loadVirtualizer } from "../../resources/virtualizer";
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
|
import { brandsUrl } from "../../util/brands-url";
|
||||||
import { showConfirmationDialog } from "../generic/show-dialog-box";
|
import { showConfirmationDialog } from "../generic/show-dialog-box";
|
||||||
import { showShortcutsDialog } from "../shortcuts/show-shortcuts-dialog";
|
import { showShortcutsDialog } from "../shortcuts/show-shortcuts-dialog";
|
||||||
import { QuickBarMode, type QuickBarParams } from "./show-dialog-quick-bar";
|
import { QuickBarMode, type QuickBarParams } from "./show-dialog-quick-bar";
|
||||||
@ -75,6 +78,8 @@ interface EntityItem extends QuickBarItem {
|
|||||||
|
|
||||||
interface DeviceItem extends QuickBarItem {
|
interface DeviceItem extends QuickBarItem {
|
||||||
deviceId: string;
|
deviceId: string;
|
||||||
|
domain?: string;
|
||||||
|
translatedDomain?: string;
|
||||||
area?: string;
|
area?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -297,7 +302,8 @@ export class QuickBar extends LitElement {
|
|||||||
this._commandItems =
|
this._commandItems =
|
||||||
this._commandItems || (await this._generateCommandItems());
|
this._commandItems || (await this._generateCommandItems());
|
||||||
} else if (this._mode === QuickBarMode.Device) {
|
} else if (this._mode === QuickBarMode.Device) {
|
||||||
this._deviceItems = this._deviceItems || this._generateDeviceItems();
|
this._deviceItems =
|
||||||
|
this._deviceItems || (await this._generateDeviceItems());
|
||||||
} else {
|
} else {
|
||||||
this._entityItems =
|
this._entityItems =
|
||||||
this._entityItems || (await this._generateEntityItems());
|
this._entityItems || (await this._generateEntityItems());
|
||||||
@ -344,10 +350,28 @@ export class QuickBar extends LitElement {
|
|||||||
tabindex="0"
|
tabindex="0"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
|
${item.domain
|
||||||
|
? html`<img
|
||||||
|
slot="start"
|
||||||
|
alt=""
|
||||||
|
crossorigin="anonymous"
|
||||||
|
referrerpolicy="no-referrer"
|
||||||
|
src=${brandsUrl({
|
||||||
|
domain: item.domain,
|
||||||
|
type: "icon",
|
||||||
|
darkOptimized: this.hass.themes?.darkMode,
|
||||||
|
})}
|
||||||
|
/>`
|
||||||
|
: nothing}
|
||||||
<span slot="headline">${item.primaryText}</span>
|
<span slot="headline">${item.primaryText}</span>
|
||||||
${item.area
|
${item.area
|
||||||
? html` <span slot="supporting-text">${item.area}</span> `
|
? html` <span slot="supporting-text">${item.area}</span> `
|
||||||
: nothing}
|
: nothing}
|
||||||
|
${item.translatedDomain
|
||||||
|
? html`<div slot="trailing-supporting-text" class="domain">
|
||||||
|
${item.translatedDomain}
|
||||||
|
</div>`
|
||||||
|
: nothing}
|
||||||
</ha-md-list-item>
|
</ha-md-list-item>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -549,23 +573,44 @@ export class QuickBar extends LitElement {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _generateDeviceItems(): DeviceItem[] {
|
private async _generateDeviceItems(): Promise<DeviceItem[]> {
|
||||||
|
const configEntries = await getConfigEntries(this.hass);
|
||||||
|
const configEntryLookup = Object.fromEntries(
|
||||||
|
configEntries.map((entry) => [entry.entry_id, entry])
|
||||||
|
);
|
||||||
|
|
||||||
return Object.values(this.hass.devices)
|
return Object.values(this.hass.devices)
|
||||||
.filter((device) => !device.disabled_by)
|
.filter((device) => !device.disabled_by)
|
||||||
.map((device) => {
|
.map((device) => {
|
||||||
const area = device.area_id
|
const deviceName = computeDeviceNameDisplay(device, this.hass);
|
||||||
? this.hass.areas[device.area_id]
|
|
||||||
: undefined;
|
const { area } = getDeviceContext(device, this.hass);
|
||||||
|
|
||||||
|
const areaName = area ? computeAreaName(area) : undefined;
|
||||||
|
|
||||||
const deviceItem = {
|
const deviceItem = {
|
||||||
primaryText: computeDeviceNameDisplay(device, this.hass),
|
primaryText: deviceName,
|
||||||
deviceId: device.id,
|
deviceId: device.id,
|
||||||
area: area?.name,
|
area: areaName,
|
||||||
action: () => navigate(`/config/devices/device/${device.id}`),
|
action: () => navigate(`/config/devices/device/${device.id}`),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 {
|
return {
|
||||||
...deviceItem,
|
...deviceItem,
|
||||||
strings: [deviceItem.primaryText],
|
domain,
|
||||||
|
translatedDomain,
|
||||||
|
strings: [deviceName, areaName, domain, domainToName].filter(
|
||||||
|
Boolean
|
||||||
|
) as string[],
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.sort((a, b) =>
|
.sort((a, b) =>
|
||||||
@ -1036,6 +1081,11 @@ export class QuickBar extends LitElement {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ha-md-list-item img {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
ha-tip {
|
ha-tip {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
@ -662,7 +662,8 @@
|
|||||||
"no_match": "No matching devices found",
|
"no_match": "No matching devices found",
|
||||||
"device": "Device",
|
"device": "Device",
|
||||||
"unnamed_device": "Unnamed device",
|
"unnamed_device": "Unnamed device",
|
||||||
"no_area": "No area"
|
"no_area": "No area",
|
||||||
|
"placeholder": "Select a device"
|
||||||
},
|
},
|
||||||
"category-picker": {
|
"category-picker": {
|
||||||
"clear": "Clear",
|
"clear": "Clear",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user