mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-27 03:06:41 +00:00
Improve action picker UI and search (#25525)
This commit is contained in:
parent
06270c771f
commit
61d9b0d2a3
4
src/common/entity/valid_service_id.ts
Normal file
4
src/common/entity/valid_service_id.ts
Normal file
@ -0,0 +1,4 @@
|
||||
const validServiceId = /^(\w+)\.(\w+)$/;
|
||||
|
||||
export const isValidServiceId = (actionId: string) =>
|
||||
validServiceId.test(actionId);
|
@ -51,6 +51,9 @@ export class HaEntityPicker extends LitElement {
|
||||
@property({ type: Boolean, attribute: "allow-custom-entity" })
|
||||
public allowCustomEntity;
|
||||
|
||||
@property({ type: Boolean, attribute: "show-entity-id" })
|
||||
public showEntityId = false;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public value?: string;
|
||||
@ -166,11 +169,15 @@ export class HaEntityPicker extends LitElement {
|
||||
`;
|
||||
};
|
||||
|
||||
private get _showEntityId() {
|
||||
return this.showEntityId || this.hass.userData?.showEntityIdPicker;
|
||||
}
|
||||
|
||||
private _rowRenderer: ComboBoxLitRenderer<EntityComboBoxItem> = (
|
||||
item,
|
||||
{ index }
|
||||
) => {
|
||||
const showEntityId = this.hass.userData?.showEntityIdPicker;
|
||||
const showEntityId = this._showEntityId;
|
||||
|
||||
return html`
|
||||
<ha-combo-box-item type="button" compact .borderTop=${index !== 0}>
|
||||
|
@ -85,8 +85,11 @@ export class HaServiceControl extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: "show-advanced", type: Boolean }) public showAdvanced =
|
||||
false;
|
||||
@property({ attribute: "show-advanced", type: Boolean })
|
||||
public showAdvanced = false;
|
||||
|
||||
@property({ attribute: "show-service-id", type: Boolean })
|
||||
public showServiceId = false;
|
||||
|
||||
@property({ attribute: "hide-picker", type: Boolean, reflect: true })
|
||||
public hidePicker = false;
|
||||
@ -435,6 +438,7 @@ export class HaServiceControl extends LitElement {
|
||||
.value=${this._value?.action}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._serviceChanged}
|
||||
.showServiceId=${this.showServiceId}
|
||||
></ha-service-picker>`}
|
||||
${this.hideDescription
|
||||
? nothing
|
||||
|
@ -1,15 +1,25 @@
|
||||
import { mdiRoomService } from "@mdi/js";
|
||||
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { html, LitElement, nothing, type TemplateResult } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { isValidServiceId } from "../common/entity/valid_service_id";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { domainToName } from "../data/integration";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-combo-box";
|
||||
import "./ha-combo-box-item";
|
||||
import "./ha-service-icon";
|
||||
import { getServiceIcons } from "../data/icons";
|
||||
import { domainToName } from "../data/integration";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import "./ha-combo-box-item";
|
||||
import "./ha-generic-picker";
|
||||
import type { HaGenericPicker } from "./ha-generic-picker";
|
||||
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
|
||||
import type { PickerValueRenderer } from "./ha-picker-field";
|
||||
import "./ha-service-icon";
|
||||
|
||||
interface ServiceComboBoxItem extends PickerComboBoxItem {
|
||||
domain_name?: string;
|
||||
service_id?: string;
|
||||
}
|
||||
|
||||
@customElement("ha-service-picker")
|
||||
class HaServicePicker extends LitElement {
|
||||
@ -17,66 +27,121 @@ class HaServicePicker extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public placeholder?: string;
|
||||
|
||||
@property() public value?: string;
|
||||
|
||||
@state() private _filter?: string;
|
||||
@property({ attribute: "show-service-id", type: Boolean })
|
||||
public showServiceId = false;
|
||||
|
||||
protected willUpdate() {
|
||||
if (!this.hasUpdated) {
|
||||
@query("ha-generic-picker") private _picker?: HaGenericPicker;
|
||||
|
||||
public async open() {
|
||||
await this.updateComplete;
|
||||
await this._picker?.open();
|
||||
}
|
||||
|
||||
protected firstUpdated(props) {
|
||||
super.firstUpdated(props);
|
||||
this.hass.loadBackendTranslation("services");
|
||||
getServiceIcons(this.hass);
|
||||
}
|
||||
}
|
||||
|
||||
private _rowRenderer: ComboBoxLitRenderer<{ service: string; name: string }> =
|
||||
(item) => html`
|
||||
<ha-combo-box-item type="button">
|
||||
private _rowRenderer: ComboBoxLitRenderer<ServiceComboBoxItem> = (
|
||||
item,
|
||||
{ index }
|
||||
) => html`
|
||||
<ha-combo-box-item type="button" border-top .borderTop=${index !== 0}>
|
||||
<ha-service-icon
|
||||
slot="start"
|
||||
.hass=${this.hass}
|
||||
.service=${item.service}
|
||||
.service=${item.id}
|
||||
></ha-service-icon>
|
||||
<span slot="headline">${item.name}</span>
|
||||
<span slot="supporting-text"
|
||||
>${item.name === item.service ? "" : item.service}</span
|
||||
>
|
||||
<span slot="headline">${item.primary}</span>
|
||||
<span slot="supporting-text">${item.secondary}</span>
|
||||
${item.service_id && this.showServiceId
|
||||
? html`<span slot="supporting-text" class="code">
|
||||
${item.service_id}
|
||||
</span>`
|
||||
: nothing}
|
||||
${item.domain_name
|
||||
? html`
|
||||
<div slot="trailing-supporting-text" class="domain">
|
||||
${item.domain_name}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
</ha-combo-box-item>
|
||||
`;
|
||||
|
||||
protected render() {
|
||||
private _valueRenderer: PickerValueRenderer = (value) => {
|
||||
const serviceId = value;
|
||||
const [domain, service] = serviceId.split(".");
|
||||
|
||||
if (!this.hass.services[domain]?.[service]) {
|
||||
return html`
|
||||
<ha-combo-box
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize("ui.components.service-picker.action")}
|
||||
.filteredItems=${this._filteredServices(
|
||||
this.hass.localize,
|
||||
this.hass.services,
|
||||
this._filter
|
||||
)}
|
||||
.value=${this.value}
|
||||
.disabled=${this.disabled}
|
||||
.renderer=${this._rowRenderer}
|
||||
item-value-path="service"
|
||||
item-label-path="name"
|
||||
allow-custom-value
|
||||
@filter-changed=${this._filterChanged}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-combo-box>
|
||||
<ha-svg-icon slot="start" .path=${mdiRoomService}></ha-svg-icon>
|
||||
<span slot="headline">${value}</span>
|
||||
`;
|
||||
}
|
||||
|
||||
const serviceName =
|
||||
this.hass.localize(`component.${domain}.services.${service}.name`) ||
|
||||
this.hass.services[domain][service].name ||
|
||||
service;
|
||||
|
||||
return html`
|
||||
<ha-service-icon
|
||||
slot="start"
|
||||
.hass=${this.hass}
|
||||
.service=${serviceId}
|
||||
></ha-service-icon>
|
||||
<span slot="headline">${serviceName}</span>
|
||||
${this.showServiceId
|
||||
? html`<span slot="supporting-text" class="code">${serviceId}</span>`
|
||||
: nothing}
|
||||
`;
|
||||
};
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const placeholder =
|
||||
this.placeholder ??
|
||||
this.hass.localize("ui.components.service-picker.action");
|
||||
|
||||
return html`
|
||||
<ha-generic-picker
|
||||
.hass=${this.hass}
|
||||
.autofocus=${this.autofocus}
|
||||
allow-custom-value
|
||||
.notFoundLabel=${this.hass.localize(
|
||||
"ui.components.service-picker.no_match"
|
||||
)}
|
||||
.label=${this.label}
|
||||
.placeholder=${placeholder}
|
||||
.value=${this.value}
|
||||
.getItems=${this._getItems}
|
||||
.rowRenderer=${this._rowRenderer}
|
||||
.valueRenderer=${this._valueRenderer}
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
</ha-generic-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
private _getItems = () =>
|
||||
this._services(this.hass.localize, this.hass.services);
|
||||
|
||||
private _services = memoizeOne(
|
||||
(
|
||||
localize: LocalizeFunc,
|
||||
services: HomeAssistant["services"]
|
||||
): {
|
||||
service: string;
|
||||
name: string;
|
||||
}[] => {
|
||||
): ServiceComboBoxItem[] => {
|
||||
if (!services) {
|
||||
return [];
|
||||
}
|
||||
const result: { service: string; name: string }[] = [];
|
||||
const items: ServiceComboBoxItem[] = [];
|
||||
|
||||
Object.keys(services)
|
||||
.sort()
|
||||
@ -84,56 +149,60 @@ class HaServicePicker extends LitElement {
|
||||
const services_keys = Object.keys(services[domain]).sort();
|
||||
|
||||
for (const service of services_keys) {
|
||||
result.push({
|
||||
service: `${domain}.${service}`,
|
||||
name: `${domainToName(localize, domain)}: ${
|
||||
const serviceId = `${domain}.${service}`;
|
||||
const domainName = domainToName(localize, domain);
|
||||
|
||||
const name =
|
||||
this.hass.localize(
|
||||
`component.${domain}.services.${service}.name`
|
||||
) ||
|
||||
services[domain][service].name ||
|
||||
service
|
||||
}`,
|
||||
service;
|
||||
|
||||
const description =
|
||||
this.hass.localize(
|
||||
`component.${domain}.services.${service}.description`
|
||||
) || services[domain][service].description;
|
||||
|
||||
items.push({
|
||||
id: serviceId,
|
||||
primary: name,
|
||||
secondary: description,
|
||||
domain_name: domainName,
|
||||
service_id: serviceId,
|
||||
search_labels: [serviceId, domainName, name, description].filter(
|
||||
Boolean
|
||||
),
|
||||
sorting_label: serviceId,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
return items;
|
||||
}
|
||||
);
|
||||
|
||||
private _filteredServices = memoizeOne(
|
||||
(
|
||||
localize: LocalizeFunc,
|
||||
services: HomeAssistant["services"],
|
||||
filter?: string
|
||||
) => {
|
||||
if (!services) {
|
||||
return [];
|
||||
}
|
||||
const processedServices = this._services(localize, services);
|
||||
private _valueChanged(ev: ValueChangedEvent<string>) {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value;
|
||||
|
||||
if (!filter) {
|
||||
return processedServices;
|
||||
}
|
||||
const split_filter = filter.split(" ");
|
||||
return processedServices.filter((service) => {
|
||||
const lower_service_name = service.name.toLowerCase();
|
||||
const lower_service = service.service.toLowerCase();
|
||||
return split_filter.every(
|
||||
(f) => lower_service_name.includes(f) || lower_service.includes(f)
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
private _filterChanged(ev: CustomEvent): void {
|
||||
this._filter = ev.detail.value.toLowerCase();
|
||||
if (!value) {
|
||||
this._setValue(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
private _valueChanged(ev) {
|
||||
this.value = ev.detail.value;
|
||||
if (!isValidServiceId(value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._setValue(value);
|
||||
}
|
||||
|
||||
private _setValue(value: string | undefined) {
|
||||
this.value = value;
|
||||
|
||||
fireEvent(this, "value-changed", { value });
|
||||
fireEvent(this, "change");
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -142,6 +142,7 @@ class HaPanelDevAction extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.value=${this._serviceData?.action}
|
||||
@value-changed=${this._serviceChanged}
|
||||
show-service-id
|
||||
></ha-service-picker>
|
||||
<ha-yaml-editor
|
||||
id="yaml-editor"
|
||||
@ -156,6 +157,7 @@ class HaPanelDevAction extends LitElement {
|
||||
.value=${this._serviceData}
|
||||
.narrow=${this.narrow}
|
||||
show-advanced
|
||||
show-service-id
|
||||
@value-changed=${this._serviceDataChanged}
|
||||
class="card-content"
|
||||
></ha-service-control>
|
||||
|
@ -130,6 +130,7 @@ class HaPanelDevState extends LitElement {
|
||||
.value=${this._entityId}
|
||||
@value-changed=${this._entityIdChanged}
|
||||
allow-custom-entity
|
||||
show-entity-id
|
||||
></ha-entity-picker>
|
||||
${this._entityId
|
||||
? html`
|
||||
|
@ -873,7 +873,8 @@
|
||||
}
|
||||
},
|
||||
"service-picker": {
|
||||
"action": "Action"
|
||||
"action": "Action",
|
||||
"no_match": "No matching actions found"
|
||||
},
|
||||
"service-control": {
|
||||
"required": "This field is required",
|
||||
|
Loading…
x
Reference in New Issue
Block a user