mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-25 18:26:35 +00:00
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:
parent
332af4003e
commit
3888b1c48b
@ -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;
|
||||||
|
@ -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) {
|
||||||
|
@ -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)!,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user