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

View File

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

View File

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