mirror of
https://github.com/home-assistant/frontend.git
synced 2026-04-25 20:12:48 +00:00
Add entity name alias toggle and drag-to-reorder aliases in voice settings (#30201)
* Add entity name alias toggle and drag-to-reorder aliases in voice settings * Fix reorder * Update src/panels/config/voice-assistants/entity-voice-settings.ts Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com> * Use map instead of repeat * Improve key --------- Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
This commit is contained in:
@@ -12,6 +12,8 @@ class AliasesEditor extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public sortable = false;
|
||||
|
||||
protected render() {
|
||||
if (!this.aliases) {
|
||||
return nothing;
|
||||
@@ -22,6 +24,8 @@ class AliasesEditor extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.value=${this.aliases}
|
||||
.disabled=${this.disabled}
|
||||
.sortable=${this.sortable}
|
||||
update-on-blur
|
||||
.label=${this.hass!.localize("ui.dialogs.aliases.label")}
|
||||
.removeLabel=${this.hass!.localize("ui.dialogs.aliases.remove")}
|
||||
.addLabel=${this.hass!.localize("ui.dialogs.aliases.add")}
|
||||
@@ -32,8 +36,9 @@ class AliasesEditor extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _aliasesChanged(value) {
|
||||
fireEvent(this, "value-changed", { value });
|
||||
private _aliasesChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
fireEvent(this, "value-changed", { value: ev.detail.value });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { mdiDeleteOutline, mdiPlus } from "@mdi/js";
|
||||
import { mdiDeleteOutline, mdiDragHorizontalVariant, mdiPlus } from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { haStyle } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-button";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-input-helper-text";
|
||||
import "./ha-sortable";
|
||||
import "./ha-textfield";
|
||||
import type { HaTextField } from "./ha-textfield";
|
||||
|
||||
@@ -40,40 +42,65 @@ class HaMultiTextField extends LitElement {
|
||||
|
||||
@property({ type: Number }) public max?: number;
|
||||
|
||||
@property({ type: Boolean }) public sortable = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "update-on-blur" })
|
||||
public updateOnBlur = false;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
${this._items.map((item, index) => {
|
||||
const indexSuffix = `${this.itemIndex ? ` ${index + 1}` : ""}`;
|
||||
return html`
|
||||
<div class="layout horizontal center-center row">
|
||||
<ha-textfield
|
||||
.suffix=${this.inputSuffix}
|
||||
.prefix=${this.inputPrefix}
|
||||
.type=${this.inputType}
|
||||
.autocomplete=${this.autocomplete}
|
||||
.disabled=${this.disabled}
|
||||
dialogInitialFocus=${index}
|
||||
.index=${index}
|
||||
class="flex-auto"
|
||||
.label=${`${this.label ? `${this.label}${indexSuffix}` : ""}`}
|
||||
.value=${item}
|
||||
?data-last=${index === this._items.length - 1}
|
||||
@input=${this._editItem}
|
||||
@keydown=${this._keyDown}
|
||||
></ha-textfield>
|
||||
<ha-icon-button
|
||||
.disabled=${this.disabled}
|
||||
.index=${index}
|
||||
slot="navigationIcon"
|
||||
.label=${this.removeLabel ??
|
||||
this.hass?.localize("ui.common.remove") ??
|
||||
"Remove"}
|
||||
@click=${this._removeItem}
|
||||
.path=${mdiDeleteOutline}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
<ha-sortable
|
||||
handle-selector=".handle"
|
||||
draggable-selector=".row"
|
||||
.disabled=${!this.sortable || this.disabled}
|
||||
@item-moved=${this._itemMoved}
|
||||
>
|
||||
<div class="items">
|
||||
${repeat(
|
||||
this._items,
|
||||
(item, index) => `${item}-${index}`,
|
||||
(item, index) => {
|
||||
const indexSuffix = `${this.itemIndex ? ` ${index + 1}` : ""}`;
|
||||
return html`
|
||||
<div class="layout horizontal center-center row">
|
||||
<ha-textfield
|
||||
.suffix=${this.inputSuffix}
|
||||
.prefix=${this.inputPrefix}
|
||||
.type=${this.inputType}
|
||||
.autocomplete=${this.autocomplete}
|
||||
.disabled=${this.disabled}
|
||||
dialogInitialFocus=${index}
|
||||
.index=${index}
|
||||
class="flex-auto"
|
||||
.label=${`${this.label ? `${this.label}${indexSuffix}` : ""}`}
|
||||
.value=${item}
|
||||
?data-last=${index === this._items.length - 1}
|
||||
@input=${this._editItem}
|
||||
@change=${this._editItem}
|
||||
@keydown=${this._keyDown}
|
||||
></ha-textfield>
|
||||
<ha-icon-button
|
||||
.disabled=${this.disabled}
|
||||
.index=${index}
|
||||
slot="navigationIcon"
|
||||
.label=${this.removeLabel ??
|
||||
this.hass?.localize("ui.common.remove") ??
|
||||
"Remove"}
|
||||
@click=${this._removeItem}
|
||||
.path=${mdiDeleteOutline}
|
||||
></ha-icon-button>
|
||||
${this.sortable
|
||||
? html`<ha-svg-icon
|
||||
class="handle"
|
||||
.path=${mdiDragHorizontalVariant}
|
||||
></ha-svg-icon>`
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</ha-sortable>
|
||||
<div class="layout horizontal">
|
||||
<ha-button
|
||||
size="small"
|
||||
@@ -118,6 +145,12 @@ class HaMultiTextField extends LitElement {
|
||||
}
|
||||
|
||||
private async _editItem(ev: Event) {
|
||||
if (this.updateOnBlur && ev.type === "input") {
|
||||
return;
|
||||
}
|
||||
if (!this.updateOnBlur && ev.type === "change") {
|
||||
return;
|
||||
}
|
||||
const index = (ev.target as any).index;
|
||||
const items = [...this._items];
|
||||
items[index] = (ev.target as any).value;
|
||||
@@ -131,6 +164,15 @@ class HaMultiTextField extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _itemMoved(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
const { oldIndex, newIndex } = ev.detail;
|
||||
const items = [...this._items];
|
||||
const [moved] = items.splice(oldIndex, 1);
|
||||
items.splice(newIndex, 0, moved);
|
||||
this._fireChanged(items);
|
||||
}
|
||||
|
||||
private async _removeItem(ev: Event) {
|
||||
const index = (ev.target as any).index;
|
||||
const items = [...this._items];
|
||||
@@ -156,6 +198,11 @@ class HaMultiTextField extends LitElement {
|
||||
ha-icon-button {
|
||||
display: block;
|
||||
}
|
||||
.handle {
|
||||
cursor: grab;
|
||||
padding: 8px;
|
||||
margin: -8px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -900,7 +900,6 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
${this.entry.aliases.filter((a) => a !== null).length
|
||||
? this.entry.aliases
|
||||
.filter((a): a is string => a !== null)
|
||||
.sort((a, b) => stringCompare(a, b, this.hass.locale.language))
|
||||
.join(", ")
|
||||
: this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.no_aliases"
|
||||
|
||||
@@ -5,6 +5,7 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import type {
|
||||
EntityDomainFilter,
|
||||
EntityDomainFilterFunc,
|
||||
@@ -287,27 +288,55 @@ export class EntityVoiceSettings extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
)}
|
||||
</ha-alert>`
|
||||
: html`<ha-aliases-editor
|
||||
.hass=${this.hass}
|
||||
.aliases=${(this._aliases ?? this.entry.aliases).filter(
|
||||
(a): a is string => a !== null
|
||||
)}
|
||||
@value-changed=${this._aliasesChanged}
|
||||
@blur=${this._saveAliases}
|
||||
></ha-aliases-editor>`}
|
||||
: html`
|
||||
<ha-md-list-item>
|
||||
<span slot="headline">
|
||||
${this.hass.states[this.entityId]
|
||||
? computeStateName(this.hass.states[this.entityId])
|
||||
: this.entityId}
|
||||
</span>
|
||||
<span slot="supporting-text">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.voice-settings.entity_name_alias_description"
|
||||
)}
|
||||
</span>
|
||||
<ha-switch
|
||||
slot="end"
|
||||
.checked=${(this._aliases ?? this.entry.aliases).includes(null)}
|
||||
@change=${this._toggleEntityNameAlias}
|
||||
></ha-switch>
|
||||
</ha-md-list-item>
|
||||
<ha-aliases-editor
|
||||
.hass=${this.hass}
|
||||
.aliases=${(this._aliases ?? this.entry.aliases).filter(
|
||||
(a): a is string => a !== null
|
||||
)}
|
||||
sortable
|
||||
@value-changed=${this._aliasesChanged}
|
||||
></ha-aliases-editor>
|
||||
`}
|
||||
`;
|
||||
}
|
||||
|
||||
private _aliasesChanged(ev) {
|
||||
const currentLength =
|
||||
this._aliases?.length ?? this.entry?.aliases?.length ?? 0;
|
||||
|
||||
this._aliases = ev.detail.value;
|
||||
|
||||
// if an entry was deleted, then save changes
|
||||
if (currentLength > ev.detail.value.length) {
|
||||
this._saveAliases();
|
||||
private async _toggleEntityNameAlias(ev) {
|
||||
const enabled = ev.target.checked;
|
||||
const currentAliases = this._aliases ?? this.entry?.aliases ?? [];
|
||||
if (enabled) {
|
||||
this._aliases = [null, ...currentAliases.filter((a) => a !== null)];
|
||||
} else {
|
||||
this._aliases = currentAliases.filter((a): a is string => a !== null);
|
||||
}
|
||||
await this._saveAliases();
|
||||
}
|
||||
|
||||
private _aliasesChanged(ev) {
|
||||
const currentAliases = this._aliases ?? this.entry?.aliases ?? [];
|
||||
const hasNull = currentAliases.includes(null);
|
||||
const nullAliases: (string | null)[] = hasNull ? [null] : [];
|
||||
const newStringAliases: string[] = ev.detail.value;
|
||||
|
||||
this._aliases = [...nullAliases, ...newStringAliases];
|
||||
this._saveAliases();
|
||||
}
|
||||
|
||||
private async _2faChanged(ev) {
|
||||
@@ -326,7 +355,8 @@ export class EntityVoiceSettings extends SubscribeMixin(LitElement) {
|
||||
if (!this._aliases) {
|
||||
return;
|
||||
}
|
||||
const nullAliases = (this.entry?.aliases ?? []).filter((a) => a === null);
|
||||
const hasNull = this._aliases.includes(null);
|
||||
const nullAliases: null[] = hasNull ? [null] : [];
|
||||
const stringAliases = this._aliases
|
||||
.filter((a): a is string => a !== null)
|
||||
.map((alias) => alias.trim())
|
||||
|
||||
@@ -1885,8 +1885,9 @@
|
||||
"voice-settings": {
|
||||
"expose_header": "Expose",
|
||||
"aliases_header": "Aliases",
|
||||
"aliases_description": "Aliases are supported by Assist and Google Assistant.",
|
||||
"aliases_description": "Aliases are alternative names to call your entity. Only supported by Assist and Google Assistant.",
|
||||
"aliases_no_unique_id": "Aliases are not supported for entities without a unique ID. See the {faq_link} for more detail.",
|
||||
"entity_name_alias_description": "Default name. Disable it if you want your voice assistants to ignore it and just use aliases.",
|
||||
"ask_pin": "Ask for PIN",
|
||||
"manual_config": "Managed in configuration.yaml",
|
||||
"unsupported": "Unsupported",
|
||||
|
||||
Reference in New Issue
Block a user