Compare commits

..

4 Commits

Author SHA1 Message Date
Petar Petrov
bb36e4aa42 Exclude conditional elements 2025-12-18 17:05:54 +02:00
Petar Petrov
5c9e052030 improve typing 2025-12-18 12:18:51 +02:00
Petar Petrov
392a87f33a refactor 2025-12-18 09:15:16 +02:00
Petar Petrov
72cd243ee3 Picture elements position by click 2025-12-18 09:07:15 +02:00
21 changed files with 1520 additions and 1400 deletions

View File

@@ -1,6 +1,16 @@
// From https://github.com/epoberezkin/fast-deep-equal // From https://github.com/epoberezkin/fast-deep-equal
// MIT License - Copyright (c) 2017 Evgeny Poberezkin // MIT License - Copyright (c) 2017 Evgeny Poberezkin
export const deepEqual = (a: any, b: any): boolean => {
interface DeepEqualOptions {
/** Compare Symbol properties in addition to string keys */
compareSymbols?: boolean;
}
export const deepEqual = (
a: any,
b: any,
options?: DeepEqualOptions
): boolean => {
if (a === b) { if (a === b) {
return true; return true;
} }
@@ -18,7 +28,7 @@ export const deepEqual = (a: any, b: any): boolean => {
return false; return false;
} }
for (i = length; i-- !== 0; ) { for (i = length; i-- !== 0; ) {
if (!deepEqual(a[i], b[i])) { if (!deepEqual(a[i], b[i], options)) {
return false; return false;
} }
} }
@@ -35,7 +45,7 @@ export const deepEqual = (a: any, b: any): boolean => {
} }
} }
for (i of a.entries()) { for (i of a.entries()) {
if (!deepEqual(i[1], b.get(i[0]))) { if (!deepEqual(i[1], b.get(i[0]), options)) {
return false; return false;
} }
} }
@@ -93,11 +103,28 @@ export const deepEqual = (a: any, b: any): boolean => {
for (i = length; i-- !== 0; ) { for (i = length; i-- !== 0; ) {
const key = keys[i]; const key = keys[i];
if (!deepEqual(a[key], b[key])) { if (!deepEqual(a[key], b[key], options)) {
return false; return false;
} }
} }
// Compare Symbol properties if requested
if (options?.compareSymbols) {
const symbolsA = Object.getOwnPropertySymbols(a);
const symbolsB = Object.getOwnPropertySymbols(b);
if (symbolsA.length !== symbolsB.length) {
return false;
}
for (const sym of symbolsA) {
if (!Object.prototype.hasOwnProperty.call(b, sym)) {
return false;
}
if (!deepEqual(a[sym], b[sym], options)) {
return false;
}
}
}
return true; return true;
} }

View File

@@ -16,10 +16,8 @@ import { slugify } from "../../../common/string/slugify";
import { groupBy } from "../../../common/util/group-by"; import { groupBy } from "../../../common/util/group-by";
import { afterNextRender } from "../../../common/util/render-status"; import { afterNextRender } from "../../../common/util/render-status";
import "../../../components/ha-button"; import "../../../components/ha-button";
import "../../../components/ha-button-menu";
import "../../../components/ha-card"; 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-button";
import "../../../components/ha-icon-next"; import "../../../components/ha-icon-next";
import "../../../components/ha-list"; import "../../../components/ha-list";
@@ -228,23 +226,32 @@ class HaConfigAreaPage extends LitElement {
></ha-icon>` ></ha-icon>`
: nothing}${area.name}`} : nothing}${area.name}`}
> >
<ha-dropdown slot="toolbar-icon" @wa-select=${this._handleMenuAction}> <ha-button-menu slot="toolbar-icon">
<ha-icon-button <ha-icon-button
slot="trigger" slot="trigger"
.label=${this.hass.localize("ui.common.menu")} .label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical} .path=${mdiDotsVertical}
></ha-icon-button> ></ha-icon-button>
<ha-dropdown-item value="settings" .entry=${area}> <ha-list-item
<ha-svg-icon slot="icon" .path=${mdiPencil}></ha-svg-icon> graphic="icon"
.entry=${area}
@click=${this._showSettings}
>
${this.hass.localize("ui.panel.config.areas.edit_settings")} ${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-list-item
<ha-svg-icon slot="icon" .path=${mdiDelete}></ha-svg-icon> class="warning"
graphic="icon"
@click=${this._deleteConfirm}
>
${this.hass.localize("ui.panel.config.areas.editor.delete")} ${this.hass.localize("ui.panel.config.areas.editor.delete")}
</ha-dropdown-item> <ha-svg-icon class="warning" slot="graphic" .path=${mdiDelete}>
</ha-dropdown> </ha-svg-icon>
</ha-list-item>
</ha-button-menu>
<div class="container"> <div class="container">
<div class="column"> <div class="column">
@@ -606,20 +613,6 @@ class HaConfigAreaPage extends LitElement {
this._related = await findRelated(this.hass, "area", this.areaId); 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) { private _showSettings(ev: MouseEvent) {
const entry: AreaRegistryEntry = (ev.currentTarget! as any).entry; const entry: AreaRegistryEntry = (ev.currentTarget! as any).entry;
this._openDialog(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 { import {
mdiDelete, mdiDelete,
mdiDotsVertical, mdiDotsVertical,
@@ -24,12 +24,10 @@ import {
type AreasFloorHierarchy, type AreasFloorHierarchy,
} from "../../../common/areas/areas-floor-hierarchy"; } from "../../../common/areas/areas-floor-hierarchy";
import { formatListWithAnds } from "../../../common/string/format-list"; 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-fab";
import "../../../components/ha-floor-icon"; import "../../../components/ha-floor-icon";
import "../../../components/ha-icon-button"; import "../../../components/ha-icon-button";
import "../../../components/ha-list-item";
import "../../../components/ha-sortable"; import "../../../components/ha-sortable";
import type { HaSortableOptions } from "../../../components/ha-sortable"; import type { HaSortableOptions } from "../../../components/ha-sortable";
import "../../../components/ha-svg-icon"; import "../../../components/ha-svg-icon";
@@ -198,43 +196,44 @@ export class HaConfigAreasDashboard extends LitElement {
${floor.name} ${floor.name}
</h2> </h2>
<div class="actions"> <div class="actions">
<ha-dropdown <ha-button-menu
.floor=${floor} .floor=${floor}
@wa-select=${this._handleFloorAction} @action=${this._handleFloorAction}
> >
<ha-icon-button <ha-icon-button
slot="trigger" slot="trigger"
.path=${mdiDotsVertical} .path=${mdiDotsVertical}
></ha-icon-button> ></ha-icon-button>
<ha-dropdown-item value="reorder"> <ha-list-item graphic="icon"
<ha-svg-icon ><ha-svg-icon
slot="icon"
.path=${mdiSort} .path=${mdiSort}
></ha-svg-icon> slot="graphic"
${this.hass.localize( ></ha-svg-icon
>${this.hass.localize(
"ui.panel.config.areas.picker.reorder" "ui.panel.config.areas.picker.reorder"
)} )}</ha-list-item
</ha-dropdown-item> >
<wa-divider></wa-divider> <li divider role="separator"></li>
<ha-dropdown-item value="edit"> <ha-list-item graphic="icon"
<ha-svg-icon ><ha-svg-icon
slot="icon"
.path=${mdiPencil} .path=${mdiPencil}
></ha-svg-icon> slot="graphic"
${this.hass.localize( ></ha-svg-icon
>${this.hass.localize(
"ui.panel.config.areas.picker.floor.edit_floor" "ui.panel.config.areas.picker.floor.edit_floor"
)} )}</ha-list-item
</ha-dropdown-item> >
<ha-dropdown-item value="delete" variant="danger"> <ha-list-item class="warning" graphic="icon"
<ha-svg-icon ><ha-svg-icon
slot="icon" class="warning"
.path=${mdiDelete} .path=${mdiDelete}
></ha-svg-icon> slot="graphic"
${this.hass.localize( ></ha-svg-icon
>${this.hass.localize(
"ui.panel.config.areas.picker.floor.delete_floor" "ui.panel.config.areas.picker.floor.delete_floor"
)} )}</ha-list-item
</ha-dropdown-item> >
</ha-dropdown> </ha-button-menu>
</div> </div>
</div> </div>
<ha-sortable <ha-sortable
@@ -274,23 +273,23 @@ export class HaConfigAreasDashboard extends LitElement {
)} )}
</h2> </h2>
<div class="actions"> <div class="actions">
<ha-dropdown <ha-button-menu
@wa-select=${this._handleUnassignedAreasAction} @action=${this._handleUnassignedAreasAction}
> >
<ha-icon-button <ha-icon-button
slot="trigger" slot="trigger"
.path=${mdiDotsVertical} .path=${mdiDotsVertical}
></ha-icon-button> ></ha-icon-button>
<ha-dropdown-item value="reorder"> <ha-list-item graphic="icon"
<ha-svg-icon ><ha-svg-icon
slot="icon"
.path=${mdiSort} .path=${mdiSort}
></ha-svg-icon> slot="graphic"
${this.hass.localize( ></ha-svg-icon
>${this.hass.localize(
"ui.panel.config.areas.picker.reorder" "ui.panel.config.areas.picker.reorder"
)} )}</ha-list-item
</ha-dropdown-item> >
</ha-dropdown> </ha-button-menu>
</div> </div>
</div> </div>
<ha-sortable <ha-sortable
@@ -534,25 +533,23 @@ export class HaConfigAreasDashboard extends LitElement {
}, time); }, time);
} }
private _handleFloorAction(ev: CustomEvent) { private _handleFloorAction(ev: CustomEvent<ActionDetail>) {
const item = ev.detail.item as HaDropdownItem;
const floor = (ev.currentTarget as any).floor; const floor = (ev.currentTarget as any).floor;
switch (item.value) { switch (ev.detail.index) {
case "reorder": case 0:
this._showReorderDialog(); this._showReorderDialog();
break; break;
case "edit": case 1:
this._editFloor(floor); this._editFloor(floor);
break; break;
case "delete": case 2:
this._deleteFloor(floor); this._deleteFloor(floor);
break; break;
} }
} }
private _handleUnassignedAreasAction(ev: CustomEvent) { private _handleUnassignedAreasAction(ev: CustomEvent<ActionDetail>) {
const item = ev.detail.item as HaDropdownItem; if (ev.detail.index === 0) {
if (item.value === "reorder") {
this._showReorderDialog(); this._showReorderDialog();
} }
} }

