Use fuzzy filter/sort for target pickers (#16912)

* Use fuzzy filter/sort for target pickers

* PR suggestions

* Restore missed sort
This commit is contained in:
breakthestatic 2023-06-20 03:03:55 -07:00 committed by GitHub
parent 332af4003e
commit 3888b1c48b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 67 additions and 24 deletions

View File

@ -26,6 +26,10 @@ import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import { ValueChangedEvent, HomeAssistant } from "../../types"; import { ValueChangedEvent, HomeAssistant } from "../../types";
import "../ha-combo-box"; import "../ha-combo-box";
import type { HaComboBox } from "../ha-combo-box"; import type { HaComboBox } from "../ha-combo-box";
import {
fuzzyFilterSort,
ScorableTextItem,
} from "../../common/string/filter/sequence-matching";
interface Device { interface Device {
name: string; name: string;
@ -33,6 +37,8 @@ interface Device {
id: string; id: string;
} }
type ScorableDevice = ScorableTextItem & Device;
export type HaDevicePickerDeviceFilterFunc = ( export type HaDevicePickerDeviceFilterFunc = (
device: DeviceRegistryEntry device: DeviceRegistryEntry
) => boolean; ) => boolean;
@ -119,13 +125,14 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
deviceFilter: this["deviceFilter"], deviceFilter: this["deviceFilter"],
entityFilter: this["entityFilter"], entityFilter: this["entityFilter"],
excludeDevices: this["excludeDevices"] excludeDevices: this["excludeDevices"]
): Device[] => { ): ScorableDevice[] => {
if (!devices.length) { if (!devices.length) {
return [ return [
{ {
id: "no_devices", id: "no_devices",
area: "", area: "",
name: this.hass.localize("ui.components.device-picker.no_devices"), name: this.hass.localize("ui.components.device-picker.no_devices"),
strings: [],
}, },
]; ];
} }
@ -235,6 +242,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
device.area_id && areaLookup[device.area_id] device.area_id && areaLookup[device.area_id]
? areaLookup[device.area_id].name ? areaLookup[device.area_id].name
: this.hass.localize("ui.components.device-picker.no_area"), : this.hass.localize("ui.components.device-picker.no_area"),
strings: [device.name || ""],
})); }));
if (!outputDevices.length) { if (!outputDevices.length) {
return [ return [
@ -242,6 +250,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
id: "no_devices", id: "no_devices",
area: "", area: "",
name: this.hass.localize("ui.components.device-picker.no_match"), name: this.hass.localize("ui.components.device-picker.no_match"),
strings: [],
}, },
]; ];
} }
@ -284,7 +293,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
(this._init && changedProps.has("_opened") && this._opened) (this._init && changedProps.has("_opened") && this._opened)
) { ) {
this._init = true; this._init = true;
(this.comboBox as any).items = this._getDevices( const devices = this._getDevices(
this.devices!, this.devices!,
this.areas!, this.areas!,
this.entities!, this.entities!,
@ -295,6 +304,8 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
this.entityFilter, this.entityFilter,
this.excludeDevices this.excludeDevices
); );
this.comboBox.items = devices;
this.comboBox.filteredItems = devices;
} }
} }
@ -314,6 +325,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
item-label-path="name" item-label-path="name"
@opened-changed=${this._openedChanged} @opened-changed=${this._openedChanged}
@value-changed=${this._deviceChanged} @value-changed=${this._deviceChanged}
@filter-changed=${this._filterChanged}
></ha-combo-box> ></ha-combo-box>
`; `;
} }
@ -322,6 +334,14 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
return this.value || ""; return this.value || "";
} }
private _filterChanged(ev: CustomEvent): void {
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>) { private _deviceChanged(ev: ValueChangedEvent<string>) {
ev.stopPropagation(); ev.stopPropagation();
let newValue = ev.detail.value; let newValue = ev.detail.value;

View File

@ -7,15 +7,19 @@ import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { computeDomain } from "../../common/entity/compute_domain"; import { computeDomain } from "../../common/entity/compute_domain";
import { computeStateName } from "../../common/entity/compute_state_name"; import { computeStateName } from "../../common/entity/compute_state_name";
import { caseInsensitiveStringCompare } from "../../common/string/compare"; import {
fuzzyFilterSort,
ScorableTextItem,
} from "../../common/string/filter/sequence-matching";
import { ValueChangedEvent, HomeAssistant } from "../../types"; import { ValueChangedEvent, HomeAssistant } from "../../types";
import "../ha-combo-box"; import "../ha-combo-box";
import type { HaComboBox } from "../ha-combo-box"; import type { HaComboBox } from "../ha-combo-box";
import "../ha-icon-button"; import "../ha-icon-button";
import "../ha-svg-icon"; import "../ha-svg-icon";
import "./state-badge"; import "./state-badge";
import { caseInsensitiveStringCompare } from "../../common/string/compare";
interface HassEntityWithCachedName extends HassEntity { interface HassEntityWithCachedName extends HassEntity, ScorableTextItem {
friendly_name: string; friendly_name: string;
} }
@ -159,6 +163,7 @@ export class HaEntityPicker extends LitElement {
), ),
icon: "mdi:magnify", icon: "mdi:magnify",
}, },
strings: [],
}, },
]; ];
} }
@ -169,10 +174,14 @@ export class HaEntityPicker extends LitElement {
); );
return entityIds return entityIds
.map((key) => ({ .map((key) => {
...hass!.states[key], const friendly_name = computeStateName(hass!.states[key]) || key;
friendly_name: computeStateName(hass!.states[key]) || key, return {
})) ...hass!.states[key],
friendly_name,
strings: [key, friendly_name],
};
})
.sort((entityA, entityB) => .sort((entityA, entityB) =>
caseInsensitiveStringCompare( caseInsensitiveStringCompare(
entityA.friendly_name, entityA.friendly_name,
@ -201,10 +210,14 @@ export class HaEntityPicker extends LitElement {
} }
states = entityIds states = entityIds
.map((key) => ({ .map((key) => {
...hass!.states[key], const friendly_name = computeStateName(hass!.states[key]) || key;
friendly_name: computeStateName(hass!.states[key]) || key, return {
})) ...hass!.states[key],
friendly_name,
strings: [key, friendly_name],
};
})
.sort((entityA, entityB) => .sort((entityA, entityB) =>
caseInsensitiveStringCompare( caseInsensitiveStringCompare(
entityA.friendly_name, entityA.friendly_name,
@ -260,6 +273,7 @@ export class HaEntityPicker extends LitElement {
), ),
icon: "mdi:magnify", icon: "mdi:magnify",
}, },
strings: [],
}, },
]; ];
} }
@ -293,7 +307,7 @@ export class HaEntityPicker extends LitElement {
this.excludeEntities this.excludeEntities
); );
if (this._initedStates) { if (this._initedStates) {
(this.comboBox as any).filteredItems = this._states; this.comboBox.filteredItems = this._states;
} }
this._initedStates = true; this._initedStates = true;
} }
@ -340,12 +354,11 @@ export class HaEntityPicker extends LitElement {
} }
private _filterChanged(ev: CustomEvent): void { private _filterChanged(ev: CustomEvent): void {
const target = ev.target as HaComboBox;
const filterString = ev.detail.value.toLowerCase(); const filterString = ev.detail.value.toLowerCase();
(this.comboBox as any).filteredItems = this._states.filter( target.filteredItems = filterString.length
(entityState) => ? fuzzyFilterSort<HassEntityWithCachedName>(filterString, this._states)
entityState.entity_id.toLowerCase().includes(filterString) || : this._states;
computeStateName(entityState).toLowerCase().includes(filterString)
);
} }
private _setValue(value: string) { private _setValue(value: string) {

View File

@ -7,6 +7,10 @@ import { classMap } from "lit/directives/class-map";
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 { computeDomain } from "../common/entity/compute_domain"; import { computeDomain } from "../common/entity/compute_domain";
import {
fuzzyFilterSort,
ScorableTextItem,
} from "../common/string/filter/sequence-matching";
import { import {
AreaRegistryEntry, AreaRegistryEntry,
createAreaRegistryEntry, createAreaRegistryEntry,
@ -28,6 +32,8 @@ import type { HaComboBox } from "./ha-combo-box";
import "./ha-icon-button"; import "./ha-icon-button";
import "./ha-svg-icon"; import "./ha-svg-icon";
type ScorableAreaRegistryEntry = ScorableTextItem & AreaRegistryEntry;
const rowRenderer: ComboBoxLitRenderer<AreaRegistryEntry> = ( const rowRenderer: ComboBoxLitRenderer<AreaRegistryEntry> = (
item item
) => html`<mwc-list-item ) => html`<mwc-list-item
@ -306,9 +312,12 @@ export class HaAreaPicker extends LitElement {
this.entityFilter, this.entityFilter,
this.noAdd, this.noAdd,
this.excludeAreas this.excludeAreas
); ).map((area) => ({
(this.comboBox as any).items = areas; ...area,
(this.comboBox as any).filteredItems = areas; strings: [area.area_id, ...area.aliases, area.name],
}));
this.comboBox.items = areas;
this.comboBox.filteredItems = areas;
} }
} }
@ -345,8 +354,9 @@ export class HaAreaPicker extends LitElement {
return; return;
} }
const filteredItems = this.comboBox.items?.filter((item) => const filteredItems = fuzzyFilterSort<ScorableAreaRegistryEntry>(
item.name.toLowerCase().includes(filter!.toLowerCase()) filter,
this.comboBox?.items || []
); );
if (!this.noAdd && filteredItems?.length === 0) { if (!this.noAdd && filteredItems?.length === 0) {
this._suggestion = filter; this._suggestion = filter;
@ -409,7 +419,7 @@ export class HaAreaPicker extends LitElement {
name, name,
}); });
const areas = [...Object.values(this.hass.areas), area]; const areas = [...Object.values(this.hass.areas), area];
(this.comboBox as any).filteredItems = this._getAreas( this.comboBox.filteredItems = this._getAreas(
areas, areas,
Object.values(this.hass.devices)!, Object.values(this.hass.devices)!,
Object.values(this.hass.entities)!, Object.values(this.hass.entities)!,