Compare commits

..

15 Commits

Author SHA1 Message Date
Wendelin
d679916fa5 Migrate domain selection to use generic picker component (#28605)
* Migrate domain selection to use generic picker component

* Remove unused CSS, add margins

* Space tokens

* Fix validation

---------

Co-authored-by: Aidan Timson <aidan@timmo.dev>
2025-12-18 13:39:16 +00:00
Aidan Timson
96be4768d3 Migrate entity name picker to generic picker (#28604)
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2025-12-18 14:31:29 +01:00
Aidan Timson
6ff3b9f761 Migrate new label dialog to ha-wa-dialog (#28586) 2025-12-18 13:22:07 +01:00
Joakim Plate
9026009842 Let event domain expose attributes in gui (#28486) 2025-12-18 12:43:48 +01:00
Aidan Timson
54398a4784 Fix entity settings row sizing (#28585) 2025-12-18 12:42:29 +01:00
Wendelin
fa3cc970ec Migrate Z-Wave JS node configuration to use generic picker component (#28603) 2025-12-18 13:30:24 +02:00
Aidan Timson
1cf0560003 Migrate color picker to generic picker (#28598) 2025-12-18 11:23:25 +01:00
Wendelin
2a4ac15987 Generic-picker: Implement allowCustomValue (#28572)
* Introduce allowCustomValue and remove usage

* Review

* Fix secondary title
2025-12-18 12:19:57 +02:00
Aidan Timson
f264eebe49 Remove unused prop in target picker (#28601) 2025-12-18 10:14:42 +00:00
Aidan Timson
dae27e091f Migrate config entry picker to new picker syntax (#28600) 2025-12-18 10:10:55 +00:00
Aidan Timson
7ca681e417 Refactor generic pickers (#28570) 2025-12-18 10:53:50 +01:00
ndrwrbgs
1adfe63322 Add media query for prefers-reduced-motion in dialog styles (#28593)
Co-authored-by: ndrwrbgs <10776890+ndrwrbgs@users.noreply.github.com>
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2025-12-18 08:18:19 +00:00
Wendelin
119a505a0d Add iOS focus element messaging for ha-generic-picker component (#28569) 2025-12-18 09:18:01 +01:00
renovate[bot]
1f8403f6c1 Update dependency vite-tsconfig-paths to v6 (#28596)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-18 09:19:09 +02:00
Wendelin
7dd7309a47 Migrate addon-picker to generic-picker (#28567) 2025-12-17 16:33:41 +00:00
63 changed files with 2297 additions and 2137 deletions

View File

@@ -218,7 +218,7 @@
"ts-lit-plugin": "2.0.2",
"typescript": "5.9.3",
"typescript-eslint": "8.49.0",
"vite-tsconfig-paths": "5.1.4",
"vite-tsconfig-paths": "6.0.1",
"vitest": "4.0.15",
"webpack-stats-plugin": "1.1.3",
"webpackbar": "7.0.0",

View File

@@ -61,7 +61,6 @@ class HaDevicesPicker extends LitElement {
(entityId) => html`
<div>
<ha-device-picker
allow-custom-entity
.curValue=${entityId}
.hass=${this.hass}
.deviceFilter=${this.deviceFilter}
@@ -79,7 +78,6 @@ class HaDevicesPicker extends LitElement {
)}
<div>
<ha-device-picker
allow-custom-entity
.hass=${this.hass}
.helper=${this.helper}
.deviceFilter=${this.deviceFilter}

View File

@@ -99,7 +99,6 @@ class HaEntitiesPicker extends LitElement {
(entityId) => html`
<div class="entity">
<ha-entity-picker
allow-custom-entity
.curValue=${entityId}
.hass=${this.hass}
.includeDomains=${this.includeDomains}
@@ -129,7 +128,6 @@ class HaEntitiesPicker extends LitElement {
</ha-sortable>
<div>
<ha-entity-picker
allow-custom-entity
.hass=${this.hass}
.includeDomains=${this.includeDomains}
.excludeDomains=${this.excludeDomains}

View File

@@ -1,15 +1,11 @@
import "@material/mwc-menu/mwc-menu-surface";
import { mdiDragHorizontalVariant, mdiPlus } from "@mdi/js";
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import type { IFuseOptions } from "fuse.js";
import Fuse from "fuse.js";
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, query } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { ensureArray } from "../../common/array/ensure-array";
import { fireEvent } from "../../common/dom/fire_event";
import { stopPropagation } from "../../common/dom/stop_propagation";
import type { EntityNameItem } from "../../common/entity/compute_entity_name_display";
import { getEntityContext } from "../../common/entity/context/get_entity_context";
import type { EntityNameType } from "../../common/translations/entity-state";
@@ -18,20 +14,18 @@ import type { HomeAssistant, ValueChangedEvent } from "../../types";
import "../chips/ha-assist-chip";
import "../chips/ha-chip-set";
import "../chips/ha-input-chip";
import "../ha-combo-box";
import type { HaComboBox } from "../ha-combo-box";
import "../ha-combo-box-item";
import "../ha-generic-picker";
import type { HaGenericPicker } from "../ha-generic-picker";
import "../ha-input-helper-text";
import {
NO_ITEMS_AVAILABLE_ID,
type PickerComboBoxItem,
} from "../ha-picker-combo-box";
import "../ha-sortable";
interface EntityNameOption {
primary: string;
secondary?: string;
field_label: string;
value: string;
}
const rowRenderer: ComboBoxLitRenderer<EntityNameOption> = (item) => html`
<ha-combo-box-item type="button">
const rowRenderer: RenderItemFunction<PickerComboBoxItem> = (item) => html`
<ha-combo-box-item type="button" compact>
<span slot="headline">${item.primary}</span>
${item.secondary
? html`<span slot="supporting-text">${item.secondary}</span>`
@@ -79,11 +73,7 @@ export class HaEntityNamePicker extends LitElement {
@property({ type: Boolean, reflect: true }) public disabled = false;
@query(".container", true) private _container?: HTMLDivElement;
@query("ha-combo-box", true) private _comboBox!: HaComboBox;
@state() private _opened = false;
@query("ha-generic-picker", true) private _picker?: HaGenericPicker;
private _editIndex?: number;
@@ -115,7 +105,7 @@ export class HaEntityNamePicker extends LitElement {
return options;
});
private _getOptions = memoizeOne((entityId?: string) => {
private _getItems = memoizeOne((entityId?: string) => {
if (!entityId) {
return [];
}
@@ -124,7 +114,7 @@ export class HaEntityNamePicker extends LitElement {
const items = (
["entity", "device", "area", "floor"] as const
).map<EntityNameOption>((name) => {
).map<PickerComboBoxItem>((name) => {
const stateObj = this.hass.states[entityId];
const isValid = types.has(name);
const primary = this.hass.localize(
@@ -137,25 +127,39 @@ export class HaEntityNamePicker extends LitElement {
`ui.components.entity.entity-name-picker.types.${name}_missing` as LocalizeKeys
)) || "-";
const id = formatOptionValue({ type: name });
return {
id,
primary,
secondary,
field_label: primary,
value: formatOptionValue({ type: name }),
search_labels: {
primary,
secondary: secondary || null,
id,
},
sorting_label: primary,
};
});
return items;
});
private _customNameOption = memoizeOne((text: string) => ({
private _customNameOption = memoizeOne(
(text: string): PickerComboBoxItem => ({
id: formatOptionValue({ type: "text", text }),
primary: this.hass.localize(
"ui.components.entity.entity-name-picker.custom_name"
),
secondary: `"${text}"`,
field_label: text,
value: formatOptionValue({ type: "text", text }),
}));
search_labels: {
primary: text,
secondary: `"${text}"`,
id: formatOptionValue({ type: "text", text }),
},
sorting_label: text,
})
);
private _formatItem = (item: EntityNameItem) => {
if (item.type === "text") {
@@ -171,12 +175,29 @@ export class HaEntityNamePicker extends LitElement {
protected render() {
const value = this._items;
const options = this._getOptions(this.entityId);
const validTypes = this._validTypes(this.entityId);
return html`
${this.label ? html`<label>${this.label}</label>` : nothing}
<div class="container">
<ha-generic-picker
.hass=${this.hass}
.disabled=${this.disabled}
.required=${this.required && !value.length}
.getItems=${this._getFilteredItems}
.getAdditionalItems=${this._getAdditionalItems}
.rowRenderer=${rowRenderer}
.searchFn=${this._searchFn}
.notFoundLabel=${this.hass.localize(
"ui.components.entity.entity-name-picker.no_match"
)}
.value=${this._getPickerValue()}
allow-custom-value
.customValueLabel=${this.hass.localize(
"ui.components.entity.entity-name-picker.custom_name"
)}
@value-changed=${this._pickerValueChanged}
>
<div slot="field" class="container">
<ha-sortable
no-style
@item-moved=${this._moveItem}
@@ -226,33 +247,8 @@ export class HaEntityNamePicker extends LitElement {
`}
</ha-chip-set>
</ha-sortable>
<mwc-menu-surface
.open=${this._opened}
@closed=${this._onClosed}
@opened=${this._onOpened}
@input=${stopPropagation}
.anchor=${this._container}
>
<ha-combo-box
.hass=${this.hass}
.value=${""}
.autofocus=${this.autofocus}
.disabled=${this.disabled}
.required=${this.required && !value.length}
.items=${options}
allow-custom-value
item-id-path="value"
item-value-path="value"
item-label-path="field_label"
.renderer=${rowRenderer}
@opened-changed=${this._openedChanged}
@value-changed=${this._comboBoxValueChanged}
@filter-changed=${this._filterChanged}
>
</ha-combo-box>
</mwc-menu-surface>
</div>
</ha-generic-picker>
${this._renderHelper()}
`;
}
@@ -267,32 +263,22 @@ export class HaEntityNamePicker extends LitElement {
: nothing;
}
private _onClosed(ev) {
private async _addItem(ev: Event) {
ev.stopPropagation();
this._opened = false;
this._editIndex = undefined;
await this.updateComplete;
await this._picker?.open();
}
private async _onOpened(ev) {
if (!this._opened) {
return;
}
private async _editItem(ev: Event) {
ev.stopPropagation();
this._opened = true;
await this._comboBox?.focus();
await this._comboBox?.open();
}
private async _addItem(ev) {
ev.stopPropagation();
this._opened = true;
}
private async _editItem(ev) {
ev.stopPropagation();
const idx = parseInt(ev.currentTarget.dataset.idx, 10);
const idx = parseInt(
(ev.currentTarget as HTMLElement).dataset.idx || "",
10
);
this._editIndex = idx;
this._opened = true;
await this.updateComplete;
await this._picker?.open();
}
private get _items(): EntityNameItem[] {
@@ -322,78 +308,80 @@ export class HaEntityNamePicker extends LitElement {
}
);
private _openedChanged(ev: ValueChangedEvent<boolean>) {
const open = ev.detail.value;
if (open) {
const options = this._comboBox.items || [];
private _getPickerValue(): string | undefined {
if (this._editIndex != null) {
const item = this._items[this._editIndex];
return item ? formatOptionValue(item) : undefined;
}
return undefined;
}
const initialItem =
private _getFilteredItems = (
searchString?: string,
_section?: string
): PickerComboBoxItem[] => {
const items = this._getItems(this.entityId);
const currentItem =
this._editIndex != null ? this._items[this._editIndex] : undefined;
const initialValue = initialItem ? formatOptionValue(initialItem) : "";
const filteredItems = this._filterSelectedOptions(options, initialValue);
if (initialItem?.type === "text" && initialItem.text) {
filteredItems.push(this._customNameOption(initialItem.text));
}
this._comboBox.filteredItems = filteredItems;
this._comboBox.setInputValue(initialValue);
} else {
this._opened = false;
this._comboBox.setInputValue("");
}
}
private _filterSelectedOptions = (
options: EntityNameOption[],
current?: string
) => {
const items = this._items;
const currentValue = currentItem ? formatOptionValue(currentItem) : "";
const excludedValues = new Set(
items
this._items
.filter((item) => UNIQUE_TYPES.has(item.type))
.map((item) => formatOptionValue(item))
);
const filteredOptions = options.filter(
(option) => !excludedValues.has(option.value) || option.value === current
const filteredItems = items.filter(
(item) => !excludedValues.has(item.id) || item.id === currentValue
);
return filteredOptions;
// When editing an existing text item, include it in the base items
if (currentItem?.type === "text" && currentItem.text && !searchString) {
filteredItems.push(this._customNameOption(currentItem.text));
}
return filteredItems;
};
private _filterChanged(ev: ValueChangedEvent<string>) {
const input = ev.detail.value;
const filter = input?.toLowerCase() || "";
const options = this._comboBox.items || [];
private _getAdditionalItems = (
searchString?: string
): PickerComboBoxItem[] => {
if (!searchString) {
return [];
}
const currentItem =
this._editIndex != null ? this._items[this._editIndex] : undefined;
const currentValue = currentItem ? formatOptionValue(currentItem) : "";
let filteredItems = this._filterSelectedOptions(options, currentValue);
if (!filter) {
this._comboBox.filteredItems = filteredItems;
return;
// Don't add if it's the same as the current item being edited
if (
currentItem?.type === "text" &&
currentItem.text &&
currentItem.text === searchString
) {
return [];
}
const fuseOptions: IFuseOptions<EntityNameOption> = {
keys: ["primary", "secondary", "value"],
isCaseSensitive: false,
minMatchCharLength: Math.min(filter.length, 2),
threshold: 0.2,
ignoreDiacritics: true,
// Always return custom name option when there's a search string
// This prevents "No matching items found" from showing
return [this._customNameOption(searchString)];
};
const fuse = new Fuse(filteredItems, fuseOptions);
filteredItems = fuse.search(filter).map((result) => result.item);
filteredItems.push(this._customNameOption(input));
this._comboBox.filteredItems = filteredItems;
private _searchFn = (
search: string,
filteredItems: PickerComboBoxItem[],
_allItems: PickerComboBoxItem[]
): PickerComboBoxItem[] => {
// Remove NO_ITEMS_AVAILABLE_ID if we have additional items (custom name option)
// This prevents "No matching items found" from showing when custom values are allowed
const hasAdditionalItems = this._getAdditionalItems(search).length > 0;
if (hasAdditionalItems) {
return filteredItems.filter(
(item) => typeof item !== "string" || item !== NO_ITEMS_AVAILABLE_ID
);
}
return filteredItems;
};
private async _moveItem(ev: CustomEvent) {
ev.stopPropagation();
@@ -403,25 +391,21 @@ export class HaEntityNamePicker extends LitElement {
const element = newValue.splice(oldIndex, 1)[0];
newValue.splice(newIndex, 0, element);
this._setValue(newValue);
await this.updateComplete;
this._filterChanged({ detail: { value: "" } } as ValueChangedEvent<string>);
}
private async _removeItem(ev) {
private async _removeItem(ev: Event) {
ev.stopPropagation();
const value = [...this._items];
const idx = parseInt(ev.target.dataset.idx, 10);
const idx = parseInt((ev.target as HTMLElement).dataset.idx || "", 10);
value.splice(idx, 1);
this._setValue(value);
await this.updateComplete;
this._filterChanged({ detail: { value: "" } } as ValueChangedEvent<string>);
}
private _comboBoxValueChanged(ev: ValueChangedEvent<string>): void {
private _pickerValueChanged(ev: ValueChangedEvent<string>): void {
ev.stopPropagation();
const value = ev.detail.value;
if (this.disabled || value === "") {
if (this.disabled || !value) {
return;
}
@@ -431,11 +415,16 @@ export class HaEntityNamePicker extends LitElement {
if (this._editIndex != null) {
newValue[this._editIndex] = item;
this._editIndex = undefined;
} else {
newValue.push(item);
}
this._setValue(newValue);
if (this._picker) {
this._picker.value = undefined;
}
}
private _setValue(value: EntityNameItem[]) {
@@ -497,10 +486,6 @@ export class HaEntityNamePicker extends LitElement {
order: 1;
}
mwc-menu-surface {
--mdc-menu-min-width: 100%;
}
ha-chip-set {
padding: var(--ha-space-2) var(--ha-space-2);
}

View File

@@ -277,12 +277,13 @@ export class HaEntityPicker extends LitElement {
.disabled=${this.disabled}
.autofocus=${this.autofocus}
.allowCustomValue=${this.allowCustomEntity}
.required=${this.required}
.label=${this.label}
.placeholder=${placeholder}
.helper=${this.helper}
.value=${this.addButton ? undefined : this.value}
.searchLabel=${this.searchLabel}
.notFoundLabel=${this._notFoundLabel}
.placeholder=${placeholder}
.value=${this.addButton ? undefined : this.value}
.rowRenderer=${this._rowRenderer}
.getItems=${this._getItems}
.getAdditionalItems=${this._getAdditionalItems}
@@ -290,6 +291,7 @@ export class HaEntityPicker extends LitElement {
.searchFn=${this._searchFn}
.valueRenderer=${this._valueRenderer}
.searchKeys=${entityComboBoxKeys}
use-top-label
.addButtonLabel=${this.addButton
? this.hass.localize("ui.components.entity.entity-picker.add")
: undefined}

View File

@@ -471,14 +471,14 @@ export class HaStatisticPicker extends LitElement {
.hass=${this.hass}
.autofocus=${this.autofocus}
.allowCustomValue=${this.allowCustomEntity}
.label=${this.label}
.disabled=${this.disabled}
.label=${this.label}
.placeholder=${placeholder}
.value=${this.value}
.notFoundLabel=${this._notFoundLabel}
.emptyLabel=${this.hass.localize(
"ui.components.statistic-picker.no_statistics"
)}
.placeholder=${placeholder}
.value=${this.value}
.rowRenderer=${this._rowRenderer}
.getItems=${this._getItems}
.getAdditionalItems=${this._getAdditionalItems}

View File

@@ -1,29 +1,29 @@
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
import { html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { isComponentLoaded } from "../common/config/is_component_loaded";
import { fireEvent } from "../common/dom/fire_event";
import { stringCompare } from "../common/string/compare";
import type { HassioAddonInfo } from "../data/hassio/addon";
import { fetchHassioAddonsInfo } from "../data/hassio/addon";
import type { HomeAssistant, ValueChangedEvent } from "../types";
import "./ha-alert";
import "./ha-combo-box";
import type { HaComboBox } from "./ha-combo-box";
import "./ha-combo-box-item";
import "./ha-generic-picker";
import type { HaGenericPicker } from "./ha-generic-picker";
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
const rowRenderer: ComboBoxLitRenderer<HassioAddonInfo> = (item) => html`
const SEARCH_KEYS = [
{ name: "primary", weight: 10 },
{ name: "secondary", weight: 8 },
{ name: "search_labels.description", weight: 6 },
{ name: "search_labels.repository", weight: 5 },
];
const rowRenderer: RenderItemFunction<PickerComboBoxItem> = (item) => html`
<ha-combo-box-item type="button">
<span slot="headline">${item.name}</span>
<span slot="supporting-text">${item.slug}</span>
<span slot="headline">${item.primary}</span>
<span slot="supporting-text">${item.secondary}</span>
${item.icon
? html`
<img
alt=""
slot="start"
.src="/api/hassio/addons/${item.slug}/icon"
/>
`
? html` <img alt="" slot="start" .src=${item.icon} /> `
: nothing}
</ha-combo-box-item>
`;
@@ -38,22 +38,22 @@ class HaAddonPicker extends LitElement {
@property() public helper?: string;
@state() private _addons?: HassioAddonInfo[];
@state() private _addons?: PickerComboBoxItem[];
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = false;
@query("ha-combo-box") private _comboBox!: HaComboBox;
@query("ha-generic-picker") private _genericPicker!: HaGenericPicker;
@state() private _error?: string;
public open() {
this._comboBox?.open();
this._genericPicker?.open();
}
public focus() {
this._comboBox?.focus();
this._genericPicker?.focus();
}
protected firstUpdated() {
@@ -61,29 +61,34 @@ class HaAddonPicker extends LitElement {
}
protected render() {
const label =
this.label === undefined && this.hass
? this.hass.localize("ui.components.addon-picker.addon")
: this.label;
if (this._error) {
return html`<ha-alert alert-type="error">${this._error}</ha-alert>`;
}
if (!this._addons) {
return nothing;
}
return html`
<ha-combo-box
<ha-generic-picker
.hass=${this.hass}
.label=${this.label === undefined && this.hass
? this.hass.localize("ui.components.addon-picker.addon")
: this.label}
.value=${this._value}
.required=${this.required}
.disabled=${this.disabled}
.autofocus=${this.autofocus}
.label=${label}
.valueRenderer=${this._valueRenderer}
.helper=${this.helper}
.renderer=${rowRenderer}
.items=${this._addons}
item-value-path="slug"
item-id-path="slug"
item-label-path="name"
.disabled=${this.disabled}
.required=${this.required}
.value=${this.value}
.getItems=${this._getItems}
.searchKeys=${SEARCH_KEYS}
.rowRenderer=${rowRenderer}
@value-changed=${this._addonChanged}
></ha-combo-box>
>
</ha-generic-picker>
`;
}
@@ -93,9 +98,19 @@ class HaAddonPicker extends LitElement {
const addonsInfo = await fetchHassioAddonsInfo(this.hass);
this._addons = addonsInfo.addons
.filter((addon) => addon.version)
.sort((a, b) =>
stringCompare(a.name, b.name, this.hass.locale.language)
);
.map((addon) => ({
id: addon.slug,
primary: addon.name,
secondary: addon.slug,
icon: addon.icon
? `/api/hassio/addons/${addon.slug}/icon`
: undefined,
search_labels: {
description: addon.description || null,
repository: addon.repository || null,
},
sorting_label: [addon.name, addon.slug].filter(Boolean).join("_"),
}));
} else {
this._error = this.hass.localize(
"ui.components.addon-picker.error.no_supervisor"
@@ -108,6 +123,8 @@ class HaAddonPicker extends LitElement {
}
}
private _getItems = () => this._addons!;
private get _value() {
return this.value || "";
}
@@ -128,6 +145,17 @@ class HaAddonPicker extends LitElement {
fireEvent(this, "change");
}, 0);
}
private _valueRenderer = (itemId: string) => {
const item = this._addons!.find((addon) => addon.id === itemId);
return html`${item?.icon
? html`<img
slot="start"
alt=${item.primary ?? "Unknown"}
.src=${item.icon}
/>`
: nothing}<span slot="headline">${item?.primary || "Unknown"}</span>`;
};
}
declare global {

View File

@@ -51,9 +51,6 @@ export class HaAreaPicker extends LitElement {
@property({ type: Boolean, attribute: "no-add" })
public noAdd = false;
@property({ type: Boolean, attribute: "show-label" })
public showLabel = false;
/**
* Show only areas with entities from specific domains.
* @type {Array}
@@ -366,16 +363,19 @@ export class HaAreaPicker extends LitElement {
};
protected render(): TemplateResult {
const placeholder =
this.placeholder ?? this.hass.localize("ui.components.area-picker.area");
const baseLabel =
this.label ?? this.hass.localize("ui.components.area-picker.area");
const valueRenderer = this._computeValueRenderer(this.hass.areas);
let showLabel = this.showLabel;
if (this.value) {
// Only show label if there's no floor
let label: string | undefined = baseLabel;
if (this.value && baseLabel) {
const area = this.hass.areas[this.value];
if (area) {
const { floor } = getAreaContext(area, this.hass.floors);
showLabel = !floor && this.showLabel;
if (floor) {
label = undefined;
}
}
}
@@ -383,14 +383,12 @@ export class HaAreaPicker extends LitElement {
<ha-generic-picker
.hass=${this.hass}
.autofocus=${this.autofocus}
.label=${this.label}
.label=${label}
.helper=${this.helper}
.notFoundLabel=${this._notFoundLabel}
.emptyLabel=${this.hass.localize("ui.components.area-picker.no_areas")}
.disabled=${this.disabled}
.required=${this.required}
.placeholder=${placeholder}
.showLabel=${showLabel}
.value=${this.value}
.getItems=${this._getItems}
.getAdditionalItems=${this._getAdditionalItems}

View File

@@ -1,25 +1,23 @@
import { mdiInvertColorsOff, mdiPalette } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query } from "lit/decorators";
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { computeCssColor, THEME_COLORS } from "../common/color/compute-color";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import type { LocalizeKeys } from "../common/translations/localize";
import type { HomeAssistant } from "../types";
import "./ha-list-item";
import "./ha-md-divider";
import "./ha-select";
import type { HaSelect } from "./ha-select";
import "./ha-generic-picker";
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
import type { PickerValueRenderer } from "./ha-picker-field";
@customElement("ha-color-picker")
export class HaColorPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public label?: string;
@property() public helper?: string;
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public value?: string;
@property({ type: String, attribute: "default_color" })
@@ -33,137 +31,178 @@ export class HaColorPicker extends LitElement {
@property({ type: Boolean }) public disabled = false;
@query("ha-select") private _select?: HaSelect;
@property({ type: Boolean }) public required = false;
connectedCallback(): void {
super.connectedCallback();
// Refresh layout options when the field is connected to the DOM to ensure current value displayed
this._select?.layoutOptions();
render() {
const effectiveValue = this.value ?? this.defaultColor ?? "";
return html`
<ha-generic-picker
.hass=${this.hass}
.disabled=${this.disabled}
.required=${this.required}
.hideClearIcon=${!this.value && !!this.defaultColor}
.label=${this.label}
.helper=${this.helper}
.value=${effectiveValue}
.getItems=${this._getItems}
.rowRenderer=${this._rowRenderer}
.valueRenderer=${this._valueRenderer}
@value-changed=${this._valueChanged}
>
</ha-generic-picker>
`;
}
private _valueSelected(ev) {
ev.stopPropagation();
if (!this.isConnected) return;
const value = ev.target.value;
this.value = value === this.defaultColor ? undefined : value;
fireEvent(this, "value-changed", {
value: this.value,
private _getItems = () =>
this._getColors(
this.includeNone,
this.includeState,
this.defaultColor,
this.value
);
private _getColors = (
includeNone: boolean,
includeState: boolean,
defaultColor: string | undefined,
currentValue: string | undefined
): PickerComboBoxItem[] => {
const items: PickerComboBoxItem[] = [];
const defaultSuffix = this.hass.localize(
"ui.components.color-picker.default"
);
const addDefaultSuffix = (label: string, isDefault: boolean) =>
isDefault && defaultSuffix ? `${label} (${defaultSuffix})` : label;
if (includeNone) {
const noneLabel =
this.hass.localize("ui.components.color-picker.none") || "None";
items.push({
id: "none",
primary: addDefaultSuffix(noneLabel, defaultColor === "none"),
icon_path: mdiInvertColorsOff,
sorting_label: noneLabel,
});
}
render() {
const value = this.value || this.defaultColor || "";
if (includeState) {
const stateLabel =
this.hass.localize("ui.components.color-picker.state") || "State";
items.push({
id: "state",
primary: addDefaultSuffix(stateLabel, defaultColor === "state"),
icon_path: mdiPalette,
sorting_label: stateLabel,
});
}
const isCustom = !(
THEME_COLORS.has(value) ||
value === "none" ||
value === "state"
);
return html`
<ha-select
.icon=${Boolean(value)}
.label=${this.label}
.value=${value}
.helper=${this.helper}
.disabled=${this.disabled}
@closed=${stopPropagation}
@selected=${this._valueSelected}
fixedMenuPosition
naturalMenuWidth
.clearable=${!this.defaultColor}
>
${value
? html`
<span slot="icon">
${value === "none"
? html`
<ha-svg-icon path=${mdiInvertColorsOff}></ha-svg-icon>
`
: value === "state"
? html`<ha-svg-icon path=${mdiPalette}></ha-svg-icon>`
: this._renderColorCircle(value || "grey")}
</span>
`
: nothing}
${this.includeNone
? html`
<ha-list-item value="none" graphic="icon">
${this.hass.localize("ui.components.color-picker.none")}
${this.defaultColor === "none"
? ` (${this.hass.localize("ui.components.color-picker.default")})`
: nothing}
<ha-svg-icon
slot="graphic"
path=${mdiInvertColorsOff}
></ha-svg-icon>
</ha-list-item>
`
: nothing}
${this.includeState
? html`
<ha-list-item value="state" graphic="icon">
${this.hass.localize("ui.components.color-picker.state")}
${this.defaultColor === "state"
? ` (${this.hass.localize("ui.components.color-picker.default")})`
: nothing}
<ha-svg-icon slot="graphic" path=${mdiPalette}></ha-svg-icon>
</ha-list-item>
`
: nothing}
${this.includeState || this.includeNone
? html`<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>`
: nothing}
${Array.from(THEME_COLORS).map(
(color) => html`
<ha-list-item .value=${color} graphic="icon">
${this.hass.localize(
Array.from(THEME_COLORS).forEach((color) => {
const themeLabel =
this.hass.localize(
`ui.components.color-picker.colors.${color}` as LocalizeKeys
) || color}
${this.defaultColor === color
? ` (${this.hass.localize("ui.components.color-picker.default")})`
: nothing}
<span slot="graphic">${this._renderColorCircle(color)}</span>
</ha-list-item>
`
)}
${isCustom
? html`
<ha-list-item .value=${value} graphic="icon">
${value}
<span slot="graphic">${this._renderColorCircle(value)}</span>
</ha-list-item>
`
: nothing}
</ha-select>
) || color;
items.push({
id: color,
primary: addDefaultSuffix(themeLabel, defaultColor === color),
sorting_label: themeLabel,
});
});
const isSpecial =
currentValue === "none" ||
currentValue === "state" ||
THEME_COLORS.has(currentValue || "");
const hasValue = currentValue && currentValue.length > 0;
if (hasValue && !isSpecial) {
items.push({
id: currentValue!,
primary: currentValue!,
sorting_label: currentValue!,
});
}
return items;
};
private _rowRenderer: (
item: PickerComboBoxItem,
index?: number
) => ReturnType<typeof html> = (item) => html`
<ha-combo-box-item type="button" compact>
${item.id === "none"
? html`<ha-svg-icon
slot="start"
.path=${mdiInvertColorsOff}
></ha-svg-icon>`
: item.id === "state"
? html`<ha-svg-icon slot="start" .path=${mdiPalette}></ha-svg-icon>`
: html`<span slot="start">
${this._renderColorCircle(item.id)}
</span>`}
<span slot="headline">${item.primary}</span>
</ha-combo-box-item>
`;
private _valueRenderer: PickerValueRenderer = (value: string) => {
if (value === "none") {
return html`
<ha-svg-icon slot="start" .path=${mdiInvertColorsOff}></ha-svg-icon>
<span slot="headline">
${this.hass.localize("ui.components.color-picker.none")}
</span>
`;
}
if (value === "state") {
return html`
<ha-svg-icon slot="start" .path=${mdiPalette}></ha-svg-icon>
<span slot="headline">
${this.hass.localize("ui.components.color-picker.state")}
</span>
`;
}
return html`
<span slot="start">${this._renderColorCircle(value)}</span>
<span slot="headline">
${this.hass.localize(
`ui.components.color-picker.colors.${value}` as LocalizeKeys
) || value}
</span>
`;
};
private _renderColorCircle(color: string) {
return html`
<span
class="circle-color"
style=${styleMap({
"--circle-color": computeCssColor(color),
display: "block",
"background-color": "var(--circle-color, var(--divider-color))",
border: "1px solid var(--outline-color)",
"border-radius": "var(--ha-border-radius-pill)",
width: "20px",
height: "20px",
"box-sizing": "border-box",
})}
></span>
`;
}
static styles = css`
.circle-color {
display: block;
background-color: var(--circle-color, var(--divider-color));
border: 1px solid var(--outline-color);
border-radius: var(--ha-border-radius-pill);
width: 20px;
height: 20px;
box-sizing: border-box;
private _valueChanged(ev: CustomEvent<{ value?: string }>) {
ev.stopPropagation();
const selected = ev.detail.value;
const normalized =
selected && selected === this.defaultColor
? undefined
: (selected ?? undefined);
this.value = normalized;
fireEvent(this, "value-changed", { value: this.value });
}
ha-select {
width: 100%;
}
`;
}
declare global {

View File

@@ -57,10 +57,9 @@ class HaConfigEntryPicker extends LitElement {
return html`
<ha-generic-picker
.hass=${this.hass}
.placeholder=${this.label === undefined && this.hass
.label=${this.label === undefined && this.hass
? this.hass.localize("ui.components.config-entry-picker.config_entry")
: this.label}
show-label
.value=${this.value}
.required=${this.required}
.disabled=${this.disabled}

View File

@@ -389,14 +389,14 @@ export class HaFloorPicker extends LitElement {
<ha-generic-picker
.hass=${this.hass}
.autofocus=${this.autofocus}
.disabled=${this.disabled}
.label=${this.label}
.helper=${this.helper}
.disabled=${this.disabled}
.placeholder=${placeholder}
.notFoundLabel=${this._notFoundLabel}
.emptyLabel=${this.hass.localize(
"ui.components.floor-picker.no_floors"
)}
.placeholder=${placeholder}
.value=${this.value}
.getItems=${this._getItems}
.getAdditionalItems=${this._getAdditionalItems}

View File

@@ -7,8 +7,10 @@ import { ifDefined } from "lit/directives/if-defined";
import memoizeOne from "memoize-one";
import { tinykeys } from "tinykeys";
import { fireEvent } from "../common/dom/fire_event";
import { PickerMixin } from "../mixins/picker-mixin";
import type { FuseWeightedKey } from "../resources/fuseMultiTerm";
import type { HomeAssistant } from "../types";
import { isIosApp } from "../util/is_ios";
import "./ha-bottom-sheet";
import "./ha-button";
import "./ha-combo-box-item";
@@ -20,39 +22,18 @@ import type {
PickerComboBoxSearchFn,
} from "./ha-picker-combo-box";
import "./ha-picker-field";
import type { PickerValueRenderer } from "./ha-picker-field";
import "./ha-svg-icon";
@customElement("ha-generic-picker")
export class HaGenericPicker extends LitElement {
export class HaGenericPicker extends PickerMixin(LitElement) {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = false;
@property({ type: Boolean, attribute: "allow-custom-value" })
public allowCustomValue;
@property() public value?: string;
@property() public icon?: string;
@property() public label?: string;
@property() public helper?: string;
@property() public placeholder?: string;
@property({ type: String, attribute: "search-label" })
public searchLabel?: string;
@property({ attribute: "hide-clear-icon", type: Boolean })
public hideClearIcon = false;
@property({ attribute: "show-label", type: Boolean })
public showLabel = false;
/** To prevent lags, getItems needs to be memoized */
@property({ attribute: false })
public getItems!: (
@@ -66,9 +47,6 @@ export class HaGenericPicker extends LitElement {
@property({ attribute: false })
public rowRenderer?: RenderItemFunction<PickerComboBoxItem>;
@property({ attribute: false })
public valueRenderer?: PickerValueRenderer;
@property({ attribute: false })
public searchFn?: PickerComboBoxSearchFn<PickerComboBoxItem>;
@@ -118,7 +96,11 @@ export class HaGenericPicker extends LitElement {
@property({ attribute: "selected-section" }) public selectedSection?: string;
@property({ attribute: "unknown-item-text" }) public unknownItemText?: string;
@property({ type: Boolean, attribute: "use-top-label" })
public useTopLabel = false;
@property({ attribute: "custom-value-label" })
public customValueLabel?: string;
@query(".container") private _containerElement?: HTMLDivElement;
@@ -149,11 +131,13 @@ export class HaGenericPicker extends LitElement {
private _unsubscribeTinyKeys?: () => void;
protected render() {
return html`
${this.label
// Only show label if it's not a top label and there is a value.
const label = this.useTopLabel && this.value ? undefined : this.label;
return html`<div class="container">
${this.useTopLabel && this.label
? html`<label ?disabled=${this.disabled}>${this.label}</label>`
: nothing}
<div class="container">
<div id="picker">
<slot name="field">
${this.addButtonLabel && !this.value
@@ -173,14 +157,20 @@ export class HaGenericPicker extends LitElement {
type="button"
class=${this._opened ? "opened" : ""}
compact
.unknown=${this._unknownValue(this.value, this.getItems())}
.unknown=${this._unknownValue(
this.allowCustomValue,
this.value,
this.getItems()
)}
.unknownItemText=${this.unknownItemText}
aria-label=${ifDefined(this.label)}
@click=${this.open}
@clear=${this._clear}
.icon=${this.icon}
.showLabel=${this.showLabel}
.image=${this.image}
.label=${label}
.placeholder=${this.placeholder}
.helper=${this.helper}
.value=${this.value}
.valueRenderer=${this.valueRenderer}
.required=${this.required}
@@ -188,6 +178,7 @@ export class HaGenericPicker extends LitElement {
.invalid=${this.invalid}
.hideClearIcon=${this.hideClearIcon}
>
<slot name="start"></slot>
</ha-picker-field>`}
</slot>
</div>
@@ -226,8 +217,7 @@ export class HaGenericPicker extends LitElement {
</ha-bottom-sheet>`
: nothing}
</div>
${this._renderHelper()}
`;
${this._renderHelper()}`;
}
private _renderComboBox(dialogMode = false) {
@@ -236,6 +226,7 @@ export class HaGenericPicker extends LitElement {
}
return html`
<ha-picker-combo-box
id="combo-box"
.hass=${this.hass}
.allowCustomValue=${this.allowCustomValue}
.label=${this.searchLabel}
@@ -252,13 +243,24 @@ export class HaGenericPicker extends LitElement {
.sectionTitleFunction=${this.sectionTitleFunction}
.selectedSection=${this.selectedSection}
.searchKeys=${this.searchKeys}
.customValueLabel=${this.customValueLabel}
></ha-picker-combo-box>
`;
}
private _unknownValue = memoizeOne(
(value?: string, items?: (PickerComboBoxItem | string)[]) => {
if (value === undefined || value === null || value === "" || !items) {
(
allowCustomValue: boolean,
value?: string,
items?: (PickerComboBoxItem | string)[]
) => {
if (
allowCustomValue ||
value === undefined ||
value === null ||
value === "" ||
!items
) {
return false;
}
@@ -284,6 +286,15 @@ export class HaGenericPicker extends LitElement {
private _dialogOpened = () => {
this._opened = true;
requestAnimationFrame(() => {
if (this.hass && isIosApp(this.hass)) {
this.hass.auth.external!.fireMessage({
type: "focus_element",
payload: {
element_id: "combo-box",
},
});
return;
}
this._comboBox?.focus();
});
};
@@ -310,7 +321,7 @@ export class HaGenericPicker extends LitElement {
this._newValue = value;
}
private _clear(e) {
private _clear(e: CustomEvent) {
e.stopPropagation();
this._setValue(undefined);
}

View File

@@ -113,7 +113,6 @@ export class HaIconPicker extends LitElement {
<ha-generic-picker
.hass=${this.hass}
allow-custom-value
show-label
.getItems=${this._getIconPickerItems}
.helper=${this.helper}
.disabled=${this.disabled}
@@ -122,7 +121,7 @@ export class HaIconPicker extends LitElement {
.invalid=${this.invalid}
.rowRenderer=${rowRenderer}
.icon=${this._icon}
.placeholder=${this.label}
.label=${this.label}
.value=${this._value}
.searchFn=${this._filterIcons}
.notFoundLabel=${this.hass?.localize(
@@ -131,6 +130,7 @@ export class HaIconPicker extends LitElement {
popover-placement="bottom-start"
@value-changed=${this._valueChanged}
>
<slot name="start"></slot>
</ha-generic-picker>
`;
}

View File

@@ -116,6 +116,11 @@ export class HaLanguagePicker extends LitElement {
> `;
protected render() {
const label =
this.label ??
(this.hass?.localize("ui.components.language-picker.language") ||
"Language");
const value =
this.value ??
(this.required && !this.disabled ? this._getItems()[0].id : this.value);
@@ -129,10 +134,7 @@ export class HaLanguagePicker extends LitElement {
.emptyLabel=${this.hass?.localize(
"ui.components.language-picker.no_languages"
) || "No languages available"}
.placeholder=${this.label ??
(this.hass?.localize("ui.components.language-picker.language") ||
"Language")}
show-label
.label=${label}
.value=${value}
.valueRenderer=${this._valueRenderer}
.disabled=${this.disabled}

View File

@@ -1,55 +1,17 @@
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { titleCase } from "../common/string/title-case";
import { fetchConfig } from "../data/lovelace/config/types";
import type { LovelaceViewRawConfig } from "../data/lovelace/config/view";
import { getPanelIcon, getPanelTitle } from "../data/panel";
import type { HomeAssistant, PanelInfo, ValueChangedEvent } from "../types";
import "./ha-combo-box";
import type { HaComboBox } from "./ha-combo-box";
import "./ha-combo-box-item";
import type { HomeAssistant, ValueChangedEvent } from "../types";
import "./ha-generic-picker";
import "./ha-icon";
interface NavigationItem {
path: string;
icon: string;
title: string;
}
const DEFAULT_ITEMS: NavigationItem[] = [];
const rowRenderer: ComboBoxLitRenderer<NavigationItem> = (item) => html`
<ha-combo-box-item type="button">
<ha-icon .icon=${item.icon} slot="start"></ha-icon>
<span slot="headline">${item.title || item.path}</span>
${item.title
? html`<span slot="supporting-text">${item.path}</span>`
: nothing}
</ha-combo-box-item>
`;
const createViewNavigationItem = (
prefix: string,
view: LovelaceViewRawConfig,
index: number
) => ({
path: `/${prefix}/${view.path ?? index}`,
icon: view.icon ?? "mdi:view-compact",
title: view.title ?? (view.path ? titleCase(view.path) : `${index}`),
});
const createPanelNavigationItem = (hass: HomeAssistant, panel: PanelInfo) => ({
path: `/${panel.url_path}`,
icon: getPanelIcon(panel) || "mdi:view-dashboard",
title: getPanelTitle(hass, panel) || "",
});
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
@customElement("ha-navigation-picker")
export class HaNavigationPicker extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public label?: string;
@@ -61,46 +23,51 @@ export class HaNavigationPicker extends LitElement {
@property({ type: Boolean }) public required = false;
@state() private _opened = false;
@state() private _loading = true;
private navigationItemsLoaded = false;
protected firstUpdated() {
this._loadNavigationItems();
}
private navigationItems: NavigationItem[] = DEFAULT_ITEMS;
private _navigationItems: PickerComboBoxItem[] = [];
@query("ha-combo-box", true) private comboBox!: HaComboBox;
protected render(): TemplateResult {
protected render() {
return html`
<ha-combo-box
<ha-generic-picker
.hass=${this.hass}
item-value-path="path"
item-label-path="path"
.value=${this._value}
.value=${this._loading ? undefined : this.value}
allow-custom-value
.filteredItems=${this.navigationItems}
.label=${this.label}
.placeholder=${this.label}
.helper=${this.helper}
.disabled=${this.disabled}
.disabled=${this._loading || this.disabled}
.required=${this.required}
.renderer=${rowRenderer}
@opened-changed=${this._openedChanged}
.getItems=${this._getItems}
.valueRenderer=${this._valueRenderer}
.customValueLabel=${this.hass.localize(
"ui.components.navigation-picker.add_custom_path"
)}
@value-changed=${this._valueChanged}
@filter-changed=${this._filterChanged}
>
</ha-combo-box>
</ha-generic-picker>
`;
}
private async _openedChanged(ev: ValueChangedEvent<boolean>) {
this._opened = ev.detail.value;
if (this._opened && !this.navigationItemsLoaded) {
this._loadNavigationItems();
}
}
private _valueRenderer = (itemId: string) => {
const item = this._navigationItems.find((navItem) => navItem.id === itemId);
return html`
${item?.icon
? html`<ha-icon slot="start" .icon=${item.icon}></ha-icon>`
: nothing}
<span slot="headline">${item?.primary || itemId}</span>
${item?.primary
? html`<span slot="supporting-text">${itemId}</span>`
: nothing}
`;
};
private _getItems = () => this._navigationItems;
private async _loadNavigationItems() {
this.navigationItemsLoaded = true;
const panels = Object.entries(this.hass!.panels).map(([id, panel]) => ({
id,
...panel,
@@ -124,27 +91,47 @@ export class HaNavigationPicker extends LitElement {
const panelViewConfig = new Map(viewConfigs);
this.navigationItems = [];
this._navigationItems = [];
for (const panel of panels) {
this.navigationItems.push(createPanelNavigationItem(this.hass!, panel));
const path = `/${panel.url_path}`;
const panelTitle = getPanelTitle(this.hass, panel);
const primary = panelTitle || path;
this._navigationItems.push({
id: path,
primary,
secondary: panelTitle ? path : undefined,
icon: getPanelIcon(panel) || "mdi:view-dashboard",
sorting_label: [
primary.startsWith("/") ? `zzz${primary}` : primary,
path,
]
.filter(Boolean)
.join("_"),
});
const config = panelViewConfig.get(panel.id);
if (!config || !("views" in config)) continue;
config.views.forEach((view, index) =>
this.navigationItems.push(
createViewNavigationItem(panel.url_path, view, index)
)
);
config.views.forEach((view, index) => {
const viewPath = `/${panel.url_path}/${view.path ?? index}`;
const viewPrimary =
view.title ?? (view.path ? titleCase(view.path) : `${index}`);
this._navigationItems.push({
id: viewPath,
secondary: viewPath,
icon: view.icon ?? "mdi:view-compact",
primary: viewPrimary,
sorting_label: [
viewPrimary.startsWith("/") ? `zzz${viewPrimary}` : viewPrimary,
viewPath,
].join("_"),
});
});
}
this.comboBox.filteredItems = this.navigationItems;
}
protected shouldUpdate(changedProps: PropertyValues) {
return !this._opened || changedProps.has("_opened");
this._loading = false;
}
private _valueChanged(ev: ValueChangedEvent<string>) {
@@ -152,61 +139,18 @@ export class HaNavigationPicker extends LitElement {
this._setValue(ev.detail.value);
}
private _setValue(value: string) {
private _setValue(value = "") {
this.value = value;
fireEvent(
this,
"value-changed",
{ value: this._value },
{ value: this.value },
{
bubbles: false,
composed: false,
}
);
}
private _filterChanged(ev: CustomEvent): void {
const filterString = ev.detail.value.toLowerCase();
const characterCount = filterString.length;
if (characterCount >= 2) {
const filteredItems: NavigationItem[] = [];
this.navigationItems.forEach((item) => {
if (
item.path.toLowerCase().includes(filterString) ||
item.title.toLowerCase().includes(filterString)
) {
filteredItems.push(item);
}
});
if (filteredItems.length > 0) {
this.comboBox.filteredItems = filteredItems;
} else {
this.comboBox.filteredItems = [];
}
} else {
this.comboBox.filteredItems = this.navigationItems;
}
}
private get _value() {
return this.value || "";
}
static styles = css`
ha-icon,
ha-svg-icon {
color: var(--primary-text-color);
position: relative;
bottom: 0px;
}
*[slot="prefix"] {
margin-right: 8px;
margin-inline-end: 8px;
margin-inline-start: initial;
}
`;
}
declare global {

View File

@@ -1,6 +1,6 @@
import type { LitVirtualizer } from "@lit-labs/virtualizer";
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
import { mdiMagnify, mdiMinusBoxOutline } from "@mdi/js";
import { mdiMagnify, mdiMinusBoxOutline, mdiPlus } from "@mdi/js";
import Fuse from "fuse.js";
import { css, html, LitElement, nothing } from "lit";
import {
@@ -53,7 +53,8 @@ export interface PickerComboBoxItem {
icon_path?: string;
icon?: string;
}
const NO_ITEMS_AVAILABLE_ID = "___no_items_available___";
export const NO_ITEMS_AVAILABLE_ID = "___no_items_available___";
const DEFAULT_ROW_RENDERER: RenderItemFunction<PickerComboBoxItem> = (
item
@@ -91,6 +92,9 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
@property({ type: Boolean, attribute: "allow-custom-value" })
public allowCustomValue;
@property({ attribute: "custom-value-label" })
public customValueLabel?: string;
@property() public label?: string;
@property() public value?: string;
@@ -187,10 +191,15 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
}
protected render() {
const searchLabel =
this.label ??
(this.allowCustomValue
? (this.hass?.localize("ui.components.combo-box.search_or_custom") ??
"Search | Add custom value")
: (this.hass?.localize("ui.common.search") ?? "Search"));
return html`<ha-textfield
.label=${this.label ??
this.hass?.localize("ui.common.search") ??
"Search"}
.label=${searchLabel}
@input=${this._filterChanged}
></ha-textfield>
${this._renderSectionButtons()}
@@ -438,13 +447,23 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
);
}
if (this.allowCustomValue && searchString) {
filteredItems.push({
id: searchString,
primary:
this.customValueLabel ??
this.hass?.localize("ui.components.combo-box.add_custom_item") ??
"Add custom item",
secondary: `"${searchString}"`,
icon_path: mdiPlus,
});
}
this._items = filteredItems as PickerComboBoxItem[];
}
this._selectedItemIndex = -1;
if (this._virtualizerElement) {
this._virtualizerElement.scrollTo(0, 0);
}
this._valuePinned = true;
};
private _toggleSection(ev: Event) {
@@ -639,7 +658,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
typeof item === "string" ? item : item?.id;
private _getInitialSelectedIndex() {
if (!this._virtualizerElement || !this.value) {
if (!this._virtualizerElement || this._search || !this.value) {
return 0;
}

View File

@@ -9,13 +9,15 @@ import {
type TemplateResult,
} from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { fireEvent } from "../common/dom/fire_event";
import { localizeContext } from "../data/context";
import { PickerMixin } from "../mixins/picker-mixin";
import type { HomeAssistant } from "../types";
import "./ha-combo-box-item";
import type { HaComboBoxItem } from "./ha-combo-box-item";
import "./ha-icon-button";
import "./ha-icon";
import "./ha-icon-button";
declare global {
interface HASSDomEvents {
@@ -26,32 +28,7 @@ declare global {
export type PickerValueRenderer = (value: string) => TemplateResult<1>;
@customElement("ha-picker-field")
export class HaPickerField extends LitElement {
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = false;
@property() public value?: string;
@property() public icon?: string;
@property() public helper?: string;
@property() public placeholder?: string;
@property({ type: Boolean, reflect: true }) public unknown = false;
@property({ attribute: "unknown-item-text" }) public unknownItemText?: string;
@property({ attribute: "hide-clear-icon", type: Boolean })
public hideClearIcon = false;
@property({ attribute: "show-label", type: Boolean })
public showLabel = false;
@property({ attribute: false })
public valueRenderer?: PickerValueRenderer;
export class HaPickerField extends PickerMixin(LitElement) {
@property({ type: Boolean, reflect: true }) public invalid = false;
@query("ha-combo-box-item", true) public item!: HaComboBoxItem;
@@ -66,31 +43,48 @@ export class HaPickerField extends LitElement {
}
protected render() {
const hasValue = !!this.value?.length;
const hasValue = !!this.value;
const showClearIcon =
!!this.value && !this.required && !this.disabled && !this.hideClearIcon;
const placeholderText = this.placeholder ?? this.label;
const overlineLabel =
this.showLabel && hasValue && this.placeholder
? html`<span slot="overline">${this.placeholder}</span>`
this.label && hasValue
? html`<span slot="overline"
>${this.label}${this.required ? " *" : ""}</span
>`
: nothing;
const headlineContent = hasValue
? this.valueRenderer
? this.valueRenderer(this.value ?? "")
: html`<span slot="headline">${this.value}</span>`
: this.placeholder
: placeholderText
? html`<span slot="headline" class="placeholder">
${this.placeholder}
${placeholderText}${this.required ? " *" : ""}
</span>`
: nothing;
return html`
<ha-combo-box-item .disabled=${this.disabled} type="button" compact>
${this.icon
<ha-combo-box-item
aria-label=${ifDefined(this.label || this.placeholder)}
.disabled=${this.disabled}
type="button"
compact
>
${this.image
? html`<img
alt=${this.label ?? ""}
slot="start"
.src=${this.image}
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>`
: this.icon
? html`<ha-icon slot="start" .icon=${this.icon}></ha-icon>`
: nothing}
: html`<slot name="start"></slot>`}
${overlineLabel}${headlineContent}
${this.unknown
? html`<div slot="supporting-text" class="unknown">
@@ -117,7 +111,7 @@ export class HaPickerField extends LitElement {
`;
}
private _clear(e) {
private _clear(e: CustomEvent) {
e.stopPropagation();
fireEvent(this, "clear");
}
@@ -200,7 +194,10 @@ export class HaPickerField extends LitElement {
.placeholder {
color: var(--secondary-text-color);
padding: 0 8px;
}
:host([invalid]) .placeholder {
color: var(--mdc-theme-error, var(--error-color, #b00020));
}
.unknown {

View File

@@ -28,7 +28,6 @@ export class HaAddonSelector extends LitElement {
.helper=${this.helper}
.disabled=${this.disabled}
.required=${this.required}
allow-custom-entity
></ha-addon-picker>`;
}

View File

@@ -29,7 +29,6 @@ export class HaConfigEntrySelector extends LitElement {
.disabled=${this.disabled}
.required=${this.required}
.integration=${this.selector.config_entry?.integration}
allow-custom-entity
></ha-config-entry-picker>`;
}

View File

@@ -107,7 +107,6 @@ export class HaDeviceSelector extends LitElement {
.placeholder=${this.placeholder}
.disabled=${this.disabled}
.required=${this.required}
allow-custom-entity
></ha-device-picker>
`;
}

View File

@@ -66,15 +66,14 @@ export class HaEntitySelector extends LitElement {
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.placeholder=${this.placeholder}
.helper=${this.helper}
.includeEntities=${this.selector.entity?.include_entities}
.excludeEntities=${this.selector.entity?.exclude_entities}
.entityFilter=${this._filterEntities}
.createDomains=${this._createDomains}
.placeholder=${this.placeholder}
.disabled=${this.disabled}
.required=${this.required}
allow-custom-entity
></ha-entity-picker>`;
}
@@ -83,13 +82,13 @@ export class HaEntitySelector extends LitElement {
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.placeholder=${this.placeholder}
.helper=${this.helper}
.includeEntities=${this.selector.entity.include_entities}
.excludeEntities=${this.selector.entity.exclude_entities}
.reorder=${this.selector.entity.reorder ?? false}
.entityFilter=${this._filterEntities}
.createDomains=${this._createDomains}
.placeholder=${this.placeholder}
.disabled=${this.disabled}
.required=${this.required}
></ha-entities-picker>

View File

@@ -52,7 +52,7 @@ export class HaIconSelector extends LitElement {
${!placeholder && stateObj
? html`
<ha-state-icon
slot="fallback"
slot="start"
.hass=${this.hass}
.stateObj=${stateObj}
></ha-state-icon>

View File

@@ -135,7 +135,6 @@ class HaServicePicker extends LitElement {
<ha-generic-picker
.hass=${this.hass}
.autofocus=${this.autofocus}
allow-custom-value
.notFoundLabel=${this.hass.localize(
"ui.components.service-picker.no_match"
)}

View File

@@ -235,6 +235,12 @@ export class HaWaDialog extends ScrollableFadeMixin(LitElement) {
);
max-width: var(--ha-dialog-max-width, var(--safe-width));
}
@media (prefers-reduced-motion: reduce) {
wa-dialog {
--show-duration: 0ms;
--hide-duration: 0ms;
}
}
:host([width="small"]) wa-dialog {
--width: min(var(--ha-dialog-width-sm, 320px), var(--full-width));

View File

@@ -132,9 +132,9 @@ class HaUserPicker extends LitElement {
.hass=${this.hass}
.autofocus=${this.autofocus}
.label=${this.label}
.notFoundLabel=${this._notFoundLabel}
.placeholder=${placeholder}
.value=${this.value}
.notFoundLabel=${this._notFoundLabel}
.getItems=${this._getItems}
.valueRenderer=${this._valueRenderer}
.rowRenderer=${this._rowRenderer}

View File

@@ -88,7 +88,6 @@ export const DOMAINS_HIDE_DEFAULT_MORE_INFO = [
"select",
"text",
"update",
"event",
];
/** Domains that should have the history hidden in the more info dialog. */

View File

@@ -176,6 +176,13 @@ interface EMOutgoingMessageAddEntityTo extends EMMessage {
};
}
interface EMOutgoingMessageFocusElement extends EMMessage {
type: "focus_element";
payload: {
element_id: string;
};
}
type EMOutgoingMessageWithoutAnswer =
| EMMessageResultError
| EMMessageResultSuccess
@@ -197,7 +204,8 @@ type EMOutgoingMessageWithoutAnswer =
| EMOutgoingMessageThreadStoreInPlatformKeychain
| EMOutgoingMessageImprovScan
| EMOutgoingMessageImprovConfigureDevice
| EMOutgoingMessageAddEntityTo;
| EMOutgoingMessageAddEntityTo
| EMOutgoingMessageFocusElement;
export interface EMIncomingMessageRestart {
id: number;

View File

@@ -0,0 +1,38 @@
import type { ReactiveElement } from "lit";
import { property } from "lit/decorators";
import type { Constructor } from "../types";
import type { PickerValueRenderer } from "../components/ha-picker-field";
export const PickerMixin = <T extends Constructor<ReactiveElement>>(
superClass: T
) => {
class PickerFieldClass extends superClass {
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = false;
@property() public icon?: string;
@property() public image?: string;
@property() public label?: string;
@property() public placeholder?: string;
@property() public helper?: string;
@property() public value?: string;
@property({ type: Boolean, reflect: true }) public unknown = false;
@property({ attribute: "unknown-item-text" })
public unknownItemText?: string;
@property({ attribute: "hide-clear-icon", type: Boolean })
public hideClearIcon = false;
@property({ attribute: false })
public valueRenderer?: PickerValueRenderer;
}
return PickerFieldClass;
};

View File

@@ -5,13 +5,15 @@ import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-combo-box";
import { createCloseHeading } from "../../../components/ha-dialog";
import "../../../components/ha-dialog-footer";
import "../../../components/ha-fade-in";
import "../../../components/ha-generic-picker";
import "../../../components/ha-markdown";
import "../../../components/ha-password-field";
import type { PickerComboBoxItem } from "../../../components/ha-picker-combo-box";
import "../../../components/ha-spinner";
import "../../../components/ha-textfield";
import "../../../components/ha-wa-dialog";
import type {
ApplicationCredential,
ApplicationCredentialsConfig,
@@ -59,6 +61,10 @@ export class DialogAddApplicationCredential extends LitElement {
@state() private _config?: ApplicationCredentialsConfig;
@state() private _open = false;
@state() private _invalid = false;
public showDialog(params: AddApplicationCredentialDialogParams) {
this._params = params;
this._domain = params.selectedDomain;
@@ -69,6 +75,7 @@ export class DialogAddApplicationCredential extends LitElement {
this._clientSecret = "";
this._error = undefined;
this._loading = false;
this._open = true;
this._fetchConfig();
}
@@ -90,16 +97,16 @@ export class DialogAddApplicationCredential extends LitElement {
? domainToName(this.hass.localize, this._domain!)
: "";
return html`
<ha-dialog
open
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
@closed=${this._abortDialog}
scrimClickAction
escapeKeyAction
.heading=${createCloseHeading(
this.hass,
this.hass.localize(
.preventScrimClose=${!!this._domain ||
!!this._name ||
!!this._clientId ||
!!this._clientSecret}
.headerTitle=${this.hass.localize(
"ui.panel.config.application_credentials.editor.caption"
)
)}
>
${!this._config
@@ -165,20 +172,23 @@ export class DialogAddApplicationCredential extends LitElement {
: nothing}
${this._params.selectedDomain
? nothing
: html`<ha-combo-box
: html`<ha-generic-picker
name="domain"
.hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.config.application_credentials.editor.domain"
)}
.value=${this._domain}
.items=${this._domains}
item-id-path="id"
item-value-path="id"
item-label-path="name"
.invalid=${this._invalid && !this._domain}
.getItems=${this._getDomainItems}
required
.disabled=${!this._domains}
.valueRenderer=${this._domainRenderer}
@value-changed=${this._handleDomainPicked}
></ha-combo-box>`}
.errorMessage=${this.hass.localize(
"ui.common.error_required"
)}
></ha-generic-picker>`}
${this._description
? html`<ha-markdown
breaks
@@ -192,9 +202,10 @@ export class DialogAddApplicationCredential extends LitElement {
"ui.panel.config.application_credentials.editor.name"
)}
.value=${this._name}
.invalid=${this._invalid && !this._name}
required
@input=${this._handleValueChanged}
.validationMessage=${this.hass.localize(
.errorMessage=${this.hass.localize(
"ui.common.error_required"
)}
dialogInitialFocus
@@ -206,9 +217,10 @@ export class DialogAddApplicationCredential extends LitElement {
"ui.panel.config.application_credentials.editor.client_id"
)}
.value=${this._clientId}
.invalid=${this._invalid && !this._clientId}
required
@input=${this._handleValueChanged}
.validationMessage=${this.hass.localize(
.errorMessage=${this.hass.localize(
"ui.common.error_required"
)}
dialogInitialFocus
@@ -223,9 +235,10 @@ export class DialogAddApplicationCredential extends LitElement {
)}
name="clientSecret"
.value=${this._clientSecret}
.invalid=${this._invalid && !this._clientSecret}
required
@input=${this._handleValueChanged}
.validationMessage=${this.hass.localize(
.errorMessage=${this.hass.localize(
"ui.common.error_required"
)}
.helper=${this.hass.localize(
@@ -235,30 +248,33 @@ export class DialogAddApplicationCredential extends LitElement {
></ha-password-field>
</div>
<ha-dialog-footer slot="footer">
<ha-button
appearance="plain"
slot="secondaryAction"
@click=${this._abortDialog}
@click=${this._closeDialog}
.disabled=${this._loading}
>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button
slot="primaryAction"
.disabled=${!this._domain ||
!this._clientId ||
!this._clientSecret}
@click=${this._addApplicationCredential}
.loading=${this._loading}
>
${this.hass.localize(
"ui.panel.config.application_credentials.editor.add"
)}
</ha-button>`}
</ha-dialog>
</ha-button>
</ha-dialog-footer>`}
</ha-wa-dialog>
`;
}
private _closeDialog() {
this._open = false;
}
public closeDialog() {
this._params = undefined;
this._domains = undefined;
@@ -303,9 +319,16 @@ export class DialogAddApplicationCredential extends LitElement {
private async _addApplicationCredential(ev) {
ev.preventDefault();
if (!this._domain || !this._clientId || !this._clientSecret) {
if (
!this._domain ||
!this._name ||
!this._clientId ||
!this._clientSecret
) {
this._invalid = true;
return;
}
this._invalid = false;
this._loading = true;
this._error = "";
@@ -328,6 +351,20 @@ export class DialogAddApplicationCredential extends LitElement {
this.closeDialog();
}
private _getDomainItems = (): PickerComboBoxItem[] =>
this._domains?.map((domain) => ({
id: domain.id,
primary: domain.name,
sorting_label: domain.name,
})) ?? [];
private _domainRenderer = (domainId: string) => {
const domain = this._domains?.find((d) => d.id === domainId);
return html`<span slot="headline"
>${domain ? domain.name : domainId}</span
>`;
};
static get styles(): CSSResultGroup {
return [
haStyleDialog,
@@ -338,15 +375,12 @@ export class DialogAddApplicationCredential extends LitElement {
}
.row {
display: flex;
padding: 8px 0;
}
ha-combo-box {
display: block;
margin-bottom: 24px;
padding: var(--ha-space-2) 0;
}
ha-textfield {
display: block;
margin-bottom: 24px;
margin-top: var(--ha-space-4);
margin-bottom: var(--ha-space-4);
}
a {
text-decoration: none;
@@ -355,7 +389,8 @@ export class DialogAddApplicationCredential extends LitElement {
--mdc-icon-size: 16px;
}
ha-markdown {
margin-bottom: 16px;
margin-top: var(--ha-space-4);
margin-bottom: var(--ha-space-4);
}
ha-fade-in {
display: flex;

View File

@@ -160,7 +160,7 @@ class DialogFloorDetail extends LitElement {
${!this._icon
? html`
<ha-floor-icon
slot="fallback"
slot="start"
.floor=${{ level: this._level }}
></ha-floor-icon>
`

View File

@@ -16,10 +16,8 @@ import { slugify } from "../../../common/string/slugify";
import { groupBy } from "../../../common/util/group-by";
import { afterNextRender } from "../../../common/util/render-status";
import "../../../components/ha-button";
import "../../../components/ha-button-menu";
import "../../../components/ha-card";
import "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import type { HaDropdownItem } from "../../../components/ha-dropdown-item";
import "../../../components/ha-icon-button";
import "../../../components/ha-icon-next";
import "../../../components/ha-list";
@@ -228,23 +226,32 @@ class HaConfigAreaPage extends LitElement {
></ha-icon>`
: nothing}${area.name}`}
>
<ha-dropdown slot="toolbar-icon" @wa-select=${this._handleMenuAction}>
<ha-button-menu slot="toolbar-icon">
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-dropdown-item value="settings" .entry=${area}>
<ha-svg-icon slot="icon" .path=${mdiPencil}></ha-svg-icon>
<ha-list-item
graphic="icon"
.entry=${area}
@click=${this._showSettings}
>
${this.hass.localize("ui.panel.config.areas.edit_settings")}
</ha-dropdown-item>
<ha-svg-icon slot="graphic" .path=${mdiPencil}> </ha-svg-icon>
</ha-list-item>
<ha-dropdown-item value="delete" variant="danger">
<ha-svg-icon slot="icon" .path=${mdiDelete}></ha-svg-icon>
<ha-list-item
class="warning"
graphic="icon"
@click=${this._deleteConfirm}
>
${this.hass.localize("ui.panel.config.areas.editor.delete")}
</ha-dropdown-item>
</ha-dropdown>
<ha-svg-icon class="warning" slot="graphic" .path=${mdiDelete}>
</ha-svg-icon>
</ha-list-item>
</ha-button-menu>
<div class="container">
<div class="column">
@@ -606,20 +613,6 @@ class HaConfigAreaPage extends LitElement {
this._related = await findRelated(this.hass, "area", this.areaId);
}
private _handleMenuAction(ev: CustomEvent) {
const item = ev.detail.item as HaDropdownItem & {
entry: AreaRegistryEntry;
};
switch (item.value) {
case "settings":
this._openDialog(item.entry);
break;
case "delete":
this._deleteConfirm();
break;
}
}
private _showSettings(ev: MouseEvent) {
const entry: AreaRegistryEntry = (ev.currentTarget! as any).entry;
this._openDialog(entry);

View File

@@ -1,4 +1,4 @@
import "@home-assistant/webawesome/dist/components/divider/divider";
import type { ActionDetail } from "@material/mwc-list";
import {
mdiDelete,
mdiDotsVertical,
@@ -24,12 +24,10 @@ import {
type AreasFloorHierarchy,
} from "../../../common/areas/areas-floor-hierarchy";
import { formatListWithAnds } from "../../../common/string/format-list";
import "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import type { HaDropdownItem } from "../../../components/ha-dropdown-item";
import "../../../components/ha-fab";
import "../../../components/ha-floor-icon";
import "../../../components/ha-icon-button";
import "../../../components/ha-list-item";
import "../../../components/ha-sortable";
import type { HaSortableOptions } from "../../../components/ha-sortable";
import "../../../components/ha-svg-icon";
@@ -198,43 +196,44 @@ export class HaConfigAreasDashboard extends LitElement {
${floor.name}
</h2>
<div class="actions">
<ha-dropdown
<ha-button-menu
.floor=${floor}
@wa-select=${this._handleFloorAction}
@action=${this._handleFloorAction}
>
<ha-icon-button
slot="trigger"
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-dropdown-item value="reorder">
<ha-svg-icon
slot="icon"
<ha-list-item graphic="icon"
><ha-svg-icon
.path=${mdiSort}
></ha-svg-icon>
${this.hass.localize(
slot="graphic"
></ha-svg-icon
>${this.hass.localize(
"ui.panel.config.areas.picker.reorder"
)}
</ha-dropdown-item>
<wa-divider></wa-divider>
<ha-dropdown-item value="edit">
<ha-svg-icon
slot="icon"
)}</ha-list-item
>
<li divider role="separator"></li>
<ha-list-item graphic="icon"
><ha-svg-icon
.path=${mdiPencil}
></ha-svg-icon>
${this.hass.localize(
slot="graphic"
></ha-svg-icon
>${this.hass.localize(
"ui.panel.config.areas.picker.floor.edit_floor"
)}
</ha-dropdown-item>
<ha-dropdown-item value="delete" variant="danger">
<ha-svg-icon
slot="icon"
)}</ha-list-item
>
<ha-list-item class="warning" graphic="icon"
><ha-svg-icon
class="warning"
.path=${mdiDelete}
></ha-svg-icon>
${this.hass.localize(
slot="graphic"
></ha-svg-icon
>${this.hass.localize(
"ui.panel.config.areas.picker.floor.delete_floor"
)}
</ha-dropdown-item>
</ha-dropdown>
)}</ha-list-item
>
</ha-button-menu>
</div>
</div>
<ha-sortable
@@ -274,23 +273,23 @@ export class HaConfigAreasDashboard extends LitElement {
)}
</h2>
<div class="actions">
<ha-dropdown
@wa-select=${this._handleUnassignedAreasAction}
<ha-button-menu
@action=${this._handleUnassignedAreasAction}
>
<ha-icon-button
slot="trigger"
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-dropdown-item value="reorder">
<ha-svg-icon
slot="icon"
<ha-list-item graphic="icon"
><ha-svg-icon
.path=${mdiSort}
></ha-svg-icon>
${this.hass.localize(
slot="graphic"
></ha-svg-icon
>${this.hass.localize(
"ui.panel.config.areas.picker.reorder"
)}
</ha-dropdown-item>
</ha-dropdown>
)}</ha-list-item
>
</ha-button-menu>
</div>
</div>
<ha-sortable
@@ -534,25 +533,23 @@ export class HaConfigAreasDashboard extends LitElement {
}, time);
}
private _handleFloorAction(ev: CustomEvent) {
const item = ev.detail.item as HaDropdownItem;
private _handleFloorAction(ev: CustomEvent<ActionDetail>) {
const floor = (ev.currentTarget as any).floor;
switch (item.value) {
case "reorder":
switch (ev.detail.index) {
case 0:
this._showReorderDialog();
break;
case "edit":
case 1:
this._editFloor(floor);
break;
case "delete":
case 2:
this._deleteFloor(floor);
break;
}
}
private _handleUnassignedAreasAction(ev: CustomEvent) {
const item = ev.detail.item as HaDropdownItem;
if (item.value === "reorder") {
private _handleUnassignedAreasAction(ev: CustomEvent<ActionDetail>) {
if (ev.detail.index === 0) {
this._showReorderDialog();
}
}

View File

@@ -148,7 +148,7 @@ class DialogAutomationSave extends LitElement implements HassDialog {
@value-changed=${this._iconChanged}
>
<ha-domain-icon
slot="fallback"
slot="start"
domain=${this._params.domain}
.hass=${this.hass}
>
@@ -176,8 +176,10 @@ class DialogAutomationSave extends LitElement implements HassDialog {
id="category"
.hass=${this.hass}
.scope=${this._params.domain}
.label=${this.hass.localize(
"ui.components.category-picker.category"
)}
.value=${this._entryUpdates.category}
show-label
@value-changed=${this._registryEntryChanged}
></ha-category-picker>`
: nothing}
@@ -194,7 +196,6 @@ class DialogAutomationSave extends LitElement implements HassDialog {
id="area"
.hass=${this.hass}
.value=${this._entryUpdates.area}
show-label
@value-changed=${this._registryEntryChanged}
></ha-area-picker>`
: nothing}

View File

@@ -40,7 +40,6 @@ export class HaZoneCondition extends LitElement {
@value-changed=${this._entityPicked}
.hass=${this.hass}
.disabled=${this.disabled}
allow-custom-entity
.entityFilter=${zoneAndLocationFilter}
></ha-entity-picker>
<ha-entity-picker
@@ -51,7 +50,6 @@ export class HaZoneCondition extends LitElement {
@value-changed=${this._zonePicked}
.hass=${this.hass}
.disabled=${this.disabled}
allow-custom-entity
.includeDomains=${includeDomains}
></ha-entity-picker>
`;

View File

@@ -1,7 +1,7 @@
import "@home-assistant/webawesome/dist/components/divider/divider";
import { ResizeController } from "@lit-labs/observers/resize-controller";
import { consume } from "@lit/context";
import {
mdiChevronRight,
mdiCog,
mdiContentDuplicate,
mdiDelete,
@@ -23,7 +23,7 @@ import { differenceInDays } from "date-fns";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { computeCssColor } from "../../../common/color/compute-color";
@@ -50,9 +50,6 @@ import type {
} from "../../../components/data-table/ha-data-table";
import "../../../components/data-table/ha-data-table-labels";
import "../../../components/entity/ha-entity-toggle";
import "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import type { HaDropdownItem } from "../../../components/ha-dropdown-item";
import "../../../components/ha-fab";
import "../../../components/ha-filter-blueprints";
import "../../../components/ha-filter-categories";
@@ -61,7 +58,12 @@ import "../../../components/ha-filter-entities";
import "../../../components/ha-filter-floor-areas";
import "../../../components/ha-filter-labels";
import "../../../components/ha-icon-button";
import "../../../components/ha-md-divider";
import "../../../components/ha-md-menu";
import type { HaMdMenu } from "../../../components/ha-md-menu";
import "../../../components/ha-md-menu-item";
import type { HaMdMenuItem } from "../../../components/ha-md-menu-item";
import "../../../components/ha-sub-menu";
import "../../../components/ha-svg-icon";
import "../../../components/ha-tooltip";
import { createAreaRegistryEntry } from "../../../data/area_registry";
@@ -173,6 +175,8 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
@consume({ context: fullEntitiesContext, subscribe: true })
_entityReg!: EntityRegistryEntry[];
@state() private _overflowAutomation?: AutomationItem;
@storage({ key: "automation-table-sort", state: false, subscribe: false })
private _activeSorting?: SortingChangedEvent;
@@ -200,6 +204,8 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
})
private _activeHiddenColumns?: string[];
@query("#overflow-menu") private _overflowMenu!: HaMdMenu;
private _sizeController = new ResizeController(this, {
callback: (entries) => entries[0]?.contentRect.width,
});
@@ -362,81 +368,12 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
moveable: false,
hideable: false,
template: (automation) => html`
<ha-dropdown
.automation=${automation}
@wa-select=${this._handleRowOverflowMenu}
>
<ha-icon-button
slot="trigger"
.automation=${automation}
.label=${this.hass.localize("ui.common.overflow_menu")}
.path=${mdiDotsVertical}
@click=${this._showOverflowMenu}
></ha-icon-button>
<ha-dropdown-item value="show-info">
<ha-svg-icon
slot="icon"
.path=${mdiInformationOutline}
></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.automation.editor.show_info"
)}
</ha-dropdown-item>
<ha-dropdown-item value="show-settings">
<ha-svg-icon slot="icon" .path=${mdiCog}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.automation.picker.show_settings"
)}
</ha-dropdown-item>
<ha-dropdown-item value="edit-category">
<ha-svg-icon slot="icon" .path=${mdiTag}></ha-svg-icon>
${this.hass.localize(
`ui.panel.config.automation.picker.${automation.category ? "edit_category" : "assign_category"}`
)}
</ha-dropdown-item>
<ha-dropdown-item value="run-actions">
<ha-svg-icon slot="icon" .path=${mdiPlay}></ha-svg-icon>
${this.hass.localize("ui.panel.config.automation.editor.run")}
</ha-dropdown-item>
<ha-dropdown-item value="show-trace">
<ha-svg-icon
slot="icon"
.path=${mdiTransitConnection}
></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.automation.editor.show_trace"
)}
</ha-dropdown-item>
<wa-divider></wa-divider>
<ha-dropdown-item value="duplicate">
<ha-svg-icon
slot="icon"
.path=${mdiContentDuplicate}
></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.automation.picker.duplicate"
)}
</ha-dropdown-item>
<ha-dropdown-item value="toggle">
<ha-svg-icon
slot="icon"
.path=${automation.state === "off"
? mdiToggleSwitch
: mdiToggleSwitchOffOutline}
></ha-svg-icon>
${automation.state === "off"
? this.hass.localize(
"ui.panel.config.automation.editor.enable"
)
: this.hass.localize(
"ui.panel.config.automation.editor.disable"
)}
</ha-dropdown-item>
<ha-dropdown-item value="delete" variant="danger">
<ha-svg-icon slot="icon" .path=${mdiDelete}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.automation.picker.delete"
)}
</ha-dropdown-item>
</ha-dropdown>
`,
},
};
@@ -444,38 +381,17 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
}
);
private _handleRowOverflowMenu = (ev: CustomEvent) => {
const item = ev.detail.item as HaDropdownItem;
const automation = (
(ev.currentTarget as HTMLElement).closest("ha-dropdown") as any
).automation;
switch (item.value) {
case "show-info":
this._showInfo(automation);
break;
case "show-settings":
this._showSettings(automation);
break;
case "edit-category":
this._editCategory(automation);
break;
case "run-actions":
this._runActions(automation);
break;
case "show-trace":
this._showTrace(automation);
break;
case "duplicate":
this._duplicate(automation);
break;
case "toggle":
this._toggle(automation);
break;
case "delete":
this._deleteConfirm(automation);
break;
private _showOverflowMenu = (ev) => {
if (
this._overflowMenu.open &&
ev.target === this._overflowMenu.anchorElement
) {
this._overflowMenu.close();
return;
}
this._overflowAutomation = ev.target.automation;
this._overflowMenu.anchorElement = ev.target;
this._overflowMenu.show();
};
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
@@ -493,38 +409,34 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
];
}
private _renderCategoryItems = (submenu = false) =>
html`${this._categories?.map(
protected render(): TemplateResult {
const categoryItems = html`${this._categories?.map(
(category) =>
html`<ha-dropdown-item
.slot=${submenu ? "submenu" : ""}
value="move_category"
data-category=${category.category_id}
html`<ha-md-menu-item
.value=${category.category_id}
.clickAction=${this._handleBulkCategory}
>
${category.icon
? html`<ha-icon slot="icon" .icon=${category.icon}></ha-icon>`
: html`<ha-svg-icon slot="icon" .path=${mdiTag}></ha-svg-icon>`}
${category.name}
</ha-dropdown-item>`
? html`<ha-icon slot="start" .icon=${category.icon}></ha-icon>`
: html`<ha-svg-icon slot="start" .path=${mdiTag}></ha-svg-icon>`}
<div slot="headline">${category.name}</div>
</ha-md-menu-item>`
)}
<ha-dropdown-item
.slot=${submenu ? "submenu" : ""}
value="__no_category__"
>
<ha-md-menu-item .value=${null} .clickAction=${this._handleBulkCategory}>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.no_category"
)}
</ha-dropdown-item>
<wa-divider .slot=${submenu ? "submenu" : ""}></wa-divider>
<ha-dropdown-item
.slot=${submenu ? "submenu" : ""}
value="__create_category__"
>
</div>
</ha-md-menu-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item .clickAction=${this._bulkCreateCategory}>
<div slot="headline">
${this.hass.localize("ui.panel.config.category.editor.add")}
</ha-dropdown-item>`;
</div>
</ha-md-menu-item>`;
private _renderLabelItems = (submenu = false) =>
html`${this._labels?.map((label) => {
const labelItems = html`${this._labels?.map((label) => {
const color = label.color ? computeCssColor(label.color) : undefined;
const selected = this._selected.every((entityId) =>
this.hass.entities[entityId]?.labels.includes(label.label_id)
@@ -534,14 +446,14 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
this._selected.some((entityId) =>
this.hass.entities[entityId]?.labels.includes(label.label_id)
);
return html`<ha-dropdown-item
.slot=${submenu ? "submenu" : ""}
return html`<ha-md-menu-item
.value=${label.label_id}
data-action=${selected ? "remove" : "add"}
.action=${selected ? "remove" : "add"}
@click=${this._handleBulkLabel}
keep-open
>
<ha-checkbox
slot="icon"
slot="start"
.checked=${selected}
.indeterminate=${partial}
reducedTouchTarget
@@ -555,50 +467,46 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
: nothing}
${label.name}
</ha-label>
</ha-dropdown-item>`;
</ha-md-menu-item>`;
})}
<wa-divider .slot=${submenu ? "submenu" : ""}></wa-divider>
<ha-dropdown-item
.slot=${submenu ? "submenu" : ""}
value="__create_label__"
@click=${this._bulkCreateLabel}
>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item .clickAction=${this._bulkCreateLabel}>
<div slot="headline">
${this.hass.localize("ui.panel.config.labels.add_label")}
</ha-dropdown-item>`;
</div></ha-md-menu-item
>`;
private _renderAreaItems = (submenu = false) =>
html`${Object.values(this.hass.areas).map(
const areaItems = html`${Object.values(this.hass.areas).map(
(area) =>
html`<ha-dropdown-item
.slot=${submenu ? "submenu" : ""}
value="move_area"
data-area=${area.area_id}
html`<ha-md-menu-item
.value=${area.area_id}
.clickAction=${this._handleBulkArea}
>
${area.icon
? html`<ha-icon slot="icon" .icon=${area.icon}></ha-icon>`
? html`<ha-icon slot="start" .icon=${area.icon}></ha-icon>`
: html`<ha-svg-icon
slot="icon"
slot="start"
.path=${mdiTextureBox}
></ha-svg-icon>`}
${area.name}
</ha-dropdown-item>`
<div slot="headline">${area.name}</div>
</ha-md-menu-item>`
)}
<ha-dropdown-item .slot=${submenu ? "submenu" : ""} value="__no_area__">
<ha-md-menu-item .value=${null} .clickAction=${this._handleBulkArea}>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.no_area"
)}
</ha-dropdown-item>
<wa-divider .slot=${submenu ? "submenu" : ""}></wa-divider>
<ha-dropdown-item
.slot=${submenu ? "submenu" : ""}
value="__create_area__"
>
</div>
</ha-md-menu-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item .clickAction=${this._bulkCreateArea}>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.add_area"
)}
</ha-dropdown-item>`;
</div>
</ha-md-menu-item>`;
protected render(): TemplateResult {
const areasInOverflow =
(this._sizeController.value && this._sizeController.value < 900) ||
(!this._sizeController.value && this.hass.dockedSidebar === "docked");
@@ -619,9 +527,9 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
<hass-tabs-subpage-data-table
.hass=${this.hass}
.narrow=${this.narrow}
.backPath=${this._searchParms.has("historyBack")
? undefined
: "/config"}
.backPath=${
this._searchParms.has("historyBack") ? undefined : "/config"
}
id="entity_id"
.route=${this.route}
.tabs=${configSections.automations}
@@ -633,14 +541,16 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
.selected=${this._selected.length}
@selection-changed=${this._handleSelectionChanged}
has-filters
.filters=${Object.values(this._filters).filter((filter) =>
.filters=${
Object.values(this._filters).filter((filter) =>
Array.isArray(filter.value)
? filter.value.length
: filter.value &&
Object.values(filter.value).some((val) =>
Array.isArray(val) ? val.length : val
)
).length}
).length
}
.columns=${this._columns(
this.narrow,
this.hass.localize,
@@ -733,11 +643,9 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
.narrow=${this.narrow}
@expanded-changed=${this._filterExpanded}
></ha-filter-blueprints>
${!this.narrow
? html`<ha-dropdown
slot="selection-bar"
@wa-select=${this._handleOverflowMenuSelect}
>
${
!this.narrow
? html`<ha-md-button-menu slot="selection-bar">
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
@@ -749,11 +657,11 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-assist-chip>
${this._renderCategoryItems()}
</ha-dropdown>
${categoryItems}
</ha-md-button-menu>
${labelsInOverflow
? nothing
: html`<ha-dropdown slot="selection-bar">
: html`<ha-md-button-menu slot="selection-bar">
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
@@ -765,14 +673,11 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-assist-chip>
${this._renderLabelItems()}
</ha-dropdown>`}
${labelItems}
</ha-md-button-menu>`}
${areasInOverflow
? nothing
: html`<ha-dropdown
slot="selection-bar"
@wa-select=${this._handleOverflowMenuSelect}
>
: html`<ha-md-button-menu slot="selection-bar">
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
@@ -784,15 +689,13 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-assist-chip>
${this._renderAreaItems()}
</ha-dropdown>`}`
: nothing}
<ha-dropdown
has-overflow
slot="selection-bar"
@wa-select=${this._handleOverflowMenuSelect}
>
${this.narrow
${areaItems}
</ha-md-button-menu>`}`
: nothing
}
<ha-md-button-menu has-overflow slot="selection-bar">
${
this.narrow
? html`<ha-assist-chip
.label=${this.hass.localize(
"ui.panel.config.automation.picker.bulk_action"
@@ -810,48 +713,89 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
"ui.panel.config.automation.picker.bulk_action"
)}
slot="trigger"
></ha-icon-button>`}
${this.narrow
? html`<ha-dropdown-item>
></ha-icon-button>`
}
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon
></ha-assist-chip>
${
this.narrow
? html`<ha-sub-menu>
<ha-md-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.move_category"
)}
${this._renderCategoryItems(true)}
</ha-dropdown-item>`
: nothing}
${this.narrow || labelsInOverflow
? html`<ha-dropdown-item>
</div>
<ha-svg-icon
slot="end"
.path=${mdiChevronRight}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu slot="menu">${categoryItems}</ha-md-menu>
</ha-sub-menu>`
: nothing
}
${
this.narrow || labelsInOverflow
? html`<ha-sub-menu>
<ha-md-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.add_label"
)}
${this._renderLabelItems(true)}
</ha-dropdown-item>`
: nothing}
${this.narrow || areasInOverflow
? html`<ha-dropdown-item>
</div>
<ha-svg-icon
slot="end"
.path=${mdiChevronRight}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu slot="menu">${labelItems}</ha-md-menu>
</ha-sub-menu>`
: nothing
}
${
this.narrow || areasInOverflow
? html`<ha-sub-menu>
<ha-md-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.move_area"
)}
${this._renderAreaItems(true)}
</ha-dropdown-item>`
: nothing}
<ha-dropdown-item value="enable">
<ha-svg-icon slot="icon" .path=${mdiToggleSwitch}></ha-svg-icon>
</div>
<ha-svg-icon
slot="end"
.path=${mdiChevronRight}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu slot="menu">${areaItems}</ha-md-menu>
</ha-sub-menu>`
: nothing
}
<ha-md-menu-item .clickAction=${this._handleBulkEnable}>
<ha-svg-icon slot="start" .path=${mdiToggleSwitch}></ha-svg-icon>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.enable"
)}
</ha-dropdown-item>
<ha-dropdown-item value="disable">
</div>
</ha-md-menu-item>
<ha-md-menu-item .clickAction=${this._handleBulkDisable}>
<ha-svg-icon
slot="icon"
slot="start"
.path=${mdiToggleSwitchOffOutline}
></ha-svg-icon>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.disable"
)}
</ha-dropdown-item>
</ha-dropdown>
${!this.automations.length
</div>
</ha-md-menu-item>
</ha-md-button-menu>
${
!this.automations.length
? html`<div class="empty" slot="empty">
<ha-svg-icon .path=${mdiRobotHappy}></ha-svg-icon>
<h1>
@@ -871,7 +815,10 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
)}
</p>
<ha-button
href=${documentationUrl(this.hass, "/docs/automation/editor/")}
href=${documentationUrl(
this.hass,
"/docs/automation/editor/"
)}
target="_blank"
appearance="plain"
rel="noreferrer"
@@ -881,7 +828,8 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
<ha-svg-icon slot="end" .path=${mdiOpenInNew}> </ha-svg-icon>
</ha-button>
</div>`
: nothing}
: nothing
}
<ha-fab
slot="fab"
.label=${this.hass.localize(
@@ -893,6 +841,80 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</ha-fab>
</hass-tabs-subpage-data-table>
<ha-md-menu id="overflow-menu" positioning="fixed">
<ha-md-menu-item .clickAction=${this._showInfo}>
<ha-svg-icon
.path=${mdiInformationOutline}
slot="start"
></ha-svg-icon>
<div slot="headline">
${this.hass.localize("ui.panel.config.automation.editor.show_info")}
</div>
</ha-md-menu-item>
<ha-md-menu-item .clickAction=${this._showSettings}>
<ha-svg-icon .path=${mdiCog} slot="start"></ha-svg-icon>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.picker.show_settings"
)}
</div>
</ha-md-menu-item>
<ha-md-menu-item .clickAction=${this._editCategory}>
<ha-svg-icon .path=${mdiTag} slot="start"></ha-svg-icon>
<div slot="headline">
${this.hass.localize(
`ui.panel.config.automation.picker.${this._overflowAutomation?.category ? "edit_category" : "assign_category"}`
)}
</div>
</ha-md-menu-item>
<ha-md-menu-item .clickAction=${this._runActions}>
<ha-svg-icon .path=${mdiPlay} slot="start"></ha-svg-icon>
<div slot="headline">
${this.hass.localize("ui.panel.config.automation.editor.run")}
</div>
</ha-md-menu-item>
<ha-md-menu-item .clickAction=${this._showTrace}>
<ha-svg-icon .path=${mdiTransitConnection} slot="start"></ha-svg-icon>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.editor.show_trace"
)}
</div>
</ha-md-menu-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item .clickAction=${this._duplicate}>
<ha-svg-icon .path=${mdiContentDuplicate} slot="start"></ha-svg-icon>
<div slot="headline">
${this.hass.localize("ui.panel.config.automation.picker.duplicate")}
</div>
</ha-md-menu-item>
<ha-md-menu-item .clickAction=${this._toggle}>
<ha-svg-icon
.path=${
this._overflowAutomation?.state === "off"
? mdiToggleSwitch
: mdiToggleSwitchOffOutline
}
slot="start"
></ha-svg-icon>
<div slot="headline">
${
this._overflowAutomation?.state === "off"
? this.hass.localize("ui.panel.config.automation.editor.enable")
: this.hass.localize(
"ui.panel.config.automation.editor.disable"
)
}
</div>
</ha-md-menu-item>
<ha-md-menu-item .clickAction=${this._deleteConfirm} class="warning">
<ha-svg-icon .path=${mdiDelete} slot="start"></ha-svg-icon>
<div slot="headline">
${this.hass.localize("ui.panel.config.automation.picker.delete")}
</div>
</ha-md-menu-item>
</ha-md-menu>
`;
}
@@ -1049,22 +1071,33 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
this._applyFilters();
}
private _showInfo = (automation: AutomationItem) => {
private _showInfo = (item: HaMdMenuItem) => {
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
.automation;
fireEvent(this, "hass-more-info", { entityId: automation.entity_id });
};
private _showSettings = (automation: AutomationItem) => {
private _showSettings = (item: HaMdMenuItem) => {
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
.automation;
fireEvent(this, "hass-more-info", {
entityId: automation.entity_id,
view: "settings",
});
};
private _runActions = (automation: AutomationItem) => {
private _runActions = (item: HaMdMenuItem) => {
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
.automation;
triggerAutomationActions(this.hass, automation.entity_id);
};
private _editCategory = (automation: AutomationItem) => {
private _editCategory = (item: HaMdMenuItem) => {
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
.automation;
const entityReg = this._entityReg.find(
(reg) => reg.entity_id === automation.entity_id
);
@@ -1085,7 +1118,10 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
});
};
private _showTrace = (automation: AutomationItem) => {
private _showTrace = (item: HaMdMenuItem) => {
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
.automation;
if (!automation.attributes.id) {
showAlertDialog(this, {
text: this.hass.localize(
@@ -1099,14 +1135,20 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
);
};
private _toggle = async (automation: AutomationItem): Promise<void> => {
private _toggle = async (item: HaMdMenuItem): Promise<void> => {
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
.automation;
const service = automation.state === "off" ? "turn_on" : "turn_off";
await this.hass.callService("automation", service, {
entity_id: automation.entity_id,
});
};
private _deleteConfirm = async (automation: AutomationItem) => {
private _deleteConfirm = async (item: HaMdMenuItem) => {
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
.automation;
showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.automation.picker.delete_confirm_title"
@@ -1122,11 +1164,8 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
});
};
private async _delete(automation: AutomationEntity) {
private async _delete(automation) {
try {
if (!automation.attributes.id) {
throw new Error("Automation ID is missing");
}
await deleteAutomation(this.hass, automation.attributes.id);
this._selected = this._selected.filter(
(entityId) => entityId !== automation.entity_id
@@ -1146,11 +1185,11 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
}
}
private _duplicate = async (automation: AutomationEntity) => {
private _duplicate = async (item: HaMdMenuItem) => {
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
.automation;
try {
if (!automation.attributes.id) {
throw new Error("Automation ID is missing");
}
const config = await fetchAutomationFileConfig(
this.hass,
automation.attributes.id
@@ -1222,6 +1261,11 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
}
}
private _handleBulkCategory = async (item) => {
const category = item.value;
this._bulkAddCategory(category);
};
private async _bulkAddCategory(category: string) {
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
this._selected.forEach((entityId) => {
@@ -1248,9 +1292,8 @@ ${rejected
}
private async _handleBulkLabel(ev) {
ev.stopPropagation();
const label = ev.currentTarget.value;
const action = ev.currentTarget.dataset.action;
const action = ev.currentTarget.action;
this._bulkLabel(label, action);
}
@@ -1284,6 +1327,11 @@ ${rejected
}
}
private _handleBulkArea = (item) => {
const area = item.value;
this._bulkAddArea(area);
};
private async _bulkAddArea(area: string) {
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
this._selected.forEach((entityId) => {
@@ -1319,40 +1367,6 @@ ${rejected
});
};
private _handleOverflowMenuSelect = (ev: CustomEvent) => {
const item = ev.detail.item as HaDropdownItem;
switch (item.value) {
case "enable":
this._handleBulkEnable();
break;
case "disable":
this._handleBulkDisable();
break;
case "__create_category__":
this._bulkCreateCategory();
break;
case "__no_category__":
this._bulkAddCategory("");
break;
case "move_category":
if (item.dataset.category) {
this._bulkAddCategory(item.dataset.category);
}
break;
case "__create_area__":
this._bulkCreateArea();
break;
case "__no_area__":
this._bulkAddArea("");
break;
case "move_area":
if (item.dataset.area) {
this._bulkAddArea(item.dataset.area);
}
break;
}
};
private _handleBulkEnable = async () => {
const promises: Promise<ServiceCallResponse>[] = [];
this._selected.forEach((entityId) => {
@@ -1463,7 +1477,7 @@ ${rejected
ha-assist-chip {
--ha-assist-chip-container-shape: 10px;
}
ha-dropdown ha-assist-chip {
ha-md-button-menu ha-assist-chip {
--md-assist-chip-trailing-space: 8px;
}
ha-label {

View File

@@ -43,7 +43,6 @@ export class HaZoneTrigger extends LitElement {
.disabled=${this.disabled}
@value-changed=${this._entityPicked}
.hass=${this.hass}
allow-custom-entity
.entityFilter=${zoneAndLocationFilter}
></ha-entity-picker>
<ha-entity-picker
@@ -54,7 +53,6 @@ export class HaZoneTrigger extends LitElement {
.disabled=${this.disabled}
@value-changed=${this._zonePicked}
.hass=${this.hass}
allow-custom-entity
.includeDomains=${includeDomains}
></ha-entity-picker>

View File

@@ -65,6 +65,9 @@ class DialogAssignCategory extends LitElement {
<ha-category-picker
.hass=${this.hass}
.scope=${this._scope}
.label=${this.hass.localize(
"ui.components.category-picker.category"
)}
.value=${this._category}
@value-changed=${this._categoryChanged}
></ha-category-picker>

View File

@@ -39,9 +39,6 @@ export class HaCategoryPicker extends SubscribeMixin(LitElement) {
@property({ type: Boolean, attribute: "no-add" })
public noAdd = false;
@property({ type: Boolean, attribute: "show-label" })
public showLabel = false;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = false;
@@ -183,10 +180,6 @@ export class HaCategoryPicker extends SubscribeMixin(LitElement) {
};
protected render(): TemplateResult {
const placeholder =
this.placeholder ??
this.hass.localize("ui.components.category-picker.category");
const valueRenderer = this._computeValueRenderer(this._categories);
return html`
@@ -194,13 +187,12 @@ export class HaCategoryPicker extends SubscribeMixin(LitElement) {
.hass=${this.hass}
.autofocus=${this.autofocus}
.label=${this.label}
.placeholder=${this.placeholder}
.value=${this.value}
.notFoundLabel=${this._notFoundLabel}
.emptyLabel=${this.hass.localize(
"ui.components.category-picker.no_categories"
)}
.placeholder=${placeholder}
.showLabel=${this.showLabel}
.value=${this.value}
.getItems=${this._getItems}
.getAdditionalItems=${this._getAdditionalItems}
.valueRenderer=${valueRenderer}

View File

@@ -6,10 +6,8 @@ import { fireEvent } from "../../../../common/dom/fire_event";
import { debounce } from "../../../../common/util/debounce";
import "../../../../components/ha-alert";
import "../../../../components/ha-button";
import "../../../../components/ha-button-menu";
import "../../../../components/ha-card";
import "../../../../components/ha-dropdown";
import "../../../../components/ha-dropdown-item";
import type { HaDropdownItem } from "../../../../components/ha-dropdown-item";
import "../../../../components/ha-list-item";
import "../../../../components/ha-tip";
import type {
@@ -55,26 +53,26 @@ export class CloudAccount extends SubscribeMixin(LitElement) {
.narrow=${this.narrow}
header="Home Assistant Cloud"
>
<ha-dropdown slot="toolbar-icon" @wa-select=${this._handleMenuAction}>
<ha-button-menu slot="toolbar-icon" @action=${this._handleMenuAction}>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-dropdown-item value="reset">
<ha-svg-icon slot="icon" .path=${mdiDeleteForever}></ha-svg-icon>
<ha-list-item graphic="icon">
${this.hass.localize(
"ui.panel.config.cloud.account.reset_cloud_data"
)}
</ha-dropdown-item>
<ha-dropdown-item value="download">
<ha-svg-icon slot="icon" .path=${mdiDownload}></ha-svg-icon>
<ha-svg-icon slot="graphic" .path=${mdiDeleteForever}></ha-svg-icon>
</ha-list-item>
<ha-list-item graphic="icon">
${this.hass.localize(
"ui.panel.config.cloud.account.download_support_package"
)}
</ha-dropdown-item>
</ha-dropdown>
<ha-svg-icon slot="graphic" .path=${mdiDownload}></ha-svg-icon>
</ha-list-item>
</ha-button-menu>
<div class="content">
<ha-config-section .isWide=${this.isWide}>
<span slot="header">Home Assistant Cloud</span>
@@ -299,15 +297,13 @@ export class CloudAccount extends SubscribeMixin(LitElement) {
fireEvent(this, "ha-refresh-cloud-status");
}
private _handleMenuAction(ev: CustomEvent) {
const item = ev.detail.item as HaDropdownItem;
switch (item.value) {
case "reset":
private _handleMenuAction(ev) {
switch (ev.detail.index) {
case 0:
this._deleteCloudData();
break;
case "download":
case 1:
this._downloadSupportPackage();
break;
}
}

View File

@@ -5,10 +5,8 @@ import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import { navigate } from "../../../../common/navigate";
import "../../../../components/ha-alert";
import "../../../../components/ha-button-menu";
import "../../../../components/ha-card";
import "../../../../components/ha-dropdown";
import "../../../../components/ha-dropdown-item";
import type { HaDropdownItem } from "../../../../components/ha-dropdown-item";
import "../../../../components/ha-icon-next";
import "../../../../components/ha-list";
import "../../../../components/ha-list-item";
@@ -46,26 +44,26 @@ export class CloudLoginPanel extends LitElement {
.narrow=${this.narrow}
header="Home Assistant Cloud"
>
<ha-dropdown slot="toolbar-icon" @wa-select=${this._handleMenuAction}>
<ha-button-menu slot="toolbar-icon" @action=${this._handleMenuAction}>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-dropdown-item value="reset">
<ha-svg-icon slot="icon" .path=${mdiDeleteForever}></ha-svg-icon>
<ha-list-item graphic="icon">
${this.hass.localize(
"ui.panel.config.cloud.account.reset_cloud_data"
)}
</ha-dropdown-item>
<ha-dropdown-item value="download">
<ha-svg-icon slot="icon" .path=${mdiDownload}></ha-svg-icon>
<ha-svg-icon slot="graphic" .path=${mdiDeleteForever}></ha-svg-icon>
</ha-list-item>
<ha-list-item graphic="icon">
${this.hass.localize(
"ui.panel.config.cloud.account.download_support_package"
)}
</ha-dropdown-item>
</ha-dropdown>
<ha-svg-icon slot="graphic" .path=${mdiDownload}></ha-svg-icon>
</ha-list-item>
</ha-button-menu>
<div class="content">
<ha-config-section .isWide=${this.isWide}>
<span slot="header">Home Assistant Cloud</span>
@@ -166,15 +164,13 @@ export class CloudLoginPanel extends LitElement {
fireEvent(this, "flash-message-changed", { value: "" });
}
private _handleMenuAction(ev: CustomEvent) {
const item = ev.detail.item as HaDropdownItem;
switch (item.value) {
case "reset":
private _handleMenuAction(ev) {
switch (ev.detail.index) {
case 0:
this._deleteCloudData();
break;
case "download":
case 1:
this._downloadSupportPackage();
break;
}
}

View File

@@ -1,4 +1,4 @@
import "@home-assistant/webawesome/dist/components/divider/divider";
import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item";
import { mdiDotsVertical, mdiRefresh } from "@mdi/js";
import type { HassEntities } from "home-assistant-js-websocket";
import type { TemplateResult } from "lit";
@@ -6,12 +6,13 @@ import { LitElement, css, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event";
import "../../../components/ha-alert";
import "../../../components/ha-bar";
import "../../../components/ha-button-menu";
import "../../../components/ha-card";
import "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import type { HaDropdownItem } from "../../../components/ha-dropdown-item";
import "../../../components/ha-check-list-item";
import "../../../components/ha-list-item";
import "../../../components/ha-metric";
import { extractApiErrorMessage } from "../../../data/hassio/common";
import type {
@@ -72,24 +73,24 @@ class HaConfigSectionUpdates extends LitElement {
.path=${mdiRefresh}
@click=${this._checkUpdates}
></ha-icon-button>
<ha-dropdown @wa-select=${this._handleMenuAction}>
<ha-button-menu multi>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-dropdown-item
type="checkbox"
.checked=${this._showSkipped}
value="toggle_skipped"
<ha-check-list-item
left
@request-selected=${this._toggleSkipped}
.selected=${this._showSkipped}
>
${this.hass.localize("ui.panel.config.updates.show_skipped")}
</ha-dropdown-item>
</ha-check-list-item>
${this._supervisorInfo
? html`
<wa-divider></wa-divider>
<ha-dropdown-item
value="toggle_beta"
<li divider role="separator"></li>
<ha-list-item
@request-selected=${this._toggleBeta}
.disabled=${this._supervisorInfo.channel === "dev"}
>
${this._supervisorInfo.channel === "stable"
@@ -97,10 +98,10 @@ class HaConfigSectionUpdates extends LitElement {
: this.hass.localize(
"ui.panel.config.updates.leave_beta"
)}
</ha-dropdown-item>
</ha-list-item>
`
: ""}
</ha-dropdown>
</ha-button-menu>
</div>
<div class="content">
<ha-card outlined>
@@ -132,13 +133,21 @@ class HaConfigSectionUpdates extends LitElement {
this._supervisorInfo = await fetchHassioSupervisorInfo(this.hass);
}
private _handleMenuAction(ev: CustomEvent): void {
const item = ev.detail.item as HaDropdownItem;
switch (item.value) {
case "toggle_skipped":
private _toggleSkipped(ev: CustomEvent<RequestSelectedDetail>): void {
if (ev.detail.source !== "property") {
return;
}
this._showSkipped = !this._showSkipped;
break;
case "toggle_beta":
}
private async _toggleBeta(
ev: CustomEvent<RequestSelectedDetail>
): Promise<void> {
if (!shouldHandleRequestSelectedEvent(ev)) {
return;
}
if (this._supervisorInfo!.channel === "stable") {
showJoinBetaDialog(this, {
join: async () => this._setChannel("beta"),
@@ -146,8 +155,6 @@ class HaConfigSectionUpdates extends LitElement {
} else {
this._setChannel("stable");
}
break;
}
}
private async _setChannel(

View File

@@ -1,3 +1,4 @@
import type { ActionDetail } from "@material/mwc-list";
import {
mdiCloudLock,
mdiDotsVertical,
@@ -12,12 +13,11 @@ import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import "../../../components/chips/ha-assist-chip";
import "../../../components/ha-button-menu";
import "../../../components/ha-card";
import "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import type { HaDropdownItem } from "../../../components/ha-dropdown-item";
import "../../../components/ha-icon-button";
import "../../../components/ha-icon-next";
import "../../../components/ha-list-item";
import "../../../components/ha-menu-button";
import "../../../components/ha-svg-icon";
import "../../../components/ha-tip";
@@ -226,25 +226,25 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
.path=${mdiMagnify}
@click=${this._showQuickBar}
></ha-icon-button>
<ha-dropdown slot="actionItems" @wa-select=${this._handleMenuAction}>
<ha-button-menu slot="actionItems" @action=${this._handleMenuAction}>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-dropdown-item value="check_updates">
<ha-svg-icon slot="icon" .path=${mdiRefresh}></ha-svg-icon>
<ha-list-item graphic="icon">
${this.hass.localize("ui.panel.config.updates.check_updates")}
</ha-dropdown-item>
<ha-svg-icon slot="graphic" .path=${mdiRefresh}></ha-svg-icon>
</ha-list-item>
<ha-dropdown-item value="restart">
<ha-svg-icon slot="icon" .path=${mdiPower}></ha-svg-icon>
<ha-list-item graphic="icon">
${this.hass.localize(
"ui.panel.config.system_dashboard.restart_homeassistant"
)}
</ha-dropdown-item>
</ha-dropdown>
<ha-svg-icon slot="graphic" .path=${mdiPower}></ha-svg-icon>
</ha-list-item>
</ha-button-menu>
<ha-config-section
.narrow=${this.narrow}
@@ -371,13 +371,12 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
});
}
private async _handleMenuAction(ev: CustomEvent) {
const item = ev.detail.item as HaDropdownItem;
switch (item.value) {
case "check_updates":
private async _handleMenuAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
checkForEntityUpdates(this, this.hass);
break;
case "restart":
case 1:
showRestartDialog(this);
break;
}

View File

@@ -80,7 +80,6 @@ class DialogDeviceRegistryDetail extends LitElement {
<ha-area-picker
.hass=${this.hass}
.value=${this._areaId}
show-label
@value-changed=${this._areaPicked}
></ha-area-picker>
<ha-labels-picker

View File

@@ -1,6 +1,7 @@
import { consume } from "@lit/context";
import {
mdiCancel,
mdiChevronRight,
mdiDelete,
mdiDotsVertical,
mdiMenuDown,
@@ -38,15 +39,11 @@ import type {
SortingChangedEvent,
} from "../../../components/data-table/ha-data-table";
import "@home-assistant/webawesome/dist/components/divider/divider";
import "../../../components/data-table/ha-data-table-labels";
import "../../../components/entity/ha-battery-icon";
import "../../../components/ha-alert";
import "../../../components/ha-button-menu";
import "../../../components/ha-check-list-item";
import "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import type { HaDropdownItem } from "../../../components/ha-dropdown-item";
import "../../../components/ha-fab";
import "../../../components/ha-filter-devices";
import "../../../components/ha-filter-floor-areas";
@@ -54,6 +51,9 @@ import "../../../components/ha-filter-integrations";
import "../../../components/ha-filter-labels";
import "../../../components/ha-filter-states";
import "../../../components/ha-icon-button";
import "../../../components/ha-md-divider";
import "../../../components/ha-md-menu-item";
import "../../../components/ha-sub-menu";
import { createAreaRegistryEntry } from "../../../data/area_registry";
import type { ConfigEntry, SubEntry } from "../../../data/config_entries";
import { getSubEntries, sortConfigEntries } from "../../../data/config_entries";
@@ -702,80 +702,6 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
];
}
private _renderAreaItems = (submenu = false) =>
html`${Object.values(this.hass.areas).map(
(area) =>
html`<ha-dropdown-item
.slot=${submenu ? "submenu" : ""}
value="toggle_area"
data-area=${area.area_id}
>
${area.icon
? html`<ha-icon slot="icon" .icon=${area.icon}></ha-icon>`
: html`<ha-svg-icon
slot="icon"
.path=${mdiTextureBox}
></ha-svg-icon>`}
${area.name}
</ha-dropdown-item>`
)}
<ha-dropdown-item .slot=${submenu ? "submenu" : ""} value="__no_area__">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.no_area"
)}
</ha-dropdown-item>
<wa-divider .slot=${submenu ? "submenu" : ""}></wa-divider>
<ha-dropdown-item
.slot=${submenu ? "submenu" : ""}
value="__create_area__"
>
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.add_area"
)}
</ha-dropdown-item>`;
private _renderLabelItems = (submenu = false) =>
html`${this._labels?.map((label) => {
const color = label.color ? computeCssColor(label.color) : undefined;
const selected = this._selected.every((deviceId) =>
this.hass.devices[deviceId]?.labels.includes(label.label_id)
);
const partial =
!selected &&
this._selected.some((deviceId) =>
this.hass.devices[deviceId]?.labels.includes(label.label_id)
);
return html`<ha-dropdown-item
.slot=${submenu ? "submenu" : ""}
.value=${label.label_id}
data-action=${selected ? "remove" : "add"}
@click=${this._handleBulkLabel}
>
<ha-checkbox
slot="icon"
.checked=${selected}
.indeterminate=${partial}
reducedTouchTarget
></ha-checkbox>
<ha-label
style=${color ? `--color: ${color}` : ""}
.description=${label.description}
>
${label.icon
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
: nothing}
${label.name}
</ha-label>
</ha-dropdown-item>`;
})}
<wa-divider .slot=${submenu ? "submenu" : ""}></wa-divider>
<ha-dropdown-item
@click=${this._bulkCreateLabel}
.slot=${submenu ? "submenu" : ""}
>
${this.hass.localize("ui.panel.config.labels.add_label")}
</ha-dropdown-item>`;
protected render(): TemplateResult {
const { devicesOutput } = this._devicesAndFilterDomains(
this.hass.devices,
@@ -792,6 +718,77 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
(this._sizeController.value && this._sizeController.value < 700) ||
(!this._sizeController.value && this.hass.dockedSidebar === "docked");
const areaItems = html`${Object.values(this.hass.areas).map(
(area) =>
html`<ha-md-menu-item
.value=${area.area_id}
.clickAction=${this._handleBulkArea}
>
${area.icon
? html`<ha-icon slot="start" .icon=${area.icon}></ha-icon>`
: html`<ha-svg-icon
slot="start"
.path=${mdiTextureBox}
></ha-svg-icon>`}
<div slot="headline">${area.name}</div>
</ha-md-menu-item>`
)}
<ha-md-menu-item .value=${null} .clickAction=${this._handleBulkArea}>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.no_area"
)}
</div>
</ha-md-menu-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item .clickAction=${this._bulkCreateArea}>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.add_area"
)}
</div>
</ha-md-menu-item>`;
const labelItems = html`${this._labels?.map((label) => {
const color = label.color ? computeCssColor(label.color) : undefined;
const selected = this._selected.every((deviceId) =>
this.hass.devices[deviceId]?.labels.includes(label.label_id)
);
const partial =
!selected &&
this._selected.some((deviceId) =>
this.hass.devices[deviceId]?.labels.includes(label.label_id)
);
return html`<ha-md-menu-item
.value=${label.label_id}
.action=${selected ? "remove" : "add"}
@click=${this._handleBulkLabel}
keep-open
>
<ha-checkbox
slot="start"
.checked=${selected}
.indeterminate=${partial}
reducedTouchTarget
></ha-checkbox>
<ha-label
style=${color ? `--color: ${color}` : ""}
.description=${label.description}
>
${label.icon
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
: nothing}
${label.name}
</ha-label>
</ha-md-menu-item>`;
})}
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item .clickAction=${this._bulkCreateLabel}>
<div slot="headline">
${this.hass.localize("ui.panel.config.labels.add_label")}
</div></ha-md-menu-item
>`;
return html`
<hass-tabs-subpage-data-table
.hass=${this.hass}
@@ -909,7 +906,7 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
></ha-filter-labels>
${!this.narrow
? html`<ha-dropdown slot="selection-bar">
? html`<ha-md-button-menu slot="selection-bar">
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
@@ -921,15 +918,12 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-assist-chip>
${this._renderLabelItems()}
</ha-dropdown>
${labelItems}
</ha-md-button-menu>
${areasInOverflow
? nothing
: html`<ha-dropdown
slot="selection-bar"
@wa-select=${this._handleOverflowMenuSelect}
>
: html`<ha-md-button-menu slot="selection-bar">
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
@@ -941,14 +935,10 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-assist-chip>
${this._renderAreaItems()}
</ha-dropdown>`}`
${areaItems}
</ha-md-button-menu>`}`
: nothing}
<ha-dropdown
has-overflow
slot="selection-bar"
@wa-select=${this._handleOverflowMenuSelect}
>
<ha-md-button-menu has-overflow slot="selection-bar">
${this.narrow
? html`<ha-assist-chip
.label=${this.hass.localize(
@@ -969,32 +959,51 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
slot="trigger"
></ha-icon-button>`}
${this.narrow
? html`<ha-dropdown-item>
? html` <ha-sub-menu>
<ha-md-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.add_label"
)}
${this._renderLabelItems(true)}
</ha-dropdown-item>`
</div>
<ha-svg-icon
slot="end"
.path=${mdiChevronRight}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu slot="menu">${labelItems}</ha-md-menu>
</ha-sub-menu>`
: nothing}
${areasInOverflow
? html`<ha-dropdown-item>
? html`<ha-sub-menu>
<ha-md-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.move_area"
)}
${this._renderAreaItems(true)}
</ha-dropdown-item>`
</div>
<ha-svg-icon
slot="end"
.path=${mdiChevronRight}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu slot="menu">${areaItems}</ha-md-menu>
</ha-sub-menu>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>`
: nothing}
<ha-dropdown-item
value="delete"
<ha-md-menu-item
.clickAction=${this._deleteSelected}
.disabled=${!this._selectedCanDelete.length}
variant="danger"
class="warning"
>
<ha-svg-icon slot="icon" .path=${mdiDelete}></ha-svg-icon>
<ha-svg-icon slot="start" .path=${mdiDelete}></ha-svg-icon>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.delete_selected.button"
)}
</ha-dropdown-item>
</ha-dropdown>
</div>
</ha-md-menu-item>
</ha-md-button-menu>
</hass-tabs-subpage-data-table>
`;
}
@@ -1084,28 +1093,12 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
this._selected = ev.detail.value;
}
private _handleOverflowMenuSelect(ev: CustomEvent) {
const item = ev.detail.item as HaDropdownItem;
private _handleBulkArea = (item) => {
const area = item.value;
this._bulkAddArea(area);
};
switch (item.value) {
case "delete":
this._deleteSelected();
break;
case "__no_area__":
this._bulkAddArea(null);
break;
case "__create_area__":
this._bulkCreateArea();
break;
case "toggle_area":
if (item.dataset.area) {
this._bulkAddArea(item.dataset.area);
}
break;
}
}
private async _bulkAddArea(area: string | null) {
private async _bulkAddArea(area: string) {
const promises: Promise<DeviceRegistryEntry>[] = [];
this._selected.forEach((deviceId) => {
promises.push(
@@ -1140,6 +1133,12 @@ ${rejected
});
};
private async _handleBulkLabel(ev) {
const label = ev.currentTarget.value;
const action = ev.currentTarget.action;
this._bulkLabel(label, action);
}
private async _bulkLabel(label: string, action: "add" | "remove") {
const promises: Promise<DeviceRegistryEntry>[] = [];
this._selected.forEach((deviceId) => {
@@ -1179,13 +1178,6 @@ ${rejected
});
};
private async _handleBulkLabel(ev) {
ev.stopPropagation();
const label = ev.currentTarget.value;
const action = ev.currentTarget.dataset.action;
this._bulkLabel(label, action);
}
private _deleteSelected = () => {
showConfirmationDialog(this, {
title: this.hass.localize(
@@ -1285,7 +1277,7 @@ ${rejected
ha-assist-chip {
--ha-assist-chip-container-shape: 10px;
}
ha-dropdown ha-assist-chip {
ha-md-button-menu ha-assist-chip {
--md-assist-chip-trailing-space: 8px;
}
ha-label {

View File

@@ -403,7 +403,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
${!this._icon && !stateObj?.attributes.icon && stateObj
? html`
<ha-state-icon
slot="fallback"
slot="start"
.hass=${this.hass}
.stateObj=${stateObj}
></ha-state-icon>
@@ -778,7 +778,6 @@ export class EntityRegistrySettingsEditor extends LitElement {
.hass=${this.hass}
.value=${this._areaId}
.disabled=${this.disabled}
show-label
@value-changed=${this._areaPicked}
></ha-area-picker>`
: ""}
@@ -1013,7 +1012,6 @@ export class EntityRegistrySettingsEditor extends LitElement {
? html`<ha-area-picker
.hass=${this.hass}
.value=${this._areaId}
show-label
.disabled=${this.disabled}
@value-changed=${this._areaPicked}
></ha-area-picker>`
@@ -1545,6 +1543,12 @@ export class EntityRegistrySettingsEditor extends LitElement {
margin-inline-end: 0;
margin-inline-start: initial;
}
ha-settings-row {
display: grid;
grid-template-columns: 1fr auto;
gap: var(--ha-space-4);
align-items: start;
}
ha-textfield,
ha-icon-picker,
ha-select,

View File

@@ -1,8 +1,8 @@
import "@home-assistant/webawesome/dist/components/divider/divider";
import { consume } from "@lit/context";
import {
mdiAlertCircle,
mdiCancel,
mdiChevronRight,
mdiDelete,
mdiDotsVertical,
mdiEye,
@@ -56,9 +56,8 @@ import type {
} from "../../../components/data-table/ha-data-table";
import "../../../components/data-table/ha-data-table-labels";
import "../../../components/ha-alert";
import "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import type { HaDropdownItem } from "../../../components/ha-dropdown-item";
import "../../../components/ha-button-menu";
import "../../../components/ha-check-list-item";
import "../../../components/ha-filter-devices";
import "../../../components/ha-filter-domains";
import "../../../components/ha-filter-floor-areas";
@@ -67,6 +66,9 @@ import "../../../components/ha-filter-labels";
import "../../../components/ha-filter-states";
import "../../../components/ha-icon";
import "../../../components/ha-icon-button";
import "../../../components/ha-md-divider";
import "../../../components/ha-md-menu-item";
import "../../../components/ha-sub-menu";
import "../../../components/ha-svg-icon";
import "../../../components/ha-tooltip";
import type { ConfigEntry, SubEntry } from "../../../data/config_entries";
@@ -747,48 +749,6 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
];
}
private _renderLabelItems = (submenu = false) =>
html` ${this._labels?.map((label) => {
const color = label.color ? computeCssColor(label.color) : undefined;
const selected = this._selected.every((entityId) =>
this.hass.entities[entityId]?.labels.includes(label.label_id)
);
const partial =
!selected &&
this._selected.some((entityId) =>
this.hass.entities[entityId]?.labels.includes(label.label_id)
);
return html`<ha-dropdown-item
.slot=${submenu ? "submenu" : ""}
.value=${label.label_id}
data-action=${selected ? "remove" : "add"}
@click=${this._handleLabelMenuSelect}
>
<ha-checkbox
slot="icon"
.checked=${selected}
.indeterminate=${partial}
reducedTouchTarget
></ha-checkbox>
<ha-label
style=${color ? `--color: ${color}` : ""}
.description=${label.description}
>
${label.icon
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
: nothing}
${label.name}
</ha-label>
</ha-dropdown-item>`;
})}
<wa-divider .slot=${submenu ? "submenu" : ""}></wa-divider>
<ha-dropdown-item
@click=${this._handleCreateLabel}
.slot=${submenu ? "submenu" : ""}
>
${this.hass.localize("ui.panel.config.labels.add_label")}
</ha-dropdown-item>`;
protected render() {
if (!this.hass || this._entities === undefined) {
return html` <hass-loading-screen></hass-loading-screen> `;
@@ -813,13 +773,53 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
[...filteredDomains][0]
);
const labelItems = html` ${this._labels?.map((label) => {
const color = label.color ? computeCssColor(label.color) : undefined;
const selected = this._selected.every((entityId) =>
this.hass.entities[entityId]?.labels.includes(label.label_id)
);
const partial =
!selected &&
this._selected.some((entityId) =>
this.hass.entities[entityId]?.labels.includes(label.label_id)
);
return html`<ha-md-menu-item
.value=${label.label_id}
.action=${selected ? "remove" : "add"}
@click=${this._handleBulkLabel}
keep-open
>
<ha-checkbox
slot="start"
.checked=${selected}
.indeterminate=${partial}
reducedTouchTarget
></ha-checkbox>
<ha-label
style=${color ? `--color: ${color}` : ""}
.description=${label.description}
>
${label.icon
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
: nothing}
${label.name}
</ha-label>
</ha-md-menu-item>`;
})}
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item .clickAction=${this._bulkCreateLabel}>
<div slot="headline">
${this.hass.localize("ui.panel.config.labels.add_label")}
</div></ha-md-menu-item
>`;
return html`
<hass-tabs-subpage-data-table
.hass=${this.hass}
.narrow=${this.narrow}
.backPath=${this._searchParms.has("historyBack")
? undefined
: "/config"}
.backPath=${
this._searchParms.has("historyBack") ? undefined : "/config"
}
.route=${this.route}
.tabs=${configSections.devices}
.columns=${this._columns(this.hass.localize)}
@@ -829,14 +829,16 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
{ number: filteredEntities.length }
)}
has-filters
.filters=${Object.values(this._filters).filter((filter) =>
.filters=${
Object.values(this._filters).filter((filter) =>
Array.isArray(filter)
? filter.length
: filter &&
Object.values(filter).some((val) =>
Array.isArray(val) ? val.length : val
)
).length}
).length
}
selectable
.selected=${this._selected.length}
.initialGroupColumn=${this._activeGrouping ?? "device_full"}
@@ -863,37 +865,32 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
slot="toolbar-icon"
></ha-integration-overflow-menu>
${!this.narrow
? html`<ha-dropdown slot="selection-bar">
${
!this.narrow
? html`<ha-md-button-menu slot="selection-bar">
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.add_label"
)}
>
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon>
<ha-svg-icon slot="trailing-icon" .path=${mdiMenuDown}></ha-svg-icon>
</ha-assist-chip>
${this._renderLabelItems()}
</ha-dropdown>`
: nothing}
<ha-dropdown
slot="selection-bar"
@wa-select=${this._handleOverflowMenuSelect}
>
${this.narrow
${labelItems}
</ha-md-button-menu>`
: nothing
}
<ha-md-button-menu has-overflow slot="selection-bar">
${
this.narrow
? html`<ha-assist-chip
.label=${this.hass.localize(
"ui.panel.config.automation.picker.bulk_action"
)}
slot="trigger"
>
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon>
<ha-svg-icon slot="trailing-icon" .path=${mdiMenuDown}></ha-svg-icon>
</ha-assist-chip>`
: html`<ha-icon-button
.path=${mdiDotsVertical}
@@ -901,65 +898,105 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
"ui.panel.config.automation.picker.bulk_action"
)}
slot="trigger"
></ha-icon-button>`}
${this.narrow
? html`<ha-dropdown-item>
></ha-icon-button>`
}
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon
></ha-assist-chip>
${
this.narrow
? html`<ha-sub-menu>
<ha-md-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.add_label"
)}
${this._renderLabelItems(true)}
</ha-dropdown-item>`
: nothing}
</div>
<ha-svg-icon slot="end" .path=${mdiChevronRight}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu slot="menu">${labelItems}</ha-md-menu>
</ha-sub-menu>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>`
: nothing
}
<ha-dropdown-item value="enable">
<ha-svg-icon slot="icon" .path=${mdiToggleSwitch}></ha-svg-icon>
<ha-md-menu-item .clickAction=${this._enableSelected}>
<ha-svg-icon slot="start" .path=${mdiToggleSwitch}></ha-svg-icon>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.entities.picker.enable_selected.button"
)}
</ha-dropdown-item>
<ha-dropdown-item value="disable">
</div>
</ha-md-menu-item>
<ha-md-menu-item .clickAction=${this._disableSelected}>
<ha-svg-icon
slot="icon"
slot="start"
.path=${mdiToggleSwitchOffOutline}
></ha-svg-icon>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.entities.picker.disable_selected.button"
)}
</ha-dropdown-item>
<wa-divider></wa-divider>
</div>
</ha-md-menu-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-dropdown-item value="unhide">
<ha-svg-icon slot="icon" .path=${mdiEye}></ha-svg-icon>
<ha-md-menu-item .clickAction=${this._unhideSelected}>
<ha-svg-icon
slot="start"
.path=${mdiEye}
></ha-svg-icon>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.entities.picker.unhide_selected.button"
)}
</ha-dropdown-item>
<ha-dropdown-item value="hide">
<ha-svg-icon slot="icon" .path=${mdiEyeOff}></ha-svg-icon>
</div>
</ha-md-menu-item>
<ha-md-menu-item .clickAction=${this._hideSelected}>
<ha-svg-icon
slot="start"
.path=${mdiEyeOff}
></ha-svg-icon>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.entities.picker.hide_selected.button"
)}
</ha-dropdown-item>
</div>
</ha-md-menu-item>
<wa-divider></wa-divider>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-dropdown-item value="restore_entity_id">
<ha-svg-icon slot="icon" .path=${mdiRestore}></ha-svg-icon>
<ha-md-menu-item .clickAction=${this._restoreEntityIdSelected}>
<ha-svg-icon
slot="start"
.path=${mdiRestore}
></ha-svg-icon>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.entities.picker.restore_entity_id_selected.button"
)}
</ha-dropdown-item>
</div>
</ha-md-menu-item>
<wa-divider></wa-divider>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-dropdown-item value="remove" variant="danger">
<ha-svg-icon slot="icon" .path=${mdiDelete}></ha-svg-icon>
<ha-md-menu-item .clickAction=${this._removeSelected} class="warning">
<ha-svg-icon
slot="start"
.path=${mdiDelete}
></ha-svg-icon>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.entities.picker.delete_selected.button"
)}
</ha-dropdown-item>
</ha-dropdown>
${Array.isArray(this._filters.config_entry) &&
</div>
</ha-md-menu-item>
</ha-md-button-menu>
${
Array.isArray(this._filters.config_entry) &&
this._filters.config_entry.length
? html`<ha-alert slot="filter-pane">
${this.hass.localize(
@@ -972,11 +1009,13 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
Array.isArray(this._filters.sub_entry) &&
this._filters.sub_entry.length
? html` (${this._subEntries?.find(
(entry) => entry.subentry_id === this._filters.sub_entry![0]
(entry) =>
entry.subentry_id === this._filters.sub_entry![0]
)?.title || this._filters.sub_entry[0]})`
: nothing}
</ha-alert>`
: nothing}
: nothing
}
<ha-filter-floor-areas
.hass=${this.hass}
type="entity"
@@ -1037,16 +1076,20 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
.narrow=${this.narrow}
@expanded-changed=${this._filterExpanded}
></ha-filter-labels>
${includeAddDeviceFab
${
includeAddDeviceFab
? html`<ha-fab
.label=${this.hass.localize("ui.panel.config.devices.add_device")}
.label=${this.hass.localize(
"ui.panel.config.devices.add_device"
)}
extended
@click=${this._addDevice}
slot="fab"
>
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</ha-fab>`
: nothing}
: nothing
}
</hass-tabs-subpage-data-table>
`;
}
@@ -1180,44 +1223,6 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
this._selected = ev.detail.value;
}
private _handleLabelMenuSelect(ev: CustomEvent) {
ev.stopPropagation();
const item = ev.currentTarget as HaDropdownItem;
const label = item.value as string;
const action = (item as HTMLElement).dataset.action as "add" | "remove";
this._bulkLabel(label, action);
}
private _handleCreateLabel() {
this._bulkCreateLabel();
}
private _handleOverflowMenuSelect(ev: CustomEvent) {
const item = ev.detail.item as HaDropdownItem;
switch (item.value) {
case "enable":
this._enableSelected();
break;
case "disable":
this._disableSelected();
break;
case "unhide":
this._unhideSelected();
break;
case "hide":
this._hideSelected();
break;
case "remove":
this._removeSelected();
break;
case "restore_entity_id":
this._restoreEntityIdSelected();
break;
}
}
private _enableSelected = async () => {
showConfirmationDialog(this, {
title: this.hass.localize(
@@ -1341,6 +1346,12 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
this._clearSelection();
};
private async _handleBulkLabel(ev) {
const label = ev.currentTarget.value;
const action = ev.currentTarget.action;
await this._bulkLabel(label, action);
}
private async _bulkLabel(label: string, action: "add" | "remove") {
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
this._selected.forEach((entityId) => {
@@ -1602,7 +1613,7 @@ ${rejected
ha-assist-chip {
--ha-assist-chip-container-shape: 10px;
}
ha-dropdown ha-assist-chip {
ha-md-button-menu ha-assist-chip {
--md-assist-chip-trailing-space: 8px;
}
ha-label {

View File

@@ -1,9 +1,9 @@
import "@home-assistant/webawesome/dist/components/divider/divider";
import { ResizeController } from "@lit-labs/observers/resize-controller";
import { consume } from "@lit/context";
import {
mdiAlertCircle,
mdiCancel,
mdiChevronRight,
mdiCog,
mdiDelete,
mdiDotsVertical,
@@ -45,9 +45,6 @@ import type {
SortingChangedEvent,
} from "../../../components/data-table/ha-data-table";
import "../../../components/data-table/ha-data-table-labels";
import "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import type { HaDropdownItem } from "../../../components/ha-dropdown-item";
import "../../../components/ha-fab";
import "../../../components/ha-filter-categories";
import "../../../components/ha-filter-devices";
@@ -56,9 +53,8 @@ import "../../../components/ha-filter-floor-areas";
import "../../../components/ha-filter-labels";
import "../../../components/ha-icon";
import "../../../components/ha-icon-overflow-menu";
import "../../../components/ha-md-menu";
import "../../../components/ha-md-divider";
import "../../../components/ha-state-icon";
import "../../../components/ha-sub-menu";
import "../../../components/ha-svg-icon";
import "../../../components/ha-tooltip";
import { getSignedPath } from "../../../data/auth";
@@ -611,35 +607,42 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
);
}
private _renderCategoryItems = (submenu = false) =>
html`${this._categories?.map(
protected render(): TemplateResult {
if (
!this.hass ||
this._stateItems === undefined ||
this._entityEntries === undefined ||
this._configEntries === undefined
) {
return html`<hass-loading-screen></hass-loading-screen>`;
}
const categoryItems = html`${this._categories?.map(
(category) =>
html`<ha-dropdown-item
.slot=${submenu ? "submenu" : ""}
value="move_category"
data-category=${category.category_id}
html`<ha-md-menu-item
.value=${category.category_id}
.clickAction=${this._handleBulkCategory}
>
${category.icon
? html`<ha-icon slot="icon" .icon=${category.icon}></ha-icon>`
: html`<ha-svg-icon slot="icon" .path=${mdiTag}></ha-svg-icon>`}
${category.name}
</ha-dropdown-item>`
? html`<ha-icon slot="start" .icon=${category.icon}></ha-icon>`
: html`<ha-svg-icon slot="start" .path=${mdiTag}></ha-svg-icon>`}
<div slot="headline">${category.name}</div>
</ha-md-menu-item>`
)}
<ha-dropdown-item .slot=${submenu ? "submenu" : ""} value="no_category">
<ha-md-menu-item .value=${null} .clickAction=${this._handleBulkCategory}>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.no_category"
)}
</ha-dropdown-item>
<wa-divider .slot=${submenu ? "submenu" : ""}></wa-divider>
<ha-dropdown-item
.slot=${submenu ? "submenu" : ""}
value="create_category"
>
</div>
</ha-md-menu-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item .clickAction=${this._bulkCreateCategory}>
<div slot="headline">
${this.hass.localize("ui.panel.config.category.editor.add")}
</ha-dropdown-item>`;
private _renderLabelItems = (submenu = false) =>
html`${this._labels?.map((label) => {
</div>
</ha-md-menu-item>`;
const labelItems = html`${this._labels?.map((label) => {
const color = label.color ? computeCssColor(label.color) : undefined;
const selected = this._selected.every((entityId) =>
this._labelsForEntity(entityId).includes(label.label_id)
@@ -649,14 +652,14 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
this._selected.some((entityId) =>
this._labelsForEntity(entityId).includes(label.label_id)
);
return html`<ha-dropdown-item
@click=${this._handleLabelMenuSelect}
.slot=${submenu ? "submenu" : ""}
return html`<ha-md-menu-item
.value=${label.label_id}
data-action=${selected ? "remove" : "add"}
.action=${selected ? "remove" : "add"}
@click=${this._handleBulkLabel}
keep-open
>
<ha-checkbox
slot="icon"
slot="start"
.checked=${selected}
.indeterminate=${partial}
reducedTouchTarget
@@ -670,25 +673,13 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
: nothing}
${label.name}
</ha-label>
</ha-dropdown-item>`;
})}<wa-divider .slot=${submenu ? "submenu" : ""}></wa-divider>
<ha-dropdown-item
@click=${this._bulkCreateLabel}
.slot=${submenu ? "submenu" : ""}
>
</ha-md-menu-item> `;
})}<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item .clickAction=${this._bulkCreateLabel}>
<div slot="headline">
${this.hass.localize("ui.panel.config.labels.add_label")}
</ha-dropdown-item>`;
protected render(): TemplateResult {
if (
!this.hass ||
this._stateItems === undefined ||
this._entityEntries === undefined ||
this._configEntries === undefined
) {
return html`<hass-loading-screen></hass-loading-screen>`;
}
</div>
</ha-md-menu-item>`;
const labelsInOverflow =
(this._sizeController.value && this._sizeController.value < 700) ||
(!this._sizeController.value && this.hass.dockedSidebar === "docked");
@@ -790,10 +781,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
></ha-filter-categories>
${!this.narrow
? html`<ha-dropdown
slot="selection-bar"
@wa-select=${this._handleMenuSelect}
>
? html`<ha-md-button-menu slot="selection-bar">
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
@@ -805,11 +793,11 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-assist-chip>
${this._renderCategoryItems()}
</ha-dropdown>
${categoryItems}
</ha-md-button-menu>
${labelsInOverflow
? nothing
: html`<ha-dropdown slot="selection-bar">
: html`<ha-md-button-menu slot="selection-bar">
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
@@ -821,15 +809,14 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-assist-chip>
${this._renderLabelItems()}
</ha-dropdown>`}`
${labelItems}
</ha-md-button-menu>`}`
: nothing}
${this.narrow || labelsInOverflow
? html`<ha-dropdown
slot="selection-bar"
@wa-select=${this._handleMenuSelect}
>
${this.narrow
? html`
<ha-md-button-menu has-overflow slot="selection-bar">
${
this.narrow
? html`<ha-assist-chip
.label=${this.hass.localize(
"ui.panel.config.automation.picker.bulk_action"
@@ -847,24 +834,50 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
"ui.panel.config.automation.picker.bulk_action"
)}
slot="trigger"
></ha-icon-button>`}
${this.narrow
? html`<ha-dropdown-item>
></ha-icon-button>`
}
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon
></ha-assist-chip>
${
this.narrow
? html`<ha-sub-menu>
<ha-md-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.move_category"
)}
${this._renderCategoryItems(true)}
</ha-dropdown-item>`
: nothing}
${this.narrow || this.hass.dockedSidebar === "docked"
? html`<ha-dropdown-item>
</div>
<ha-svg-icon
slot="end"
.path=${mdiChevronRight}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu slot="menu">${categoryItems}</ha-md-menu>
</ha-sub-menu>`
: nothing
}
${
this.narrow || this.hass.dockedSidebar === "docked"
? html` <ha-sub-menu>
<ha-md-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.add_label"
)}
${this._renderLabelItems(true)}
</ha-dropdown-item>`
: nothing}
</ha-dropdown>`
</div>
<ha-svg-icon
slot="end"
.path=${mdiChevronRight}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu slot="menu">${labelItems}</ha-md-menu>
</ha-sub-menu>`
: nothing
}
</ha-md-button-menu>`
: nothing}
<ha-integration-overflow-menu
@@ -1007,7 +1020,12 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
});
}
private async _bulkAddCategory(category: string | null) {
private _handleBulkCategory = (item) => {
const category = item.value;
this._bulkAddCategory(category);
};
private async _bulkAddCategory(category: string) {
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
this._selected.forEach((entityId) => {
promises.push(
@@ -1032,6 +1050,12 @@ ${rejected
}
}
private async _handleBulkLabel(ev) {
const label = ev.currentTarget.value;
const action = ev.currentTarget.action;
this._bulkLabel(label, action);
}
private async _bulkLabel(label: string, action: "add" | "remove") {
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
this._selected.forEach((entityId) => {
@@ -1067,32 +1091,6 @@ ${rejected
this._selected = ev.detail.value;
}
private _handleMenuSelect(ev: CustomEvent) {
const item = ev.detail.item as HaDropdownItem;
switch (item.value) {
case "no_category":
this._bulkAddCategory(null);
break;
case "create_category":
this._bulkCreateCategory();
break;
case "move_category":
if (item.dataset.category) {
this._bulkAddCategory(item.dataset.category);
}
break;
}
}
private _handleLabelMenuSelect(ev: CustomEvent) {
ev.stopPropagation();
const item = ev.currentTarget as HaDropdownItem;
const label = item.value as string;
const action = (item as HTMLElement).dataset.action as "add" | "remove";
this._bulkLabel(label, action);
}
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
@@ -1433,7 +1431,7 @@ ${rejected
ha-assist-chip {
--ha-assist-chip-container-shape: 10px;
}
ha-dropdown ha-assist-chip {
ha-md-button-menu ha-assist-chip {
--md-assist-chip-trailing-space: 8px;
}
ha-label {

View File

@@ -16,13 +16,14 @@ import "../../../../../components/buttons/ha-progress-button";
import type { HaProgressButton } from "../../../../../components/buttons/ha-progress-button";
import "../../../../../components/ha-alert";
import "../../../../../components/ha-card";
import "../../../../../components/ha-generic-picker";
import "../../../../../components/ha-list-item";
import type { PickerComboBoxItem } from "../../../../../components/ha-picker-combo-box";
import "../../../../../components/ha-select";
import "../../../../../components/ha-selector/ha-selector-boolean";
import "../../../../../components/ha-settings-row";
import "../../../../../components/ha-svg-icon";
import "../../../../../components/ha-textfield";
import "../../../../../components/ha-combo-box";
import type {
ZWaveJSNodeCapabilities,
ZWaveJSNodeConfigParam,
@@ -329,19 +330,22 @@ class ZWaveJSNodeConfig extends LitElement {
) {
return html`
${labelAndDescription}
<ha-combo-box
<ha-generic-picker
.hass=${this.hass}
.value=${item.value?.toString()}
allow-custom-value
hide-clear-icon
.items=${this._getComboBoxOptions(item.metadata.states)}
.getItems=${this._getManualEntryItems(item.metadata.states)}
.disabled=${!item.metadata.writeable}
.invalid=${result?.status === "error"}
.placeholder=${item.metadata.unit}
.helper=${`${this.hass.localize("ui.panel.config.zwave_js.node_config.between_min_max", { min: item.metadata.min, max: item.metadata.max })}${defaultLabel ? `, ${defaultLabel}` : ""}`}
.valueRenderer=${this._enumeratedPickerValueRenderer(
item.metadata.states
)}
@value-changed=${this._getComboBoxValueChangedCallback(id, item)}
>
</ha-combo-box>
</ha-generic-picker>
`;
}
return html`${labelAndDescription}
@@ -363,7 +367,10 @@ class ZWaveJSNodeConfig extends LitElement {
</ha-textfield>`;
}
if (item.configuration_value_type === "enumerated") {
if (
item.configuration_value_type === "enumerated" &&
Object.keys(item.metadata.states).length < 5
) {
return html`
${labelAndDescription}
<ha-select
@@ -385,6 +392,28 @@ class ZWaveJSNodeConfig extends LitElement {
</ha-select>
`;
}
if (item.configuration_value_type === "enumerated") {
return html`
${labelAndDescription}
<ha-generic-picker
.hass=${this.hass}
.disabled=${!item.metadata.writeable}
.value=${item.value?.toString()}
.key=${id}
hide-clear-icon
@value-changed=${this._pickerValueChanged}
.helper=${defaultLabel}
.getItems=${this._getEnumeratedPickerItems(item.metadata.states!)}
.valueRenderer=${this._enumeratedPickerValueRenderer(
item.metadata.states!
)}
.property=${item.property}
.endpoint=${item.endpoint}
.propertyKey=${item.property_key}
>
</ha-generic-picker>
`;
}
return html`${labelAndDescription}
<p>${item.value}</p>`;
@@ -429,15 +458,23 @@ class ZWaveJSNodeConfig extends LitElement {
}
private _dropdownSelected(ev) {
this._handleEnumeratedPickerValueChanged(ev, ev.target.value);
}
private _pickerValueChanged(ev) {
this._handleEnumeratedPickerValueChanged(ev, ev.detail.value);
}
private _handleEnumeratedPickerValueChanged(ev, value: string) {
if (ev.target === undefined || this._config![ev.target.key] === undefined) {
return;
}
if (this._config![ev.target.key].value?.toString() === ev.target.value) {
if (this._config![ev.target.key].value?.toString() === value) {
return;
}
this._setResult(ev.target.key, undefined);
this._updateConfigParameter(ev.target, Number(ev.target.value));
this._updateConfigParameter(ev.target, Number(value));
}
private _numericInputChanged(ev) {
@@ -474,11 +511,36 @@ class ZWaveJSNodeConfig extends LitElement {
this._updateConfigParameter(ev.target, value);
}
private _getComboBoxOptions = memoizeOne((states: Record<string, string>) =>
Object.entries(states).map(([value, label]) => ({
value,
label: `${value} - ${label}`,
}))
private _getEnumeratedPickerItems = memoizeOne(
(states: Record<string, string>) => {
const items: PickerComboBoxItem[] = Object.entries(states).map(
([value, label]) => ({
id: value,
primary: label,
sorting_label: `${label}_${value}`,
})
);
return () => items;
}
);
private _enumeratedPickerValueRenderer = memoizeOne(
(states: Record<string, string>) => (value: string) =>
html`<span slot="headline">${states[value] || value}</span>`
);
private _getManualEntryItems = memoizeOne(
(states: Record<string, string>) => {
const items: PickerComboBoxItem[] = Object.entries(states).map(
([value, label]) => ({
id: value,
primary: `${label}`,
secondary: value,
sorting_label: `${label}_${value}`,
})
);
return () => items;
}
);
private _getComboBoxValueChangedCallback(

View File

@@ -5,9 +5,10 @@ import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-color-picker";
import { createCloseHeading } from "../../../components/ha-dialog";
import "../../../components/ha-dialog-footer";
import "../../../components/ha-icon-picker";
import "../../../components/ha-switch";
import "../../../components/ha-wa-dialog";
import "../../../components/ha-textarea";
import "../../../components/ha-textfield";
import type { LabelRegistryEntryMutableParams } from "../../../data/label/label_registry";
@@ -37,6 +38,8 @@ class DialogLabelDetail
@state() private _submitting = false;
@state() private _open = false;
public showDialog(params: LabelDetailDialogParams): void {
this._params = params;
this._error = undefined;
@@ -51,20 +54,17 @@ class DialogLabelDetail
this._color = "";
this._description = "";
}
document.body.addEventListener("keydown", this._handleKeyPress);
this._open = true;
}
private _handleKeyPress = (ev: KeyboardEvent) => {
if (ev.key === "Escape") {
ev.stopPropagation();
}
};
public closeDialog() {
this._open = false;
return true;
}
private _dialogClosed(): void {
this._params = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
document.body.removeEventListener("keydown", this._handleKeyPress);
return true;
}
protected render() {
@@ -73,17 +73,13 @@ class DialogLabelDetail
}
return html`
<ha-dialog
open
@closed=${this.closeDialog}
scrimClickAction
escapeKeyAction
.heading=${createCloseHeading(
this.hass,
this._params.entry
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
header-title=${this._params.entry
? this._params.entry.name || this._params.entry.label_id
: this.hass!.localize("ui.dialogs.label-detail.new_label")
)}
: this.hass!.localize("ui.dialogs.label-detail.new_label")}
@closed=${this._dialogClosed}
>
<div>
${this._error
@@ -91,7 +87,7 @@ class DialogLabelDetail
: ""}
<div class="form">
<ha-textfield
dialogInitialFocus
autofocus
.value=${this._name}
.configValue=${"name"}
@input=${this._input}
@@ -125,6 +121,8 @@ class DialogLabelDetail
></ha-textarea>
</div>
</div>
<ha-dialog-footer slot="footer">
${this._params.entry && this._params.removeEntry
? html`
<ha-button
@@ -147,7 +145,8 @@ class DialogLabelDetail
? this.hass!.localize("ui.common.update")
: this.hass!.localize("ui.common.create")}
</ha-button>
</ha-dialog>
</ha-dialog-footer>
</ha-wa-dialog>
`;
}

View File

@@ -4,10 +4,8 @@ import { customElement, property, state } from "lit/decorators";
import { cache } from "lit/directives/cache";
import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-button-menu";
import "../../../components/ha-card";
import "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import type { HaDropdownItem } from "../../../components/ha-dropdown-item";
import "../../../components/ha-expansion-panel";
import "../../../components/ha-formfield";
import "../../../components/ha-icon-button";
@@ -502,18 +500,15 @@ export class HassioNetwork extends LitElement {
`
)}
</div>
<ha-dropdown
@wa-show=${this._handleDNSMenuOpened}
@wa-hide=${this._handleDNSMenuClosed}
@wa-select=${this._handleDNSMenuSelect}
<ha-button-menu
@opened=${this._handleDNSMenuOpened}
@closed=${this._handleDNSMenuClosed}
.version=${version}
>
<ha-button
class="add-nameserver"
appearance="filled"
size="small"
slot="trigger"
class="add-nameserver"
>
<ha-button appearance="filled" size="small" slot="trigger">
${this.hass.localize(
"ui.panel.config.network.supervisor.add_dns_server"
)}
@@ -524,21 +519,21 @@ export class HassioNetwork extends LitElement {
</ha-button>
${Object.entries(PREDEFINED_DNS[version]).map(
([name, addresses]) => html`
<ha-dropdown-item
value="predefined"
<ha-list-item
@click=${this._addPredefinedDNS}
.version=${version}
.addresses=${addresses}
>
${name}
</ha-dropdown-item>
</ha-list-item>
`
)}
<ha-dropdown-item value="custom" .version=${version}>
<ha-list-item @click=${this._addCustomDNS} .version=${version}>
${this.hass.localize(
"ui.panel.config.network.supervisor.custom_dns"
)}
</ha-dropdown-item>
</ha-dropdown>
</ha-list-item>
</ha-button-menu>
`
: nothing}
</ha-expansion-panel>
@@ -752,23 +747,21 @@ export class HassioNetwork extends LitElement {
this._dnsMenuOpen = false;
}
private _handleDNSMenuSelect(ev: CustomEvent) {
const item = ev.detail.item as HaDropdownItem & {
version: "ipv4" | "ipv6";
addresses?: string[];
};
const version = item.version;
if (item.value === "predefined" && item.addresses) {
private _addPredefinedDNS(ev: Event) {
const source = ev.target as any;
const version = source.version as "ipv4" | "ipv6";
const addresses = source.addresses as string[];
if (!this._interface![version]!.nameservers) {
this._interface![version]!.nameservers = [];
}
this._interface![version]!.nameservers!.push(...item.addresses);
this._interface![version]!.nameservers!.push(...addresses);
this._dirty = true;
this.requestUpdate("_interface");
return;
}
if (item.value === "custom") {
private _addCustomDNS(ev: Event) {
const source = ev.target as any;
const version = source.version as "ipv4" | "ipv6";
if (!this._interface![version]!.nameservers) {
this._interface![version]!.nameservers = [];
}
@@ -776,7 +769,6 @@ export class HassioNetwork extends LitElement {
this._dirty = true;
this.requestUpdate("_interface");
}
}
private _removeNameserver(ev: Event): void {
const source = ev.target as any;

View File

@@ -1,7 +1,7 @@
import "@home-assistant/webawesome/dist/components/divider/divider";
import { ResizeController } from "@lit-labs/observers/resize-controller";
import { consume } from "@lit/context";
import {
mdiChevronRight,
mdiCog,
mdiContentDuplicate,
mdiDelete,
@@ -45,9 +45,6 @@ import type {
} from "../../../components/data-table/ha-data-table";
import "../../../components/data-table/ha-data-table-labels";
import "../../../components/ha-button";
import "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import type { HaDropdownItem } from "../../../components/ha-dropdown-item";
import "../../../components/ha-fab";
import "../../../components/ha-filter-categories";
import "../../../components/ha-filter-devices";
@@ -56,7 +53,11 @@ import "../../../components/ha-filter-floor-areas";
import "../../../components/ha-filter-labels";
import "../../../components/ha-icon-button";
import "../../../components/ha-icon-overflow-menu";
import "../../../components/ha-md-divider";
import "../../../components/ha-md-menu";
import "../../../components/ha-md-menu-item";
import "../../../components/ha-state-icon";
import "../../../components/ha-sub-menu";
import "../../../components/ha-svg-icon";
import "../../../components/ha-tooltip";
import { createAreaRegistryEntry } from "../../../data/area_registry";
@@ -433,8 +434,34 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
];
}
private _renderLabelItems = (submenu = false) =>
html` ${this._labels?.map((label) => {
protected render(): TemplateResult {
const categoryItems = html`${this._categories?.map(
(category) =>
html`<ha-md-menu-item
.value=${category.category_id}
.clickAction=${this._handleBulkCategory}
>
${category.icon
? html`<ha-icon slot="start" .icon=${category.icon}></ha-icon>`
: html`<ha-svg-icon slot="start" .path=${mdiTag}></ha-svg-icon>`}
<div slot="headline">${category.name}</div>
</ha-md-menu-item>`
)}
<ha-md-menu-item .value=${null} .clickAction=${this._handleBulkCategory}>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.no_category"
)}
</div>
</ha-md-menu-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item .clickAction=${this._bulkCreateCategory}>
<div slot="headline">
${this.hass.localize("ui.panel.config.category.editor.add")}
</div>
</ha-md-menu-item>`;
const labelItems = html` ${this._labels?.map((label) => {
const color = label.color ? computeCssColor(label.color) : undefined;
const selected = this._selected.every((entityId) =>
this.hass.entities[entityId]?.labels.includes(label.label_id)
@@ -444,14 +471,14 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
this._selected.some((entityId) =>
this.hass.entities[entityId]?.labels.includes(label.label_id)
);
return html`<ha-dropdown-item
.slot=${submenu ? "submenu" : ""}
value=${label.label_id}
data-action=${selected ? "remove" : "add"}
@click=${this._handleLabelMenuSelect}
return html`<ha-md-menu-item
.value=${label.label_id}
.action=${selected ? "remove" : "add"}
@click=${this._handleBulkLabel}
keep-open
>
<ha-checkbox
slot="icon"
slot="start"
.checked=${selected}
.indeterminate=${partial}
reducedTouchTarget
@@ -465,74 +492,46 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
: nothing}
${label.name}
</ha-label>
</ha-dropdown-item>`;
</ha-md-menu-item>`;
})}
<wa-divider .slot=${submenu ? "submenu" : ""}></wa-divider>
<ha-dropdown-item
@click=${this._handleCreateLabel}
.slot=${submenu ? "submenu" : ""}
value="create-label"
>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item .clickAction=${this._bulkCreateLabel}>
<div slot="headline">
${this.hass.localize("ui.panel.config.labels.add_label")}
</ha-dropdown-item>`;
</div></ha-md-menu-item
>`;
private _renderCategoryItems = (submenu = false) =>
html`${this._categories?.map(
(category) =>
html`<ha-dropdown-item
.slot=${submenu ? "submenu" : ""}
value="move_category"
data-category=${category.category_id}
>
${category.icon
? html`<ha-icon slot="icon" .icon=${category.icon}></ha-icon>`
: html`<ha-svg-icon slot="icon" .path=${mdiTag}></ha-svg-icon>`}
${category.name}
</ha-dropdown-item>`
)}
<ha-dropdown-item .slot=${submenu ? "submenu" : ""} value="no-category">
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.no_category"
)}
</ha-dropdown-item>
<wa-divider .slot=${submenu ? "submenu" : ""}></wa-divider>
<ha-dropdown-item
.slot=${submenu ? "submenu" : ""}
value="create-category"
>
${this.hass.localize("ui.panel.config.category.editor.add")}
</ha-dropdown-item>`;
private _renderAreaItems = (submenu = false) =>
html`${Object.values(this.hass.areas).map(
const areaItems = html`${Object.values(this.hass.areas).map(
(area) =>
html`<ha-dropdown-item
.slot=${submenu ? "submenu" : ""}
value="move_area"
data-area=${area.area_id}
html`<ha-md-menu-item
.value=${area.area_id}
.clickAction=${this._handleBulkArea}
>
${area.icon
? html`<ha-icon slot="icon" .icon=${area.icon}></ha-icon>`
? html`<ha-icon slot="start" .icon=${area.icon}></ha-icon>`
: html`<ha-svg-icon
slot="icon"
slot="start"
.path=${mdiTextureBox}
></ha-svg-icon>`}
${area.name}
</ha-dropdown-item>`
<div slot="headline">${area.name}</div>
</ha-md-menu-item>`
)}
<ha-dropdown-item .slot=${submenu ? "submenu" : ""} value="no-area">
<ha-md-menu-item .value=${null} .clickAction=${this._handleBulkArea}>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.no_area"
)}
</ha-dropdown-item>
<wa-divider .slot=${submenu ? "submenu" : ""}></wa-divider>
<ha-dropdown-item .slot=${submenu ? "submenu" : ""} value="create-area">
</div>
</ha-md-menu-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item .clickAction=${this._bulkCreateArea}>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.add_area"
)}
</ha-dropdown-item>`;
</div>
</ha-md-menu-item>`;
protected render(): TemplateResult {
const areasInOverflow =
(this._sizeController.value && this._sizeController.value < 900) ||
(!this._sizeController.value && this.hass.dockedSidebar === "docked");
@@ -655,10 +654,7 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
></ha-filter-categories>
${!this.narrow
? html`<ha-dropdown
slot="selection-bar"
@wa-select=${this._handleOverflowMenuSelect}
>
? html`<ha-md-button-menu slot="selection-bar">
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
@@ -670,11 +666,11 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-assist-chip>
${this._renderCategoryItems()}
</ha-dropdown>
${categoryItems}
</ha-md-button-menu>
${labelsInOverflow
? nothing
: html`<ha-dropdown slot="selection-bar">
: html`<ha-md-button-menu slot="selection-bar">
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
@@ -686,14 +682,11 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-assist-chip>
${this._renderLabelItems()}
</ha-dropdown>`}
${labelItems}
</ha-md-button-menu>`}
${areasInOverflow
? nothing
: html`<ha-dropdown
slot="selection-bar"
@wa-select=${this._handleOverflowMenuSelect}
>
: html`<ha-md-button-menu slot="selection-bar">
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
@@ -705,15 +698,14 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-assist-chip>
${this._renderAreaItems()}
</ha-dropdown>`}`
${areaItems}
</ha-md-button-menu>`}`
: nothing}
${this.narrow || areasInOverflow
? html` <ha-dropdown
slot="selection-bar"
@wa-select=${this._handleOverflowMenuSelect}
>
${this.narrow
? html`
<ha-md-button-menu has-overflow slot="selection-bar">
${
this.narrow
? html`<ha-assist-chip
.label=${this.hass.localize(
"ui.panel.config.automation.picker.bulk_action"
@@ -731,32 +723,68 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
"ui.panel.config.automation.picker.bulk_action"
)}
slot="trigger"
></ha-icon-button>`}
${this.narrow
? html`<ha-dropdown-item>
></ha-icon-button>`
}
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon
></ha-assist-chip>
${
this.narrow
? html`<ha-sub-menu>
<ha-md-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.move_category"
)}
${this._renderCategoryItems(true)}
</ha-dropdown-item>`
: nothing}
${this.narrow || labelsInOverflow
? html`<ha-dropdown-item>
</div>
<ha-svg-icon
slot="end"
.path=${mdiChevronRight}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu slot="menu">${categoryItems}</ha-md-menu>
</ha-sub-menu>`
: nothing
}
${
this.narrow || labelsInOverflow
? html`<ha-sub-menu>
<ha-md-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.add_label"
)}
${this._renderLabelItems(true)}
</ha-dropdown-item>`
: nothing}
${this.narrow || areasInOverflow
? html`<ha-dropdown-item>
</div>
<ha-svg-icon
slot="end"
.path=${mdiChevronRight}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu slot="menu">${labelItems}</ha-md-menu>
</ha-sub-menu>`
: nothing
}
${
this.narrow || areasInOverflow
? html`<ha-sub-menu>
<ha-md-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.move_area"
)}
${this._renderAreaItems(true)}
</ha-dropdown-item>`
: nothing}
</ha-dropdown>`
</div>
<ha-svg-icon
slot="end"
.path=${mdiChevronRight}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu slot="menu">${areaItems}</ha-md-menu>
</ha-sub-menu>`
: nothing
}
</ha-md-button-menu>`
: nothing}
${!this.scenes.length
? html`<div class="empty" slot="empty">
@@ -927,52 +955,12 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
}
}
private _handleLabelMenuSelect = (ev: CustomEvent) => {
ev.stopPropagation();
const item = ev.currentTarget as HaDropdownItem & {
dataset: { action?: string };
};
const action = item.dataset.action as "add" | "remove";
this._bulkLabel(item.value, action);
private _handleBulkCategory = (item) => {
const category = item.value;
this._bulkAddCategory(category);
};
private _handleCreateLabel = () => {
this._bulkCreateLabel();
};
private _handleOverflowMenuSelect = (ev: CustomEvent) => {
const item = ev.detail.item as HaDropdownItem;
switch (item.value) {
case "create-category":
this._bulkCreateCategory();
break;
case "no-category":
this._bulkAddCategory(null);
break;
case "create-label":
this._bulkCreateLabel();
break;
case "create-area":
this._bulkCreateArea();
break;
case "no-area":
this._bulkAddArea(null);
break;
case "move_category":
if (item.dataset.category) {
this._bulkAddCategory(item.dataset.category);
}
break;
case "move_area":
if (item.dataset.area) {
this._bulkAddArea(item.dataset.area);
}
break;
}
};
private async _bulkAddCategory(category: string | null) {
private async _bulkAddCategory(category: string) {
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
this._selected.forEach((entityId) => {
promises.push(
@@ -997,6 +985,12 @@ ${rejected
}
}
private async _handleBulkLabel(ev) {
const label = ev.currentTarget.value;
const action = ev.currentTarget.action;
this._bulkLabel(label, action);
}
private async _bulkLabel(label: string, action: "add" | "remove") {
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
this._selected.forEach((entityId) => {
@@ -1027,7 +1021,12 @@ ${rejected
}
}
private async _bulkAddArea(area: string | null) {
private _handleBulkArea = (item) => {
const area = item.value;
this._bulkAddArea(area);
};
private async _bulkAddArea(area: string) {
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
this._selected.forEach((entityId) => {
promises.push(
@@ -1232,7 +1231,7 @@ ${rejected
ha-assist-chip {
--ha-assist-chip-container-shape: 10px;
}
ha-dropdown ha-assist-chip {
ha-md-button-menu ha-assist-chip {
--md-assist-chip-trailing-space: 8px;
}
ha-label {

View File

@@ -1,6 +1,6 @@
import "@home-assistant/webawesome/dist/components/divider/divider";
import { consume } from "@lit/context";
import type { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import {
mdiCog,
mdiContentDuplicate,
@@ -32,10 +32,8 @@ import "../../../components/entity/ha-entities-picker";
import "../../../components/ha-alert";
import "../../../components/ha-area-picker";
import "../../../components/ha-button";
import "../../../components/ha-button-menu";
import "../../../components/ha-card";
import "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import type { HaDropdownItem } from "../../../components/ha-dropdown-item";
import "../../../components/ha-fab";
import "../../../components/ha-icon-button";
import "../../../components/ha-icon-picker";
@@ -229,66 +227,78 @@ export class HaSceneEditor extends PreventUnsavedMixin(
? computeStateName(this._scene)
: this.hass.localize("ui.panel.config.scene.editor.default_name")}
>
<ha-dropdown slot="toolbar-icon" @wa-select=${this._handleMenuAction}>
<ha-button-menu
slot="toolbar-icon"
@action=${this._handleMenuAction}
activatable
>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-dropdown-item
value="apply"
<ha-list-item
graphic="icon"
.disabled=${!this.sceneId || this._mode === "live"}
>
<ha-svg-icon slot="icon" .path=${mdiPlay}></ha-svg-icon>
${this.hass.localize("ui.panel.config.scene.picker.apply")}
</ha-dropdown-item>
<ha-dropdown-item value="info" .disabled=${!this.sceneId}>
<ha-svg-icon slot="graphic" .path=${mdiPlay}></ha-svg-icon>
</ha-list-item>
<ha-list-item graphic="icon" .disabled=${!this.sceneId}>
${this.hass.localize("ui.panel.config.scene.picker.show_info")}
<ha-svg-icon
slot="icon"
slot="graphic"
.path=${mdiInformationOutline}
></ha-svg-icon>
${this.hass.localize("ui.panel.config.scene.picker.show_info")}
</ha-dropdown-item>
<ha-dropdown-item value="settings" .disabled=${!this.sceneId}>
<ha-svg-icon slot="icon" .path=${mdiCog}></ha-svg-icon>
</ha-list-item>
<ha-list-item graphic="icon" .disabled=${!this.sceneId}>
${this.hass.localize(
"ui.panel.config.automation.picker.show_settings"
)}
</ha-dropdown-item>
<ha-svg-icon slot="graphic" .path=${mdiCog}></ha-svg-icon>
</ha-list-item>
<ha-dropdown-item value="category" .disabled=${!this.sceneId}>
<ha-svg-icon slot="icon" .path=${mdiTag}></ha-svg-icon>
<ha-list-item graphic="icon" .disabled=${!this.sceneId}>
${this.hass.localize(
`ui.panel.config.scene.picker.${this._getCategory(this._entityRegistryEntries, this._scene?.entity_id) ? "edit_category" : "assign_category"}`
)}
</ha-dropdown-item>
<ha-svg-icon slot="graphic" .path=${mdiTag}></ha-svg-icon>
</ha-list-item>
<ha-dropdown-item value="toggle_yaml">
<ha-svg-icon slot="icon" .path=${mdiPlaylistEdit}></ha-svg-icon>
<ha-list-item graphic="icon">
${this.hass.localize(
`ui.panel.config.automation.editor.edit_${this._mode !== "yaml" ? "yaml" : "ui"}`
)}
</ha-dropdown-item>
<ha-svg-icon slot="graphic" .path=${mdiPlaylistEdit}></ha-svg-icon>
</ha-list-item>
<wa-divider></wa-divider>
<li divider role="separator"></li>
<ha-dropdown-item value="duplicate" .disabled=${!this.sceneId}>
<ha-svg-icon slot="icon" .path=${mdiContentDuplicate}></ha-svg-icon>
<ha-list-item .disabled=${!this.sceneId} graphic="icon">
${this.hass.localize(
"ui.panel.config.scene.picker.duplicate_scene"
)}
</ha-dropdown-item>
<ha-svg-icon
slot="graphic"
.path=${mdiContentDuplicate}
></ha-svg-icon>
</ha-list-item>
<ha-dropdown-item
value="delete"
<ha-list-item
.disabled=${!this.sceneId}
.variant=${this.sceneId ? "danger" : "default"}
class=${classMap({ warning: Boolean(this.sceneId) })}
graphic="icon"
>
<ha-svg-icon slot="icon" .path=${mdiDelete}></ha-svg-icon>
${this.hass.localize("ui.panel.config.scene.picker.delete_scene")}
</ha-dropdown-item>
</ha-dropdown>
<ha-svg-icon
class=${classMap({ warning: Boolean(this.sceneId) })}
slot="graphic"
.path=${mdiDelete}
>
</ha-svg-icon>
</ha-list-item>
</ha-button-menu>
${this._errors ? html` <div class="errors">${this._errors}</div> ` : ""}
${this._mode === "yaml" ? this._renderYamlMode() : this._renderUiMode()}
<ha-fab
@@ -642,25 +652,24 @@ export class HaSceneEditor extends PreventUnsavedMixin(
}
}
private async _handleMenuAction(ev: CustomEvent) {
const item = ev.detail.item as HaDropdownItem;
switch (item.value) {
case "apply":
private async _handleMenuAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
activateScene(this.hass, this._scene!.entity_id);
break;
case "info":
case 1:
fireEvent(this, "hass-more-info", { entityId: this._scene!.entity_id });
break;
case "settings":
case 2:
showMoreInfoDialog(this, {
entityId: this._scene!.entity_id,
view: "settings",
});
break;
case "category":
case 3:
this._editCategory(this._scene!);
break;
case "toggle_yaml":
case 4:
if (this._mode === "yaml") {
this._initEntities(this._config!);
this._exitYamlMode();
@@ -668,10 +677,10 @@ export class HaSceneEditor extends PreventUnsavedMixin(
this._enterYamlMode();
}
break;
case "duplicate":
case 5:
this._duplicate();
break;
case "delete":
case 6:
this._deleteTapped();
break;
}

View File

@@ -1,7 +1,7 @@
import "@home-assistant/webawesome/dist/components/divider/divider";
import { ResizeController } from "@lit-labs/observers/resize-controller";
import { consume } from "@lit/context";
import {
mdiChevronRight,
mdiCog,
mdiContentDuplicate,
mdiDelete,
@@ -46,9 +46,6 @@ import type {
SortingChangedEvent,
} from "../../../components/data-table/ha-data-table";
import "../../../components/data-table/ha-data-table-labels";
import "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import type { HaDropdownItem } from "../../../components/ha-dropdown-item";
import "../../../components/ha-fab";
import "../../../components/ha-filter-blueprints";
import "../../../components/ha-filter-categories";
@@ -58,6 +55,10 @@ import "../../../components/ha-filter-floor-areas";
import "../../../components/ha-filter-labels";
import "../../../components/ha-icon-button";
import "../../../components/ha-icon-overflow-menu";
import "../../../components/ha-md-divider";
import "../../../components/ha-md-menu";
import "../../../components/ha-md-menu-item";
import "../../../components/ha-sub-menu";
import "../../../components/ha-svg-icon";
import "../../../components/ha-tooltip";
import { createAreaRegistryEntry } from "../../../data/area_registry";
@@ -418,35 +419,33 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
];
}
private _renderCategoryItems = (submenu = false) =>
html`${this._categories?.map(
protected render(): TemplateResult {
const categoryItems = html`${this._categories?.map(
(category) =>
html`<ha-dropdown-item
.slot=${submenu ? "submenu" : ""}
value="move_category"
data-category=${category.category_id}
html`<ha-md-menu-item
.value=${category.category_id}
.clickAction=${this._handleBulkCategory}
>
${category.icon
? html`<ha-icon slot="icon" .icon=${category.icon}></ha-icon>`
: html`<ha-svg-icon slot="icon" .path=${mdiTag}></ha-svg-icon>`}
${category.name}
</ha-dropdown-item>`
? html`<ha-icon slot="start" .icon=${category.icon}></ha-icon>`
: html`<ha-svg-icon slot="start" .path=${mdiTag}></ha-svg-icon>`}
<div slot="headline">${category.name}</div>
</ha-md-menu-item>`
)}
<ha-dropdown-item .slot=${submenu ? "submenu" : ""} value="no-category">
<ha-md-menu-item .value=${null} .clickAction=${this._handleBulkCategory}>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.no_category"
)}
</ha-dropdown-item>
<wa-divider .slot=${submenu ? "submenu" : ""}></wa-divider>
<ha-dropdown-item
.slot=${submenu ? "submenu" : ""}
value="create-category"
>
</div> </ha-md-menu-item
><ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item .clickAction=${this._bulkCreateCategory}>
<div slot="headline">
${this.hass.localize("ui.panel.config.category.editor.add")}
</ha-dropdown-item>`;
</div>
</ha-md-menu-item>`;
private _renderLabelItems = (submenu = false) =>
html`${this._labels?.map((label) => {
const labelItems = html`${this._labels?.map((label) => {
const color = label.color ? computeCssColor(label.color) : undefined;
const selected = this._selected.every((entityId) =>
this.hass.entities[entityId]?.labels.includes(label.label_id)
@@ -456,13 +455,15 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
this._selected.some((entityId) =>
this.hass.entities[entityId]?.labels.includes(label.label_id)
);
return html`<ha-dropdown-item
@click=${this._handleLabelMenuSelect}
.slot=${submenu ? "submenu" : ""}
value=${label.label_id}
data-action=${selected ? "remove" : "add"}
><ha-checkbox
slot="icon"
return html`<ha-md-menu-item
.value=${label.label_id}
.action=${selected ? "remove" : "add"}
@click=${this._handleBulkLabel}
keep-open
reducedTouchTarget
>
<ha-checkbox
slot="start"
.checked=${selected}
.indeterminate=${partial}
></ha-checkbox>
@@ -475,47 +476,46 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
: nothing}
${label.name}
</ha-label>
</ha-dropdown-item>`;
</ha-md-menu-item>`;
})}
<wa-divider .slot=${submenu ? "submenu" : ""}></wa-divider>
<ha-dropdown-item
@click=${this._bulkCreateLabel}
.slot=${submenu ? "submenu" : ""}
value="create-label"
>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item .clickAction=${this._bulkCreateLabel}>
<div slot="headline">
${this.hass.localize("ui.panel.config.labels.add_label")}
</ha-dropdown-item>`;
</div></ha-md-menu-item
>`;
private _renderAreaItems = (submenu = false) =>
html`${Object.values(this.hass.areas).map(
const areaItems = html`${Object.values(this.hass.areas).map(
(area) =>
html`<ha-dropdown-item
.slot=${submenu ? "submenu" : ""}
value="move_area"
data-area=${area.area_id}
html`<ha-md-menu-item
.value=${area.area_id}
.clickAction=${this._handleBulkArea}
>
${area.icon
? html`<ha-icon slot="icon" .icon=${area.icon}></ha-icon>`
? html`<ha-icon slot="start" .icon=${area.icon}></ha-icon>`
: html`<ha-svg-icon
slot="icon"
slot="start"
.path=${mdiTextureBox}
></ha-svg-icon>`}
${area.name}
</ha-dropdown-item>`
<div slot="headline">${area.name}</div>
</ha-md-menu-item>`
)}
<ha-dropdown-item .slot=${submenu ? "submenu" : ""} value="no-area">
<ha-md-menu-item .value=${null} .clickAction=${this._handleBulkArea}>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.no_area"
)}
</ha-dropdown-item>
<wa-divider .slot=${submenu ? "submenu" : ""}></wa-divider>
<ha-dropdown-item .slot=${submenu ? "submenu" : ""} value="create-area">
</div>
</ha-md-menu-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item .clickAction=${this._bulkCreateArea}>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.add_area"
)}
</ha-dropdown-item>`;
</div>
</ha-md-menu-item>`;
protected render(): TemplateResult {
const areasInOverflow =
(this._sizeController.value && this._sizeController.value < 900) ||
(!this._sizeController.value && this.hass.dockedSidebar === "docked");
@@ -647,10 +647,7 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
></ha-filter-blueprints>
${!this.narrow
? html`<ha-dropdown
slot="selection-bar"
@wa-select=${this._handleOverflowMenuSelect}
>
? html`<ha-md-button-menu slot="selection-bar">
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
@@ -662,11 +659,11 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-assist-chip>
${this._renderCategoryItems()}
</ha-dropdown>
${categoryItems}
</ha-md-button-menu>
${labelsInOverflow
? nothing
: html`<ha-dropdown slot="selection-bar">
: html`<ha-md-button-menu slot="selection-bar">
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
@@ -678,14 +675,11 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-assist-chip>
${this._renderLabelItems()}
</ha-dropdown>`}
${labelItems}
</ha-md-button-menu>`}
${areasInOverflow
? nothing
: html`<ha-dropdown
slot="selection-bar"
@wa-select=${this._handleOverflowMenuSelect}
>
: html`<ha-md-button-menu slot="selection-bar">
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
@@ -697,15 +691,14 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-assist-chip>
${this._renderAreaItems()}
</ha-dropdown>`}`
${areaItems}
</ha-md-button-menu>`}`
: nothing}
${this.narrow || areasInOverflow
? html` <ha-dropdown
slot="selection-bar"
@wa-select=${this._handleOverflowMenuSelect}
>
${this.narrow
? html`
<ha-md-button-menu has-overflow slot="selection-bar">
${
this.narrow
? html`<ha-assist-chip
.label=${this.hass.localize(
"ui.panel.config.automation.picker.bulk_action"
@@ -723,32 +716,68 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
"ui.panel.config.automation.picker.bulk_action"
)}
slot="trigger"
></ha-icon-button>`}
${this.narrow
? html`<ha-dropdown-item>
></ha-icon-button>`
}
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon
></ha-assist-chip>
${
this.narrow
? html`<ha-sub-menu>
<ha-md-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.move_category"
)}
${this._renderCategoryItems(true)}
</ha-dropdown-item>`
: nothing}
${this.narrow || labelsInOverflow
? html`<ha-dropdown-item>
</div>
<ha-svg-icon
slot="end"
.path=${mdiChevronRight}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu slot="menu">${categoryItems}</ha-md-menu>
</ha-sub-menu>`
: nothing
}
${
this.narrow || labelsInOverflow
? html`<ha-sub-menu>
<ha-md-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.add_label"
)}
${this._renderLabelItems(true)}
</ha-dropdown-item>`
: nothing}
${this.narrow || areasInOverflow
? html`<ha-dropdown-item>
</div>
<ha-svg-icon
slot="end"
.path=${mdiChevronRight}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu slot="menu">${labelItems}</ha-md-menu>
</ha-sub-menu>`
: nothing
}
${
this.narrow || areasInOverflow
? html`<ha-sub-menu>
<ha-md-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.move_area"
)}
${this._renderAreaItems(true)}
</ha-dropdown-item>`
: nothing}
</ha-dropdown>`
</div>
<ha-svg-icon
slot="end"
.path=${mdiChevronRight}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu slot="menu">${areaItems}</ha-md-menu>
</ha-sub-menu>`
: nothing
}
</ha-md-button-menu>`
: nothing}
${!this.scripts.length
? html` <div class="empty" slot="empty">
@@ -966,46 +995,9 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
this._selected = ev.detail.value;
}
private _handleLabelMenuSelect = (ev: CustomEvent) => {
ev.stopPropagation();
const item = ev.currentTarget as HaDropdownItem & {
dataset: { action?: string };
};
const action = item.dataset.action as "add" | "remove";
this._bulkLabel(item.value, action);
};
private _handleOverflowMenuSelect = (ev: CustomEvent) => {
const item = ev.detail.item as HaDropdownItem & {
dataset: { action?: string };
};
switch (item.value) {
case "create-category":
this._bulkCreateCategory();
break;
case "no-category":
this._bulkAddCategory(null!);
break;
case "create-label":
this._bulkCreateLabel();
break;
case "create-area":
this._bulkCreateArea();
break;
case "no-area":
this._bulkAddArea(null!);
break;
case "move_area":
if (item.dataset.area) {
this._bulkAddArea(item.dataset.area);
}
break;
case "move_category":
if (item.dataset.category) {
this._bulkAddCategory(item.dataset.category);
}
break;
}
private _handleBulkCategory = (item) => {
const category = item.value;
this._bulkAddCategory(category);
};
private async _bulkAddCategory(category: string) {
@@ -1033,6 +1025,12 @@ ${rejected
}
}
private async _handleBulkLabel(ev) {
const label = ev.currentTarget.value;
const action = ev.currentTarget.action;
this._bulkLabel(label, action);
}
private async _bulkLabel(label: string, action: "add" | "remove") {
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
this._selected.forEach((entityId) => {
@@ -1237,6 +1235,11 @@ ${rejected
});
};
private _handleBulkArea = (item) => {
const area = item.value;
this._bulkAddArea(area);
};
private async _bulkAddArea(area: string) {
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
this._selected.forEach((entityId) => {
@@ -1319,7 +1322,7 @@ ${rejected
ha-assist-chip {
--ha-assist-chip-container-shape: 10px;
}
ha-dropdown ha-assist-chip {
ha-md-button-menu ha-assist-chip {
--md-assist-chip-trailing-space: 8px;
}
ha-label {

View File

@@ -144,7 +144,6 @@ class HaPanelDevState extends LitElement {
.hass=${this.hass}
.value=${this._entityId}
@value-changed=${this._entityIdChanged}
allow-custom-entity
show-entity-id
></ha-entity-picker>
${this._entityId

View File

@@ -75,7 +75,6 @@ export class DialogEditHome
"ui.panel.home.editor.favorite_entities_helper"
)}
reorder
allow-custom-entity
@value-changed=${this._favoriteEntitiesChanged}
></ha-entities-picker>

View File

@@ -167,7 +167,6 @@ export class HuiEntityEditor extends LitElement {
.index=${index}
.entityFilter=${this.entityFilter}
@value-changed=${this._valueChanged}
allow-custom-entity
></ha-entity-picker>
</div>
`

View File

@@ -51,7 +51,6 @@ export class HuiGraphFooterEditor
return html`
<div class="card-config">
<ha-entity-picker
allow-custom-entity
.label=${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.entity"
)}

View File

@@ -78,7 +78,6 @@ export class HuiHeadingBadgesEditor extends LitElement {
${isEntityBadge && entityBadge
? html`
<ha-entity-picker
allow-custom-entity
hide-clear-icon
.hass=${this.hass}
.value=${entityBadge.entity ?? ""}
@@ -131,7 +130,6 @@ export class HuiHeadingBadgesEditor extends LitElement {
@value-changed=${this._entityPicked}
.value=${undefined}
@click=${preventDefault}
allow-custom-entity
add-button
></ha-entity-picker>
</div>

View File

@@ -316,7 +316,6 @@ export class HuiStatisticsGraphCardEditor
@value-changed=${this._valueChanged}
></ha-form>
<ha-statistics-picker
allow-custom-entity
.hass=${this.hass}
.placeholder=${this.hass!.localize(
"ui.panel.lovelace.editor.card.statistics-graph.pick_statistic"

View File

@@ -80,7 +80,6 @@ export class HuiEntitiesCardRowEditor extends LitElement {
`
: html`
<ha-entity-picker
allow-custom-entity
hide-clear-icon
.hass=${this.hass}
.value=${(entityConf as EntityConfig).entity}

View File

@@ -36,7 +36,6 @@ export class HuiHomeDashboardStrategyEditor
"ui.panel.lovelace.editor.strategy.home.add_favorite_entity"
)}
reorder
allow-custom-entity
@value-changed=${this._valueChanged}
>
</ha-entities-picker>

View File

@@ -672,7 +672,8 @@
"device_missing": "No related device"
},
"add": "Add",
"custom_name": "Custom name"
"custom_name": "Custom name",
"no_match": "No entities found"
},
"entity-attribute-picker": {
"attribute": "Attribute",
@@ -1308,7 +1309,9 @@
"combo-box": {
"no_match": "No matching items found",
"no_items": "No items available",
"unknown_item": "Unknown item"
"unknown_item": "Unknown item",
"search_or_custom": "Search | Add custom item",
"add_custom_item": "Add custom item"
},
"suggest_with_ai": {
"label": "Suggest",
@@ -1317,6 +1320,9 @@
"suggesting_3": "Enchanting…",
"done": "Done!",
"error": "Fail!"
},
"navigation-picker": {
"add_custom_path": "Add custom path"
}
},
"dialogs": {

View File

@@ -9354,7 +9354,7 @@ __metadata:
typescript: "npm:5.9.3"
typescript-eslint: "npm:8.49.0"
ua-parser-js: "npm:2.0.7"
vite-tsconfig-paths: "npm:5.1.4"
vite-tsconfig-paths: "npm:6.0.1"
vitest: "npm:4.0.15"
vue: "npm:2.7.16"
vue2-daterange-picker: "npm:0.6.8"
@@ -14698,9 +14698,9 @@ __metadata:
languageName: node
linkType: hard
"vite-tsconfig-paths@npm:5.1.4":
version: 5.1.4
resolution: "vite-tsconfig-paths@npm:5.1.4"
"vite-tsconfig-paths@npm:6.0.1":
version: 6.0.1
resolution: "vite-tsconfig-paths@npm:6.0.1"
dependencies:
debug: "npm:^4.1.1"
globrex: "npm:^0.1.2"
@@ -14710,7 +14710,7 @@ __metadata:
peerDependenciesMeta:
vite:
optional: true
checksum: 10/b409dbd17829f560021a71dba3e473b9c06dcf5fdc9d630b72c1f787145ec478b38caff1be04868971ac8bdcbf0f5af45eeece23dbc9c59c54b901f867740ae0
checksum: 10/7ac6d17efee21be805ed62857269d4e41d939774a8cc698e77145b4312edf996a9536e7a3b37e25486ae77d36fc5ae011378058ff290be39da16e52a47e87da7
languageName: node
linkType: hard