View File

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

View File

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

View File

@@ -5,10 +5,8 @@ import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import { navigate } from "../../../../common/navigate"; import { navigate } from "../../../../common/navigate";
import "../../../../components/ha-alert"; import "../../../../components/ha-alert";
import "../../../../components/ha-button-menu";
import "../../../../components/ha-card"; 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-icon-next";
import "../../../../components/ha-list"; import "../../../../components/ha-list";
import "../../../../components/ha-list-item"; import "../../../../components/ha-list-item";
@@ -46,26 +44,26 @@ export class CloudLoginPanel extends LitElement {
.narrow=${this.narrow} .narrow=${this.narrow}
header="Home Assistant Cloud" 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 <ha-icon-button
slot="trigger" slot="trigger"
.label=${this.hass.localize("ui.common.menu")} .label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical} .path=${mdiDotsVertical}
></ha-icon-button> ></ha-icon-button>
<ha-dropdown-item value="reset"> <ha-list-item graphic="icon">
<ha-svg-icon slot="icon" .path=${mdiDeleteForever}></ha-svg-icon>
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.cloud.account.reset_cloud_data" "ui.panel.config.cloud.account.reset_cloud_data"
)} )}
</ha-dropdown-item> <ha-svg-icon slot="graphic" .path=${mdiDeleteForever}></ha-svg-icon>
<ha-dropdown-item value="download"> </ha-list-item>
<ha-svg-icon slot="icon" .path=${mdiDownload}></ha-svg-icon> <ha-list-item graphic="icon">
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.cloud.account.download_support_package" "ui.panel.config.cloud.account.download_support_package"
)} )}
</ha-dropdown-item> <ha-svg-icon slot="graphic" .path=${mdiDownload}></ha-svg-icon>
</ha-dropdown> </ha-list-item>
</ha-button-menu>
<div class="content"> <div class="content">
<ha-config-section .isWide=${this.isWide}> <ha-config-section .isWide=${this.isWide}>
<span slot="header">Home Assistant Cloud</span> <span slot="header">Home Assistant Cloud</span>
@@ -166,15 +164,13 @@ export class CloudLoginPanel extends LitElement {
fireEvent(this, "flash-message-changed", { value: "" }); fireEvent(this, "flash-message-changed", { value: "" });
} }
private _handleMenuAction(ev: CustomEvent) { private _handleMenuAction(ev) {
const item = ev.detail.item as HaDropdownItem; switch (ev.detail.index) {
switch (item.value) { case 0:
case "reset":
this._deleteCloudData(); this._deleteCloudData();
break; break;
case "download": case 1:
this._downloadSupportPackage(); 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 { mdiDotsVertical, mdiRefresh } from "@mdi/js";
import type { HassEntities } from "home-assistant-js-websocket"; import type { HassEntities } from "home-assistant-js-websocket";
import type { TemplateResult } from "lit"; import type { TemplateResult } from "lit";
@@ -6,12 +6,13 @@ import { LitElement, css, html } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event";
import "../../../components/ha-alert"; import "../../../components/ha-alert";
import "../../../components/ha-bar"; import "../../../components/ha-bar";
import "../../../components/ha-button-menu";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../../../components/ha-dropdown"; import "../../../components/ha-check-list-item";
import "../../../components/ha-dropdown-item"; import "../../../components/ha-list-item";
import type { HaDropdownItem } from "../../../components/ha-dropdown-item";
import "../../../components/ha-metric"; import "../../../components/ha-metric";
import { extractApiErrorMessage } from "../../../data/hassio/common"; import { extractApiErrorMessage } from "../../../data/hassio/common";
import type { import type {
@@ -72,24 +73,24 @@ class HaConfigSectionUpdates extends LitElement {
.path=${mdiRefresh} .path=${mdiRefresh}
@click=${this._checkUpdates} @click=${this._checkUpdates}
></ha-icon-button> ></ha-icon-button>
<ha-dropdown @wa-select=${this._handleMenuAction}> <ha-button-menu multi>
<ha-icon-button <ha-icon-button
slot="trigger" slot="trigger"
.label=${this.hass.localize("ui.common.menu")} .label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical} .path=${mdiDotsVertical}
></ha-icon-button> ></ha-icon-button>
<ha-dropdown-item <ha-check-list-item
type="checkbox" left
.checked=${this._showSkipped} @request-selected=${this._toggleSkipped}
value="toggle_skipped" .selected=${this._showSkipped}
> >
${this.hass.localize("ui.panel.config.updates.show_skipped")} ${this.hass.localize("ui.panel.config.updates.show_skipped")}
</ha-dropdown-item> </ha-check-list-item>
${this._supervisorInfo ${this._supervisorInfo
? html` ? html`
<wa-divider></wa-divider> <li divider role="separator"></li>
<ha-dropdown-item <ha-list-item
value="toggle_beta" @request-selected=${this._toggleBeta}
.disabled=${this._supervisorInfo.channel === "dev"} .disabled=${this._supervisorInfo.channel === "dev"}
> >
${this._supervisorInfo.channel === "stable" ${this._supervisorInfo.channel === "stable"
@@ -97,10 +98,10 @@ class HaConfigSectionUpdates extends LitElement {
: this.hass.localize( : this.hass.localize(
"ui.panel.config.updates.leave_beta" "ui.panel.config.updates.leave_beta"
)} )}
</ha-dropdown-item> </ha-list-item>
` `
: ""} : ""}
</ha-dropdown> </ha-button-menu>
</div> </div>
<div class="content"> <div class="content">
<ha-card outlined> <ha-card outlined>
@@ -132,21 +133,27 @@ class HaConfigSectionUpdates extends LitElement {
this._supervisorInfo = await fetchHassioSupervisorInfo(this.hass); this._supervisorInfo = await fetchHassioSupervisorInfo(this.hass);
} }
private _handleMenuAction(ev: CustomEvent): void { private _toggleSkipped(ev: CustomEvent<RequestSelectedDetail>): void {
const item = ev.detail.item as HaDropdownItem; if (ev.detail.source !== "property") {
switch (item.value) { return;
case "toggle_skipped": }
this._showSkipped = !this._showSkipped;
break; this._showSkipped = !this._showSkipped;
case "toggle_beta": }
if (this._supervisorInfo!.channel === "stable") {
showJoinBetaDialog(this, { private async _toggleBeta(
join: async () => this._setChannel("beta"), ev: CustomEvent<RequestSelectedDetail>
}); ): Promise<void> {
} else { if (!shouldHandleRequestSelectedEvent(ev)) {
this._setChannel("stable"); return;
} }
break;
if (this._supervisorInfo!.channel === "stable") {
showJoinBetaDialog(this, {
join: async () => this._setChannel("beta"),
});
} else {
this._setChannel("stable");
} }
} }

View File

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

View File

@@ -1,6 +1,7 @@
import { consume } from "@lit/context"; import { consume } from "@lit/context";
import { import {
mdiCancel, mdiCancel,
mdiChevronRight,
mdiDelete, mdiDelete,
mdiDotsVertical, mdiDotsVertical,
mdiMenuDown, mdiMenuDown,
@@ -38,15 +39,11 @@ import type {
SortingChangedEvent, SortingChangedEvent,
} from "../../../components/data-table/ha-data-table"; } 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/data-table/ha-data-table-labels";
import "../../../components/entity/ha-battery-icon"; import "../../../components/entity/ha-battery-icon";
import "../../../components/ha-alert"; import "../../../components/ha-alert";
import "../../../components/ha-button-menu"; import "../../../components/ha-button-menu";
import "../../../components/ha-check-list-item"; 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-fab";
import "../../../components/ha-filter-devices"; import "../../../components/ha-filter-devices";
import "../../../components/ha-filter-floor-areas"; import "../../../components/ha-filter-floor-areas";
@@ -54,6 +51,9 @@ import "../../../components/ha-filter-integrations";
import "../../../components/ha-filter-labels"; import "../../../components/ha-filter-labels";
import "../../../components/ha-filter-states"; import "../../../components/ha-filter-states";
import "../../../components/ha-icon-button"; 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 { createAreaRegistryEntry } from "../../../data/area_registry";
import type { ConfigEntry, SubEntry } from "../../../data/config_entries"; import type { ConfigEntry, SubEntry } from "../../../data/config_entries";
import { getSubEntries, sortConfigEntries } 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 { protected render(): TemplateResult {
const { devicesOutput } = this._devicesAndFilterDomains( const { devicesOutput } = this._devicesAndFilterDomains(
this.hass.devices, this.hass.devices,
@@ -792,6 +718,77 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
(this._sizeController.value && this._sizeController.value < 700) || (this._sizeController.value && this._sizeController.value < 700) ||
(!this._sizeController.value && this.hass.dockedSidebar === "docked"); (!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` return html`
<hass-tabs-subpage-data-table <hass-tabs-subpage-data-table
.hass=${this.hass} .hass=${this.hass}
@@ -909,7 +906,7 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
></ha-filter-labels> ></ha-filter-labels>
${!this.narrow ${!this.narrow
? html`<ha-dropdown slot="selection-bar"> ? html`<ha-md-button-menu slot="selection-bar">
<ha-assist-chip <ha-assist-chip
slot="trigger" slot="trigger"
.label=${this.hass.localize( .label=${this.hass.localize(
@@ -921,15 +918,12 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
.path=${mdiMenuDown} .path=${mdiMenuDown}
></ha-svg-icon> ></ha-svg-icon>
</ha-assist-chip> </ha-assist-chip>
${this._renderLabelItems()} ${labelItems}
</ha-dropdown> </ha-md-button-menu>
${areasInOverflow ${areasInOverflow
? nothing ? nothing
: html`<ha-dropdown : html`<ha-md-button-menu slot="selection-bar">
slot="selection-bar"
@wa-select=${this._handleOverflowMenuSelect}
>
<ha-assist-chip <ha-assist-chip
slot="trigger" slot="trigger"
.label=${this.hass.localize( .label=${this.hass.localize(
@@ -941,14 +935,10 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
.path=${mdiMenuDown} .path=${mdiMenuDown}
></ha-svg-icon> ></ha-svg-icon>
</ha-assist-chip> </ha-assist-chip>
${this._renderAreaItems()} ${areaItems}
</ha-dropdown>`}` </ha-md-button-menu>`}`
: nothing} : nothing}
<ha-dropdown <ha-md-button-menu has-overflow slot="selection-bar">
has-overflow
slot="selection-bar"
@wa-select=${this._handleOverflowMenuSelect}
>
${this.narrow ${this.narrow
? html`<ha-assist-chip ? html`<ha-assist-chip
.label=${this.hass.localize( .label=${this.hass.localize(
@@ -969,32 +959,51 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
slot="trigger" slot="trigger"
></ha-icon-button>`} ></ha-icon-button>`}
${this.narrow ${this.narrow
? html`<ha-dropdown-item> ? html` <ha-sub-menu>
${this.hass.localize( <ha-md-menu-item slot="item">
"ui.panel.config.automation.picker.bulk_actions.add_label" <div slot="headline">
)} ${this.hass.localize(
${this._renderLabelItems(true)} "ui.panel.config.automation.picker.bulk_actions.add_label"
</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} : nothing}
${areasInOverflow ${areasInOverflow
? html`<ha-dropdown-item> ? html`<ha-sub-menu>
${this.hass.localize( <ha-md-menu-item slot="item">
"ui.panel.config.devices.picker.bulk_actions.move_area" <div slot="headline">
)} ${this.hass.localize(
${this._renderAreaItems(true)} "ui.panel.config.devices.picker.bulk_actions.move_area"
</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} : nothing}
<ha-dropdown-item <ha-md-menu-item
value="delete" .clickAction=${this._deleteSelected}
.disabled=${!this._selectedCanDelete.length} .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>
${this.hass.localize( <div slot="headline">
"ui.panel.config.devices.picker.bulk_actions.delete_selected.button" ${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> </hass-tabs-subpage-data-table>
`; `;
} }
@@ -1084,28 +1093,12 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
this._selected = ev.detail.value; this._selected = ev.detail.value;
} }
private _handleOverflowMenuSelect(ev: CustomEvent) { private _handleBulkArea = (item) => {
const item = ev.detail.item as HaDropdownItem; const area = item.value;
this._bulkAddArea(area);
};
switch (item.value) { private async _bulkAddArea(area: string) {
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) {
const promises: Promise<DeviceRegistryEntry>[] = []; const promises: Promise<DeviceRegistryEntry>[] = [];
this._selected.forEach((deviceId) => { this._selected.forEach((deviceId) => {
promises.push( 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") { private async _bulkLabel(label: string, action: "add" | "remove") {
const promises: Promise<DeviceRegistryEntry>[] = []; const promises: Promise<DeviceRegistryEntry>[] = [];
this._selected.forEach((deviceId) => { 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 = () => { private _deleteSelected = () => {
showConfirmationDialog(this, { showConfirmationDialog(this, {
title: this.hass.localize( title: this.hass.localize(
@@ -1285,7 +1277,7 @@ ${rejected
ha-assist-chip { ha-assist-chip {
--ha-assist-chip-container-shape: 10px; --ha-assist-chip-container-shape: 10px;
} }
ha-dropdown ha-assist-chip { ha-md-button-menu ha-assist-chip {
--md-assist-chip-trailing-space: 8px; --md-assist-chip-trailing-space: 8px;
} }
ha-label { ha-label {

View File

@@ -1,8 +1,8 @@
import "@home-assistant/webawesome/dist/components/divider/divider";
import { consume } from "@lit/context"; import { consume } from "@lit/context";
import { import {
mdiAlertCircle, mdiAlertCircle,
mdiCancel, mdiCancel,
mdiChevronRight,
mdiDelete, mdiDelete,
mdiDotsVertical, mdiDotsVertical,
mdiEye, mdiEye,
@@ -56,9 +56,8 @@ import type {
} from "../../../components/data-table/ha-data-table"; } from "../../../components/data-table/ha-data-table";
import "../../../components/data-table/ha-data-table-labels"; import "../../../components/data-table/ha-data-table-labels";
import "../../../components/ha-alert"; import "../../../components/ha-alert";
import "../../../components/ha-dropdown"; import "../../../components/ha-button-menu";
import "../../../components/ha-dropdown-item"; import "../../../components/ha-check-list-item";
import type { HaDropdownItem } from "../../../components/ha-dropdown-item";
import "../../../components/ha-filter-devices"; import "../../../components/ha-filter-devices";
import "../../../components/ha-filter-domains"; import "../../../components/ha-filter-domains";
import "../../../components/ha-filter-floor-areas"; import "../../../components/ha-filter-floor-areas";
@@ -67,6 +66,9 @@ import "../../../components/ha-filter-labels";
import "../../../components/ha-filter-states"; import "../../../components/ha-filter-states";
import "../../../components/ha-icon"; import "../../../components/ha-icon";
import "../../../components/ha-icon-button"; 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-svg-icon";
import "../../../components/ha-tooltip"; import "../../../components/ha-tooltip";
import type { ConfigEntry, SubEntry } from "../../../data/config_entries"; 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() { protected render() {
if (!this.hass || this._entities === undefined) { if (!this.hass || this._entities === undefined) {
return html` <hass-loading-screen></hass-loading-screen> `; return html` <hass-loading-screen></hass-loading-screen> `;
@@ -813,13 +773,53 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
[...filteredDomains][0] [...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` return html`
<hass-tabs-subpage-data-table <hass-tabs-subpage-data-table
.hass=${this.hass} .hass=${this.hass}
.narrow=${this.narrow} .narrow=${this.narrow}
.backPath=${this._searchParms.has("historyBack") .backPath=${
? undefined this._searchParms.has("historyBack") ? undefined : "/config"
: "/config"} }
.route=${this.route} .route=${this.route}
.tabs=${configSections.devices} .tabs=${configSections.devices}
.columns=${this._columns(this.hass.localize)} .columns=${this._columns(this.hass.localize)}
@@ -829,14 +829,16 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
{ number: filteredEntities.length } { number: filteredEntities.length }
)} )}
has-filters has-filters
.filters=${Object.values(this._filters).filter((filter) => .filters=${
Array.isArray(filter) Object.values(this._filters).filter((filter) =>
? filter.length Array.isArray(filter)
: filter && ? filter.length
Object.values(filter).some((val) => : filter &&
Array.isArray(val) ? val.length : val Object.values(filter).some((val) =>
) Array.isArray(val) ? val.length : val
).length} )
).length
}
selectable selectable
.selected=${this._selected.length} .selected=${this._selected.length}
.initialGroupColumn=${this._activeGrouping ?? "device_full"} .initialGroupColumn=${this._activeGrouping ?? "device_full"}
@@ -863,120 +865,157 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
slot="toolbar-icon" slot="toolbar-icon"
></ha-integration-overflow-menu> ></ha-integration-overflow-menu>
${!this.narrow
? html`<ha-dropdown slot="selection-bar"> ${
<ha-assist-chip !this.narrow
slot="trigger" ? html`<ha-md-button-menu slot="selection-bar">
.label=${this.hass.localize( <ha-assist-chip
"ui.panel.config.automation.picker.bulk_actions.add_label" 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-assist-chip>
${this._renderLabelItems()}
</ha-dropdown>`
: nothing}
<ha-dropdown
slot="selection-bar"
@wa-select=${this._handleOverflowMenuSelect}
> >
${this.narrow <ha-svg-icon slot="trailing-icon" .path=${mdiMenuDown}></ha-svg-icon>
? html`<ha-assist-chip </ha-assist-chip>
.label=${this.hass.localize( ${labelItems}
"ui.panel.config.automation.picker.bulk_action" </ha-md-button-menu>`
)} : nothing
slot="trigger" }
> <ha-md-button-menu has-overflow slot="selection-bar">
<ha-svg-icon ${
slot="trailing-icon" this.narrow
.path=${mdiMenuDown} ? html`<ha-assist-chip
></ha-svg-icon> .label=${this.hass.localize(
</ha-assist-chip>` "ui.panel.config.automation.picker.bulk_action"
: html`<ha-icon-button )}
.path=${mdiDotsVertical} slot="trigger"
.label=${this.hass.localize( >
"ui.panel.config.automation.picker.bulk_action" <ha-svg-icon slot="trailing-icon" .path=${mdiMenuDown}></ha-svg-icon>
)} </ha-assist-chip>`
slot="trigger" : html`<ha-icon-button
></ha-icon-button>`} .path=${mdiDotsVertical}
${this.narrow .label=${this.hass.localize(
? html`<ha-dropdown-item> "ui.panel.config.automation.picker.bulk_action"
)}
slot="trigger"
></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( ${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.add_label" "ui.panel.config.automation.picker.bulk_actions.add_label"
)} )}
${this._renderLabelItems(true)} </div>
</ha-dropdown-item>` <ha-svg-icon slot="end" .path=${mdiChevronRight}></ha-svg-icon>
: nothing} </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-md-menu-item .clickAction=${this._enableSelected}>
<ha-svg-icon slot="icon" .path=${mdiToggleSwitch}></ha-svg-icon> <ha-svg-icon slot="start" .path=${mdiToggleSwitch}></ha-svg-icon>
${this.hass.localize( <div slot="headline">
"ui.panel.config.entities.picker.enable_selected.button" ${this.hass.localize(
)} "ui.panel.config.entities.picker.enable_selected.button"
</ha-dropdown-item> )}
<ha-dropdown-item value="disable"> </div>
<ha-svg-icon </ha-md-menu-item>
slot="icon" <ha-md-menu-item .clickAction=${this._disableSelected}>
.path=${mdiToggleSwitchOffOutline} <ha-svg-icon
></ha-svg-icon> slot="start"
${this.hass.localize( .path=${mdiToggleSwitchOffOutline}
"ui.panel.config.entities.picker.disable_selected.button" ></ha-svg-icon>
)} <div slot="headline">
</ha-dropdown-item> ${this.hass.localize(
<wa-divider></wa-divider> "ui.panel.config.entities.picker.disable_selected.button"
)}
</div>
</ha-md-menu-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-dropdown-item value="unhide"> <ha-md-menu-item .clickAction=${this._unhideSelected}>
<ha-svg-icon slot="icon" .path=${mdiEye}></ha-svg-icon> <ha-svg-icon
${this.hass.localize( slot="start"
"ui.panel.config.entities.picker.unhide_selected.button" .path=${mdiEye}
)} ></ha-svg-icon>
</ha-dropdown-item> <div slot="headline">
<ha-dropdown-item value="hide"> ${this.hass.localize(
<ha-svg-icon slot="icon" .path=${mdiEyeOff}></ha-svg-icon> "ui.panel.config.entities.picker.unhide_selected.button"
${this.hass.localize( )}
"ui.panel.config.entities.picker.hide_selected.button" </div>
)} </ha-md-menu-item>
</ha-dropdown-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"
)}
</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-md-menu-item .clickAction=${this._restoreEntityIdSelected}>
<ha-svg-icon slot="icon" .path=${mdiRestore}></ha-svg-icon> <ha-svg-icon
${this.hass.localize( slot="start"
"ui.panel.config.entities.picker.restore_entity_id_selected.button" .path=${mdiRestore}
)} ></ha-svg-icon>
</ha-dropdown-item> <div slot="headline">
${this.hass.localize(
"ui.panel.config.entities.picker.restore_entity_id_selected.button"
)}
</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-md-menu-item .clickAction=${this._removeSelected} class="warning">
<ha-svg-icon slot="icon" .path=${mdiDelete}></ha-svg-icon> <ha-svg-icon
${this.hass.localize( slot="start"
"ui.panel.config.entities.picker.delete_selected.button" .path=${mdiDelete}
)} ></ha-svg-icon>
</ha-dropdown-item> <div slot="headline">
</ha-dropdown> ${this.hass.localize(
${Array.isArray(this._filters.config_entry) && "ui.panel.config.entities.picker.delete_selected.button"
this._filters.config_entry.length )}
? html`<ha-alert slot="filter-pane"> </div>
${this.hass.localize( </ha-md-menu-item>
"ui.panel.config.entities.picker.filtering_by_config_entry"
)} </ha-md-button-menu>
${this._entries?.find( ${
(entry) => entry.entry_id === this._filters.config_entry![0] Array.isArray(this._filters.config_entry) &&
)?.title || this._filters.config_entry[0]}${this._filters this._filters.config_entry.length
.config_entry.length === 1 && ? html`<ha-alert slot="filter-pane">
Array.isArray(this._filters.sub_entry) && ${this.hass.localize(
this._filters.sub_entry.length "ui.panel.config.entities.picker.filtering_by_config_entry"
? html` (${this._subEntries?.find( )}
(entry) => entry.subentry_id === this._filters.sub_entry![0] ${this._entries?.find(
)?.title || this._filters.sub_entry[0]})` (entry) => entry.entry_id === this._filters.config_entry![0]
: nothing} )?.title || this._filters.config_entry[0]}${this._filters
</ha-alert>` .config_entry.length === 1 &&
: nothing} Array.isArray(this._filters.sub_entry) &&
this._filters.sub_entry.length
? html` (${this._subEntries?.find(
(entry) =>
entry.subentry_id === this._filters.sub_entry![0]
)?.title || this._filters.sub_entry[0]})`
: nothing}
</ha-alert>`
: nothing
}
<ha-filter-floor-areas <ha-filter-floor-areas
.hass=${this.hass} .hass=${this.hass}
type="entity" type="entity"
@@ -1037,16 +1076,20 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
.narrow=${this.narrow} .narrow=${this.narrow}
@expanded-changed=${this._filterExpanded} @expanded-changed=${this._filterExpanded}
></ha-filter-labels> ></ha-filter-labels>
${includeAddDeviceFab ${
? html`<ha-fab includeAddDeviceFab
.label=${this.hass.localize("ui.panel.config.devices.add_device")} ? html`<ha-fab
extended .label=${this.hass.localize(
@click=${this._addDevice} "ui.panel.config.devices.add_device"
slot="fab" )}
> extended
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> @click=${this._addDevice}
</ha-fab>` slot="fab"
: nothing} >
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</ha-fab>`
: nothing
}
</hass-tabs-subpage-data-table> </hass-tabs-subpage-data-table>
`; `;
} }
@@ -1180,44 +1223,6 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
this._selected = ev.detail.value; 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 () => { private _enableSelected = async () => {
showConfirmationDialog(this, { showConfirmationDialog(this, {
title: this.hass.localize( title: this.hass.localize(
@@ -1341,6 +1346,12 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
this._clearSelection(); 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") { private async _bulkLabel(label: string, action: "add" | "remove") {
const promises: Promise<UpdateEntityRegistryEntryResult>[] = []; const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
this._selected.forEach((entityId) => { this._selected.forEach((entityId) => {
@@ -1602,7 +1613,7 @@ ${rejected
ha-assist-chip { ha-assist-chip {
--ha-assist-chip-container-shape: 10px; --ha-assist-chip-container-shape: 10px;
} }
ha-dropdown ha-assist-chip { ha-md-button-menu ha-assist-chip {
--md-assist-chip-trailing-space: 8px; --md-assist-chip-trailing-space: 8px;
} }
ha-label { 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 { ResizeController } from "@lit-labs/observers/resize-controller";
import { consume } from "@lit/context"; import { consume } from "@lit/context";
import { import {
mdiAlertCircle, mdiAlertCircle,
mdiCancel, mdiCancel,
mdiChevronRight,
mdiCog, mdiCog,
mdiDelete, mdiDelete,
mdiDotsVertical, mdiDotsVertical,
@@ -45,9 +45,6 @@ import type {
SortingChangedEvent, SortingChangedEvent,
} from "../../../components/data-table/ha-data-table"; } from "../../../components/data-table/ha-data-table";
import "../../../components/data-table/ha-data-table-labels"; 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-fab";
import "../../../components/ha-filter-categories"; import "../../../components/ha-filter-categories";
import "../../../components/ha-filter-devices"; import "../../../components/ha-filter-devices";
@@ -56,9 +53,8 @@ import "../../../components/ha-filter-floor-areas";
import "../../../components/ha-filter-labels"; import "../../../components/ha-filter-labels";
import "../../../components/ha-icon"; import "../../../components/ha-icon";
import "../../../components/ha-icon-overflow-menu"; import "../../../components/ha-icon-overflow-menu";
import "../../../components/ha-md-menu"; import "../../../components/ha-md-divider";
import "../../../components/ha-state-icon"; import "../../../components/ha-state-icon";
import "../../../components/ha-sub-menu";
import "../../../components/ha-svg-icon"; import "../../../components/ha-svg-icon";
import "../../../components/ha-tooltip"; import "../../../components/ha-tooltip";
import { getSignedPath } from "../../../data/auth"; import { getSignedPath } from "../../../data/auth";
@@ -611,35 +607,42 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
); );
} }
private _renderCategoryItems = (submenu = false) => protected render(): TemplateResult {
html`${this._categories?.map( 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) => (category) =>
html`<ha-dropdown-item html`<ha-md-menu-item
.slot=${submenu ? "submenu" : ""} .value=${category.category_id}
value="move_category" .clickAction=${this._handleBulkCategory}
data-category=${category.category_id}
> >
${category.icon ${category.icon
? html`<ha-icon slot="icon" .icon=${category.icon}></ha-icon>` ? html`<ha-icon slot="start" .icon=${category.icon}></ha-icon>`
: html`<ha-svg-icon slot="icon" .path=${mdiTag}></ha-svg-icon>`} : html`<ha-svg-icon slot="start" .path=${mdiTag}></ha-svg-icon>`}
${category.name} <div slot="headline">${category.name}</div>
</ha-dropdown-item>` </ha-md-menu-item>`
)} )}
<ha-dropdown-item .slot=${submenu ? "submenu" : ""} value="no_category"> <ha-md-menu-item .value=${null} .clickAction=${this._handleBulkCategory}>
${this.hass.localize( <div slot="headline">
"ui.panel.config.automation.picker.bulk_actions.no_category" ${this.hass.localize(
)} "ui.panel.config.automation.picker.bulk_actions.no_category"
</ha-dropdown-item> )}
<wa-divider .slot=${submenu ? "submenu" : ""}></wa-divider> </div>
<ha-dropdown-item </ha-md-menu-item>
.slot=${submenu ? "submenu" : ""} <ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
value="create_category" <ha-md-menu-item .clickAction=${this._bulkCreateCategory}>
> <div slot="headline">
${this.hass.localize("ui.panel.config.category.editor.add")} ${this.hass.localize("ui.panel.config.category.editor.add")}
</ha-dropdown-item>`; </div>
</ha-md-menu-item>`;
private _renderLabelItems = (submenu = false) => const labelItems = html`${this._labels?.map((label) => {
html`${this._labels?.map((label) => {
const color = label.color ? computeCssColor(label.color) : undefined; const color = label.color ? computeCssColor(label.color) : undefined;
const selected = this._selected.every((entityId) => const selected = this._selected.every((entityId) =>
this._labelsForEntity(entityId).includes(label.label_id) this._labelsForEntity(entityId).includes(label.label_id)
@@ -649,14 +652,14 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
this._selected.some((entityId) => this._selected.some((entityId) =>
this._labelsForEntity(entityId).includes(label.label_id) this._labelsForEntity(entityId).includes(label.label_id)
); );
return html`<ha-dropdown-item return html`<ha-md-menu-item
@click=${this._handleLabelMenuSelect}
.slot=${submenu ? "submenu" : ""}
.value=${label.label_id} .value=${label.label_id}
data-action=${selected ? "remove" : "add"} .action=${selected ? "remove" : "add"}
@click=${this._handleBulkLabel}
keep-open
> >
<ha-checkbox <ha-checkbox
slot="icon" slot="start"
.checked=${selected} .checked=${selected}
.indeterminate=${partial} .indeterminate=${partial}
reducedTouchTarget reducedTouchTarget
@@ -670,25 +673,13 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
: nothing} : nothing}
${label.name} ${label.name}
</ha-label> </ha-label>
</ha-dropdown-item>`; </ha-md-menu-item> `;
})}<wa-divider .slot=${submenu ? "submenu" : ""}></wa-divider> })}<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-dropdown-item <ha-md-menu-item .clickAction=${this._bulkCreateLabel}>
@click=${this._bulkCreateLabel} <div slot="headline">
.slot=${submenu ? "submenu" : ""} ${this.hass.localize("ui.panel.config.labels.add_label")}
> </div>
${this.hass.localize("ui.panel.config.labels.add_label")} </ha-md-menu-item>`;
</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>`;
}
const labelsInOverflow = const labelsInOverflow =
(this._sizeController.value && this._sizeController.value < 700) || (this._sizeController.value && this._sizeController.value < 700) ||
(!this._sizeController.value && this.hass.dockedSidebar === "docked"); (!this._sizeController.value && this.hass.dockedSidebar === "docked");
@@ -790,10 +781,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
></ha-filter-categories> ></ha-filter-categories>
${!this.narrow ${!this.narrow
? html`<ha-dropdown ? html`<ha-md-button-menu slot="selection-bar">
slot="selection-bar"
@wa-select=${this._handleMenuSelect}
>
<ha-assist-chip <ha-assist-chip
slot="trigger" slot="trigger"
.label=${this.hass.localize( .label=${this.hass.localize(
@@ -805,11 +793,11 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
.path=${mdiMenuDown} .path=${mdiMenuDown}
></ha-svg-icon> ></ha-svg-icon>
</ha-assist-chip> </ha-assist-chip>
${this._renderCategoryItems()} ${categoryItems}
</ha-dropdown> </ha-md-button-menu>
${labelsInOverflow ${labelsInOverflow
? nothing ? nothing
: html`<ha-dropdown slot="selection-bar"> : html`<ha-md-button-menu slot="selection-bar">
<ha-assist-chip <ha-assist-chip
slot="trigger" slot="trigger"
.label=${this.hass.localize( .label=${this.hass.localize(
@@ -821,15 +809,14 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
.path=${mdiMenuDown} .path=${mdiMenuDown}
></ha-svg-icon> ></ha-svg-icon>
</ha-assist-chip> </ha-assist-chip>
${this._renderLabelItems()} ${labelItems}
</ha-dropdown>`}` </ha-md-button-menu>`}`
: nothing} : nothing}
${this.narrow || labelsInOverflow ${this.narrow || labelsInOverflow
? html`<ha-dropdown ? html`
slot="selection-bar" <ha-md-button-menu has-overflow slot="selection-bar">
@wa-select=${this._handleMenuSelect} ${
> this.narrow
${this.narrow
? html`<ha-assist-chip ? html`<ha-assist-chip
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.automation.picker.bulk_action" "ui.panel.config.automation.picker.bulk_action"
@@ -847,24 +834,50 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
"ui.panel.config.automation.picker.bulk_action" "ui.panel.config.automation.picker.bulk_action"
)} )}
slot="trigger" slot="trigger"
></ha-icon-button>`} ></ha-icon-button>`
${this.narrow }
? html`<ha-dropdown-item> <ha-svg-icon
${this.hass.localize( slot="trailing-icon"
"ui.panel.config.automation.picker.bulk_actions.move_category" .path=${mdiMenuDown}
)} ></ha-svg-icon
${this._renderCategoryItems(true)} ></ha-assist-chip>
</ha-dropdown-item>` ${
: nothing} this.narrow
${this.narrow || this.hass.dockedSidebar === "docked" ? html`<ha-sub-menu>
? html`<ha-dropdown-item> <ha-md-menu-item slot="item">
${this.hass.localize( <div slot="headline">
"ui.panel.config.automation.picker.bulk_actions.add_label" ${this.hass.localize(
)} "ui.panel.config.automation.picker.bulk_actions.move_category"
${this._renderLabelItems(true)} )}
</ha-dropdown-item>` </div>
: nothing} <ha-svg-icon
</ha-dropdown>` 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"
)}
</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} : nothing}
<ha-integration-overflow-menu <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>[] = []; const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
this._selected.forEach((entityId) => { this._selected.forEach((entityId) => {
promises.push( 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") { private async _bulkLabel(label: string, action: "add" | "remove") {
const promises: Promise<UpdateEntityRegistryEntryResult>[] = []; const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
this._selected.forEach((entityId) => { this._selected.forEach((entityId) => {
@@ -1067,32 +1091,6 @@ ${rejected
this._selected = ev.detail.value; 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) { protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps); super.firstUpdated(changedProps);
@@ -1433,7 +1431,7 @@ ${rejected
ha-assist-chip { ha-assist-chip {
--ha-assist-chip-container-shape: 10px; --ha-assist-chip-container-shape: 10px;
} }
ha-dropdown ha-assist-chip { ha-md-button-menu ha-assist-chip {
--md-assist-chip-trailing-space: 8px; --md-assist-chip-trailing-space: 8px;
} }
ha-label { ha-label {

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,7 +11,10 @@ import { findEntities } from "../common/find-entities";
import type { LovelaceElement, LovelaceElementConfig } from "../elements/types"; import type { LovelaceElement, LovelaceElementConfig } from "../elements/types";
import type { LovelaceCard, LovelaceCardEditor } from "../types"; import type { LovelaceCard, LovelaceCardEditor } from "../types";
import { createStyledHuiElement } from "./picture-elements/create-styled-hui-element"; import { createStyledHuiElement } from "./picture-elements/create-styled-hui-element";
import type { PictureElementsCardConfig } from "./types"; import {
PREVIEW_CLICK_CALLBACK,
type PictureElementsCardConfig,
} from "./types";
import type { PersonEntity } from "../../../data/person"; import type { PersonEntity } from "../../../data/person";
@customElement("hui-picture-elements-card") @customElement("hui-picture-elements-card")
@@ -166,6 +169,7 @@ class HuiPictureElementsCard extends LitElement implements LovelaceCard {
.aspectRatio=${this._config.aspect_ratio} .aspectRatio=${this._config.aspect_ratio}
.darkModeFilter=${this._config.dark_mode_filter} .darkModeFilter=${this._config.dark_mode_filter}
.darkModeImage=${darkModeImage} .darkModeImage=${darkModeImage}
@click=${this._handleImageClick}
></hui-image> ></hui-image>
${this._elements} ${this._elements}
</div> </div>
@@ -221,6 +225,19 @@ class HuiPictureElementsCard extends LitElement implements LovelaceCard {
curCardEl === elToReplace ? newCardEl : curCardEl curCardEl === elToReplace ? newCardEl : curCardEl
); );
} }
private _handleImageClick(ev: MouseEvent): void {
if (!this.preview || !this._config?.[PREVIEW_CLICK_CALLBACK]) {
return;
}
const rect = (ev.currentTarget as HTMLElement).getBoundingClientRect();
const x = ((ev.clientX - rect.left) / rect.width) * 100;
const y = ((ev.clientY - rect.top) / rect.height) * 100;
// only the edited card has this callback
this._config[PREVIEW_CLICK_CALLBACK](x, y);
}
} }
declare global { declare global {

View File

@@ -483,6 +483,10 @@ export interface PictureCardConfig extends LovelaceCardConfig {
alt_text?: string; alt_text?: string;
} }
// Symbol for preview click callback - preserved through spreads, not serialized
// This allows the editor to attach a callback that only exists on the edited card's config
export const PREVIEW_CLICK_CALLBACK = Symbol("previewClickCallback");
export interface PictureElementsCardConfig extends LovelaceCardConfig { export interface PictureElementsCardConfig extends LovelaceCardConfig {
title?: string; title?: string;
image?: string | MediaSelectorValue; image?: string | MediaSelectorValue;
@@ -497,6 +501,7 @@ export interface PictureElementsCardConfig extends LovelaceCardConfig {
theme?: string; theme?: string;
dark_mode_image?: string | MediaSelectorValue; dark_mode_image?: string | MediaSelectorValue;
dark_mode_filter?: string; dark_mode_filter?: string;
[PREVIEW_CLICK_CALLBACK]?: (x: number, y: number) => void;
} }
export interface PictureEntityCardConfig extends LovelaceCardConfig { export interface PictureEntityCardConfig extends LovelaceCardConfig {

View File

@@ -15,12 +15,16 @@ import {
} from "superstruct"; } from "superstruct";
import type { HASSDomEvent } from "../../../../common/dom/fire_event"; import type { HASSDomEvent } from "../../../../common/dom/fire_event";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-alert";
import "../../../../components/ha-card"; import "../../../../components/ha-card";
import "../../../../components/ha-form/ha-form"; import "../../../../components/ha-form/ha-form";
import "../../../../components/ha-icon"; import "../../../../components/ha-icon";
import "../../../../components/ha-switch"; import "../../../../components/ha-switch";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import type { PictureElementsCardConfig } from "../../cards/types"; import {
PREVIEW_CLICK_CALLBACK,
type PictureElementsCardConfig,
} from "../../cards/types";
import type { LovelaceCardEditor } from "../../types"; import type { LovelaceCardEditor } from "../../types";
import "../hui-sub-element-editor"; import "../hui-sub-element-editor";
import { baseLovelaceCardConfig } from "../structs/base-card-struct"; import { baseLovelaceCardConfig } from "../structs/base-card-struct";
@@ -28,7 +32,6 @@ import type { EditDetailElementEvent, SubElementEditorConfig } from "../types";
import { configElementStyle } from "./config-elements-style"; import { configElementStyle } from "./config-elements-style";
import "../hui-picture-elements-card-row-editor"; import "../hui-picture-elements-card-row-editor";
import type { LovelaceElementConfig } from "../../elements/types"; import type { LovelaceElementConfig } from "../../elements/types";
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
import type { LocalizeFunc } from "../../../../common/translations/localize"; import type { LocalizeFunc } from "../../../../common/translations/localize";
const genericElementConfigStruct = type({ const genericElementConfigStruct = type({
@@ -66,6 +69,44 @@ export class HuiPictureElementsCardEditor
this._config = config; this._config = config;
} }
private _onPreviewClick = (x: number, y: number): void => {
if (this._subElementEditorConfig?.type === "element") {
this._handlePositionClick(x, y);
}
};
private _handlePositionClick(x: number, y: number): void {
if (
!this._subElementEditorConfig?.elementConfig ||
this._subElementEditorConfig.type !== "element" ||
this._subElementEditorConfig.elementConfig.type === "conditional"
) {
return;
}
const elementConfig = this._subElementEditorConfig
.elementConfig as LovelaceElementConfig;
const currentPosition = (elementConfig.style as Record<string, string>)
?.position;
if (currentPosition && currentPosition !== "absolute") {
return;
}
const newElement = {
...elementConfig,
style: {
...((elementConfig.style as Record<string, string>) || {}),
left: `${Math.round(x)}%`,
top: `${Math.round(y)}%`,
},
};
const updateEvent = new CustomEvent("config-changed", {
detail: { config: newElement },
});
this._handleSubElementChanged(updateEvent);
}
private _schema = memoizeOne( private _schema = memoizeOne(
(localize: LocalizeFunc) => (localize: LocalizeFunc) =>
[ [
@@ -138,6 +179,16 @@ export class HuiPictureElementsCardEditor
if (this._subElementEditorConfig) { if (this._subElementEditorConfig) {
return html` return html`
${this._subElementEditorConfig.type === "element" &&
this._subElementEditorConfig.elementConfig?.type !== "conditional"
? html`
<ha-alert alert-type="info">
${this.hass.localize(
"ui.panel.lovelace.editor.card.picture-elements.position_hint"
)}
</ha-alert>
`
: nothing}
<hui-sub-element-editor <hui-sub-element-editor
.hass=${this.hass} .hass=${this.hass}
.config=${this._subElementEditorConfig} .config=${this._subElementEditorConfig}
@@ -181,6 +232,7 @@ export class HuiPictureElementsCardEditor
return; return;
} }
// no need to attach the preview click callback here, no element is being edited
fireEvent(this, "config-changed", { config: ev.detail.value }); fireEvent(this, "config-changed", { config: ev.detail.value });
} }
@@ -191,7 +243,8 @@ export class HuiPictureElementsCardEditor
const config = { const config = {
...this._config, ...this._config,
elements: ev.detail.elements as LovelaceElementConfig[], elements: ev.detail.elements as LovelaceElementConfig[],
} as LovelaceCardConfig; [PREVIEW_CLICK_CALLBACK]: this._onPreviewClick,
} as PictureElementsCardConfig;
fireEvent(this, "config-changed", { config }); fireEvent(this, "config-changed", { config });
@@ -232,7 +285,12 @@ export class HuiPictureElementsCardEditor
elementConfig: value, elementConfig: value,
}; };
fireEvent(this, "config-changed", { config: this._config }); fireEvent(this, "config-changed", {
config: {
...this._config,
[PREVIEW_CLICK_CALLBACK]: this._onPreviewClick,
},
});
} }
private _editDetailElement(ev: HASSDomEvent<EditDetailElementEvent>): void { private _editDetailElement(ev: HASSDomEvent<EditDetailElementEvent>): void {

View File

@@ -10,7 +10,9 @@ export const getElementStubConfig = async (
): Promise<LovelaceElementConfig> => { ): Promise<LovelaceElementConfig> => {
let elementConfig: LovelaceElementConfig = { type }; let elementConfig: LovelaceElementConfig = { type };
if (type !== "conditional") { if (type === "conditional") {
elementConfig = { type, conditions: [], elements: [] };
} else {
elementConfig.style = { left: "50%", top: "50%" }; elementConfig.style = { left: "50%", top: "50%" };
} }

View File

@@ -89,7 +89,11 @@ export abstract class HuiElementEditor<
} }
public set value(config: T | undefined) { public set value(config: T | undefined) {
if (this._config && deepEqual(config, this._config)) { // Compare symbols to detect callback changes (e.g., preview click handlers)
if (
this._config &&
deepEqual(config, this._config, { compareSymbols: true })
) {
return; return;
} }
this._config = config; this._config = config;

View File

@@ -8217,6 +8217,7 @@
"dark_mode_image": "Dark mode image path", "dark_mode_image": "Dark mode image path",
"state_filter": "State filter", "state_filter": "State filter",
"dark_mode_filter": "Dark mode state filter", "dark_mode_filter": "Dark mode state filter",
"position_hint": "Click on the image preview to position this element",
"element_types": { "element_types": {
"state-badge": "State badge", "state-badge": "State badge",
"state-icon": "State icon", "state-icon": "State icon",