Compare commits

..

22 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
44fcb72b79 Migrate row-level overflow menu from ha-md-menu to ha-dropdown in automation picker
Co-authored-by: wendevlin <12148533+wendevlin@users.noreply.github.com>
2025-12-09 13:09:46 +00:00
Wendelin
a88fcd41b5 Fix submenu 2025-12-09 14:03:19 +01:00
copilot-swe-agent[bot]
0a8c0c1c89 Complete scene-dashboard and script-picker migration - add @wa-select handlers and replace ha-sub-menu with native submenu
Co-authored-by: wendevlin <12148533+wendevlin@users.noreply.github.com>
2025-12-09 12:15:06 +00:00
copilot-swe-agent[bot]
6f140d30f5 Fix scene-dashboard and script-picker - remove .clickAction, @click, keep-open, has-overflow, replace ha-sub-menu with native submenu pattern
Co-authored-by: wendevlin <12148533+wendevlin@users.noreply.github.com>
2025-12-09 12:12:15 +00:00
copilot-swe-agent[bot]
baa84ea36c Fix multiple files - remove @click, .clickAction, has-overflow, replace ha-sub-menu, move class to button
Co-authored-by: wendevlin <12148533+wendevlin@users.noreply.github.com>
2025-12-09 12:03:58 +00:00
copilot-swe-agent[bot]
68033fa8b4 Fix ha-config-devices-dashboard.ts - replace .clickAction and ha-sub-menu with proper patterns
Co-authored-by: wendevlin <12148533+wendevlin@users.noreply.github.com>
2025-12-09 11:53:09 +00:00
copilot-swe-agent[bot]
5fd9bac9e5 Use data-action attribute instead of .action property for label bulk actions
Co-authored-by: wendevlin <12148533+wendevlin@users.noreply.github.com>
2025-12-09 11:46:54 +00:00
copilot-swe-agent[bot]
a1c7f91dd0 Replace ha-sub-menu with native ha-dropdown-item submenu pattern
Co-authored-by: wendevlin <12148533+wendevlin@users.noreply.github.com>
2025-12-09 11:42:58 +00:00
Wendelin
10538989f1 Merge branch 'dev' of github.com:home-assistant/frontend into copilot/migrate-overflow-menus-to-ha-dropdown 2025-12-09 12:33:13 +01:00
Wendelin
b80481b53e Generic picker: scroll to selected value on open (#28457) 2025-12-09 12:27:56 +01:00
Aidan Timson
2ce1eaf8c6 Revert "Add basic view transitions between tab UIs (#28374)" (#28451) 2025-12-09 13:18:51 +02:00
copilot-swe-agent[bot]
0b88a78ec0 Fix ha-automation-picker.ts - replace .clickAction with @wa-select handlers
Co-authored-by: wendevlin <12148533+wendevlin@users.noreply.github.com>
2025-12-05 13:30:08 +00:00
copilot-swe-agent[bot]
885f31a4b4 Fix type errors by re-adding ha-md-menu-item imports for row overflow menus
Co-authored-by: wendevlin <12148533+wendevlin@users.noreply.github.com>
2025-12-05 12:05:31 +00:00
copilot-swe-agent[bot]
e825a2b090 Migrate ha-button-menu to ha-dropdown in entities panel
Co-authored-by: wendevlin <12148533+wendevlin@users.noreply.github.com>
2025-12-05 12:01:26 +00:00
copilot-swe-agent[bot]
657f88595b Address code review feedback - remove unused methods and clean up warning class
Co-authored-by: wendevlin <12148533+wendevlin@users.noreply.github.com>
2025-12-05 11:58:09 +00:00
copilot-swe-agent[bot]
fdeca95215 Migrate ha-button-menu to ha-dropdown in devices dashboard
Co-authored-by: wendevlin <12148533+wendevlin@users.noreply.github.com>
2025-12-05 11:55:15 +00:00
copilot-swe-agent[bot]
13b60ed2ee Migrate ha-button-menu to ha-dropdown in script picker
Co-authored-by: wendevlin <12148533+wendevlin@users.noreply.github.com>
2025-12-05 11:51:53 +00:00
copilot-swe-agent[bot]
d74127001c Migrate ha-button-menu to ha-dropdown in automation picker
Co-authored-by: wendevlin <12148533+wendevlin@users.noreply.github.com>
2025-12-05 11:47:50 +00:00
copilot-swe-agent[bot]
eb9d1c119a Migrate ha-button-menu to ha-dropdown in updates and helpers panels
Co-authored-by: wendevlin <12148533+wendevlin@users.noreply.github.com>
2025-12-05 11:43:36 +00:00
copilot-swe-agent[bot]
cecf0a95c8 Migrate ha-button-menu to ha-dropdown in scene panel
Co-authored-by: wendevlin <12148533+wendevlin@users.noreply.github.com>
2025-12-05 11:39:22 +00:00
copilot-swe-agent[bot]
950a773f7e Migrate ha-button-menu to ha-dropdown in areas, dashboard, cloud, and network panels
Co-authored-by: wendevlin <12148533+wendevlin@users.noreply.github.com>
2025-12-05 11:33:16 +00:00
copilot-swe-agent[bot]
dddcd04ce9 Initial plan 2025-12-05 11:21:21 +00:00
19 changed files with 1756 additions and 1367 deletions

View File

@@ -133,6 +133,8 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
@state() private _sectionTitle?: string;
@state() private _valuePinned = true;
private _allItems: (PickerComboBoxItem | string)[] = [];
private _selectedItemIndex = -1;
@@ -194,6 +196,15 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
.renderItem=${this._renderItem}
style="min-height: 36px;"
class=${this._listScrolled ? "scrolled" : ""}
.layout=${this.value && this._valuePinned
? {
pin: {
index: this._getInitialSelectedIndex(),
block: "center",
},
}
: undefined}
@unpinned=${this._handleUnpinned}
@scroll=${this._onScrollList}
@focus=${this._focusList}
@visibilityChanged=${this._visibilityChanged}
@@ -244,6 +255,11 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
}
}
@eventOptions({ passive: true })
private _handleUnpinned() {
this._valuePinned = false;
}
private _getAdditionalItems = (searchString?: string) =>
this.getAdditionalItems?.(searchString) || [];
@@ -592,6 +608,24 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
private _keyFunction = (item: PickerComboBoxItem | string) =>
typeof item === "string" ? item : item.id;
private _getInitialSelectedIndex() {
if (!this._virtualizerElement || !this.value) {
return 0;
}
const index = this._virtualizerElement.items.findIndex(
(item) =>
typeof item !== "string" &&
(item as PickerComboBoxItem).id === this.value
);
if (index === -1) {
return 0;
}
return index;
}
static get styles() {
return [
...super.styles,

View File

@@ -1,12 +1,17 @@
import { mdiClose } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, state, query } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-button";
import "../../../../components/ha-dialog-footer";
import {
getMobileOpenFromBottomAnimation,
getMobileCloseToBottomAnimation,
} from "../../../../components/ha-md-dialog";
import type { HaMdDialog } from "../../../../components/ha-md-dialog";
import "../../../../components/ha-dialog-header";
import "../../../../components/ha-icon-button-toggle";
import "../../../../components/ha-wa-dialog";
import type { EntityRegistryEntry } from "../../../../data/entity_registry";
import type { LightColor, LightEntity } from "../../../../data/light";
import {
@@ -36,7 +41,7 @@ class DialogLightColorFavorite extends LitElement {
@state() private _modes: LightPickerMode[] = [];
@state() private _open = false;
@query("ha-md-dialog") private _dialog?: HaMdDialog;
public async showDialog(
dialogParams: LightColorFavoriteDialogParams
@@ -44,15 +49,11 @@ class DialogLightColorFavorite extends LitElement {
this._entry = dialogParams.entry;
this._dialogParams = dialogParams;
this._color = dialogParams.initialColor ?? this._computeCurrentColor();
this._open = true;
this._updateModes();
}
public closeDialog(): void {
if (this._open) {
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
this._open = false;
this._dialog?.close();
}
private _updateModes() {
@@ -132,6 +133,7 @@ class DialogLightColorFavorite extends LitElement {
this._dialogParams = undefined;
this._entry = undefined;
this._color = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
private async _save() {
@@ -157,77 +159,83 @@ class DialogLightColorFavorite extends LitElement {
}
return html`
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
header-title=${this._dialogParams?.title || ""}
aria-labelledby="dialog-light-color-favorite-title"
<ha-md-dialog
open
@cancel=${this._cancel}
@closed=${this._dialogClosed}
aria-labelledby="dialog-light-color-favorite-title"
.getOpenAnimation=${getMobileOpenFromBottomAnimation}
.getCloseAnimation=${getMobileCloseToBottomAnimation}
>
<div class="header">
${this._modes.length > 1
? html`
<div class="modes">
${this._modes.map(
(value) => html`
<ha-icon-button-toggle
border-only
.selected=${value === this._mode}
.label=${this.hass.localize(
`ui.dialogs.more_info_control.light.color_picker.mode.${value}`
)}
.mode=${value}
@click=${this._modeChanged}
>
<span
class="wheel ${classMap({ [value]: true })}"
></span>
</ha-icon-button-toggle>
`
)}
</div>
`
: nothing}
</div>
<div class="content">
${this._mode === "color_temp"
? html`
<light-color-temp-picker
.hass=${this.hass}
.stateObj=${this.stateObj}
@color-changed=${this._colorChanged}
>
</light-color-temp-picker>
`
: nothing}
${this._mode === "color"
? html`
<light-color-rgb-picker
.hass=${this.hass}
.stateObj=${this.stateObj}
@color-changed=${this._colorChanged}
>
</light-color-rgb-picker>
`
: nothing}
</div>
<ha-dialog-footer slot="footer">
<ha-button
slot="secondaryAction"
appearance="plain"
@click=${this._cancelDialog}
<ha-dialog-header slot="headline">
<ha-icon-button
slot="navigationIcon"
@click=${this.closeDialog}
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
></ha-icon-button>
<span slot="title" id="dialog-light-color-favorite-title"
>${this._dialogParams?.title}</span
>
</ha-dialog-header>
<div slot="content">
<div class="header">
${this._modes.length > 1
? html`
<div class="modes">
${this._modes.map(
(value) => html`
<ha-icon-button-toggle
border-only
.selected=${value === this._mode}
.label=${this.hass.localize(
`ui.dialogs.more_info_control.light.color_picker.mode.${value}`
)}
.mode=${value}
@click=${this._modeChanged}
>
<span
class="wheel ${classMap({ [value]: true })}"
></span>
</ha-icon-button-toggle>
`
)}
</div>
`
: nothing}
</div>
<div class="content">
${this._mode === "color_temp"
? html`
<light-color-temp-picker
.hass=${this.hass}
.stateObj=${this.stateObj}
@color-changed=${this._colorChanged}
>
</light-color-temp-picker>
`
: nothing}
${this._mode === "color"
? html`
<light-color-rgb-picker
.hass=${this.hass}
.stateObj=${this.stateObj}
@color-changed=${this._colorChanged}
>
</light-color-rgb-picker>
`
: nothing}
</div>
</div>
<div slot="actions">
<ha-button appearance="plain" @click=${this._cancelDialog}>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button
slot="primaryAction"
@click=${this._save}
.disabled=${!this._color}
<ha-button @click=${this._save} .disabled=${!this._color}
>${this.hass.localize("ui.common.save")}</ha-button
>
${this.hass.localize("ui.common.save")}
</ha-button>
</ha-dialog-footer>
</ha-wa-dialog>
</div>
</ha-md-dialog>
`;
}
@@ -235,7 +243,7 @@ class DialogLightColorFavorite extends LitElement {
return [
haStyleDialog,
css`
ha-wa-dialog {
ha-md-dialog {
min-width: 420px; /* prevent width jumps when switching modes */
max-height: min(
600px,
@@ -244,10 +252,14 @@ class DialogLightColorFavorite extends LitElement {
}
@media all and (max-width: 450px), all and (max-height: 500px) {
ha-wa-dialog {
ha-md-dialog {
min-width: 100%;
min-height: auto;
max-height: calc(100% - 100px);
margin-bottom: 0;
--md-dialog-container-shape-start-start: 28px;
--md-dialog-container-shape-start-end: 28px;
}
}

View File

@@ -13,7 +13,6 @@ import "../components/ha-svg-icon";
import "../components/ha-tab";
import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant, Route } from "../types";
import { withViewTransition } from "../common/util/view-transition";
export interface PageNavigation {
path: string;
@@ -113,11 +112,9 @@ class HassTabsSubpage extends LitElement {
public willUpdate(changedProperties: PropertyValues) {
if (changedProperties.has("route")) {
withViewTransition(() => {
this._activeTab = this.tabs.find((tab) =>
`${this.route.prefix}${this.route.path}`.includes(tab.path)
);
});
this._activeTab = this.tabs.find((tab) =>
`${this.route.prefix}${this.route.path}`.includes(tab.path)
);
}
super.willUpdate(changedProperties);
}

View File

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

View File

@@ -1,4 +1,4 @@
import type { ActionDetail } from "@material/mwc-list";
import "@home-assistant/webawesome/dist/components/divider/divider";
import {
mdiDelete,
mdiDotsVertical,
@@ -24,10 +24,12 @@ import {
type AreasFloorHierarchy,
} from "../../../common/areas/areas-floor-hierarchy";
import { formatListWithAnds } from "../../../common/string/format-list";
import "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import type { HaDropdownItem } from "../../../components/ha-dropdown-item";
import "../../../components/ha-fab";
import "../../../components/ha-floor-icon";
import "../../../components/ha-icon-button";
import "../../../components/ha-list-item";
import "../../../components/ha-sortable";
import type { HaSortableOptions } from "../../../components/ha-sortable";
import "../../../components/ha-svg-icon";
@@ -196,44 +198,43 @@ export class HaConfigAreasDashboard extends LitElement {
${floor.name}
</h2>
<div class="actions">
<ha-button-menu
<ha-dropdown
.floor=${floor}
@action=${this._handleFloorAction}
@wa-select=${this._handleFloorAction}
>
<ha-icon-button
slot="trigger"
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-list-item graphic="icon"
><ha-svg-icon
<ha-dropdown-item value="reorder">
<ha-svg-icon
slot="icon"
.path=${mdiSort}
slot="graphic"
></ha-svg-icon
>${this.hass.localize(
></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.areas.picker.reorder"
)}</ha-list-item
>
<li divider role="separator"></li>
<ha-list-item graphic="icon"
><ha-svg-icon
)}
</ha-dropdown-item>
<wa-divider></wa-divider>
<ha-dropdown-item value="edit">
<ha-svg-icon
slot="icon"
.path=${mdiPencil}
slot="graphic"
></ha-svg-icon
>${this.hass.localize(
></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.areas.picker.floor.edit_floor"
)}</ha-list-item
>
<ha-list-item class="warning" graphic="icon"
><ha-svg-icon
class="warning"
)}
</ha-dropdown-item>
<ha-dropdown-item value="delete" variant="danger">
<ha-svg-icon
slot="icon"
.path=${mdiDelete}
slot="graphic"
></ha-svg-icon
>${this.hass.localize(
></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.areas.picker.floor.delete_floor"
)}</ha-list-item
>
</ha-button-menu>
)}
</ha-dropdown-item>
</ha-dropdown>
</div>
</div>
<ha-sortable
@@ -273,23 +274,23 @@ export class HaConfigAreasDashboard extends LitElement {
)}
</h2>
<div class="actions">
<ha-button-menu
@action=${this._handleUnassignedAreasAction}
<ha-dropdown
@wa-select=${this._handleUnassignedAreasAction}
>
<ha-icon-button
slot="trigger"
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-list-item graphic="icon"
><ha-svg-icon
<ha-dropdown-item value="reorder">
<ha-svg-icon
slot="icon"
.path=${mdiSort}
slot="graphic"
></ha-svg-icon
>${this.hass.localize(
></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.areas.picker.reorder"
)}</ha-list-item
>
</ha-button-menu>
)}
</ha-dropdown-item>
</ha-dropdown>
</div>
</div>
<ha-sortable
@@ -533,23 +534,25 @@ export class HaConfigAreasDashboard extends LitElement {
}, time);
}
private _handleFloorAction(ev: CustomEvent<ActionDetail>) {
private _handleFloorAction(ev: CustomEvent) {
const item = ev.detail.item as HaDropdownItem;
const floor = (ev.currentTarget as any).floor;
switch (ev.detail.index) {
case 0:
switch (item.value) {
case "reorder":
this._showReorderDialog();
break;
case 1:
case "edit":
this._editFloor(floor);
break;
case 2:
case "delete":
this._deleteFloor(floor);
break;
}
}
private _handleUnassignedAreasAction(ev: CustomEvent<ActionDetail>) {
if (ev.detail.index === 0) {
private _handleUnassignedAreasAction(ev: CustomEvent) {
const item = ev.detail.item as HaDropdownItem;
if (item.value === "reorder") {
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 { consume } from "@lit/context";
import {
mdiChevronRight,
mdiCog,
mdiContentDuplicate,
mdiDelete,
@@ -36,7 +36,6 @@ import { fireEvent } from "../../../common/dom/fire_event";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { navigate } from "../../../common/navigate";
import { slugify } from "../../../common/string/slugify";
import "../../../components/ha-tooltip";
import type { LocalizeFunc } from "../../../common/translations/localize";
import {
hasRejectedItems,
@@ -51,6 +50,9 @@ import type {
} from "../../../components/data-table/ha-data-table";
import "../../../components/data-table/ha-data-table-labels";
import "../../../components/entity/ha-entity-toggle";
import "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import type { HaDropdownItem } from "../../../components/ha-dropdown-item";
import "../../../components/ha-fab";
import "../../../components/ha-filter-blueprints";
import "../../../components/ha-filter-categories";
@@ -59,13 +61,9 @@ import "../../../components/ha-filter-entities";
import "../../../components/ha-filter-floor-areas";
import "../../../components/ha-filter-labels";
import "../../../components/ha-icon-button";
import "../../../components/ha-md-divider";
import "../../../components/ha-md-menu";
import type { HaMdMenu } from "../../../components/ha-md-menu";
import "../../../components/ha-md-menu-item";
import type { HaMdMenuItem } from "../../../components/ha-md-menu-item";
import "../../../components/ha-sub-menu";
import "../../../components/ha-svg-icon";
import "../../../components/ha-tooltip";
import { createAreaRegistryEntry } from "../../../data/area_registry";
import type { AutomationEntity } from "../../../data/automation";
import {
@@ -175,7 +173,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
@consume({ context: fullEntitiesContext, subscribe: true })
_entityReg!: EntityRegistryEntry[];
@state() private _overflowAutomation?: AutomationItem;
@storage({ key: "automation-table-sort", state: false, subscribe: false })
private _activeSorting?: SortingChangedEvent;
@@ -204,7 +202,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
})
private _activeHiddenColumns?: string[];
@query("#overflow-menu") private _overflowMenu!: HaMdMenu;
private _sizeController = new ResizeController(this, {
callback: (entries) => entries[0]?.contentRect.width,
@@ -368,12 +366,53 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
moveable: false,
hideable: false,
template: (automation) => html`
<ha-icon-button
.automation=${automation}
.label=${this.hass.localize("ui.common.overflow_menu")}
.path=${mdiDotsVertical}
@click=${this._showOverflowMenu}
></ha-icon-button>
<ha-dropdown .automation=${automation} @wa-select=${this._handleRowOverflowMenu}>
<ha-icon-button
slot="trigger"
.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>
`,
},
};
@@ -381,17 +420,38 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
}
);
private _showOverflowMenu = (ev) => {
if (
this._overflowMenu.open &&
ev.target === this._overflowMenu.anchorElement
) {
this._overflowMenu.close();
return;
private _handleRowOverflowMenu = (ev: CustomEvent) => {
const item = ev.detail.item as HaDropdownItem;
const automation = (
(ev.currentTarget as HTMLElement).closest("ha-dropdown") as any
).automation;
switch (item.value) {
case "show-info":
this._showInfo(automation);
break;
case "show-settings":
this._showSettings(automation);
break;
case "edit-category":
this._editCategory(automation);
break;
case "run-actions":
this._runActions(automation);
break;
case "show-trace":
this._showTrace(automation);
break;
case "duplicate":
this._duplicate(automation);
break;
case "toggle":
this._toggle(automation);
break;
case "delete":
this._deleteConfirm(automation);
break;
}
this._overflowAutomation = ev.target.automation;
this._overflowMenu.anchorElement = ev.target;
this._overflowMenu.show();
};
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
@@ -409,34 +469,37 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
];
}
protected render(): TemplateResult {
const categoryItems = html`${this._categories?.map(
private _renderCategoryItems = (submenu = false) =>
html`${this._categories?.map(
(category) =>
html`<ha-md-menu-item
html`<ha-dropdown-item
.slot=${submenu ? "submenu" : ""}
.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>`
? 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-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>`;
<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>`;
const labelItems = html`${this._labels?.map((label) => {
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)
@@ -446,14 +509,14 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
this._selected.some((entityId) =>
this.hass.entities[entityId]?.labels.includes(label.label_id)
);
return html`<ha-md-menu-item
return html`<ha-dropdown-item
.slot=${submenu ? "submenu" : ""}
.value=${label.label_id}
.action=${selected ? "remove" : "add"}
data-action=${selected ? "remove" : "add"}
@click=${this._handleBulkLabel}
keep-open
>
<ha-checkbox
slot="start"
slot="icon"
.checked=${selected}
.indeterminate=${partial}
reducedTouchTarget
@@ -467,46 +530,49 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
: nothing}
${label.name}
</ha-label>
</ha-md-menu-item>`;
</ha-dropdown-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
>`;
<wa-divider .slot=${submenu ? "submenu" : ""}></wa-divider>
<ha-dropdown-item
.slot=${submenu ? "submenu" : ""}
value="__create_label__"
@click=${this._bulkCreateLabel}
>
${this.hass.localize("ui.panel.config.labels.add_label")}
</ha-dropdown-item>`;
const areaItems = html`${Object.values(this.hass.areas).map(
private _renderAreaItems = (submenu = false) =>
html`${Object.values(this.hass.areas).map(
(area) =>
html`<ha-md-menu-item
html`<ha-dropdown-item
.slot=${submenu ? "submenu" : ""}
.value=${area.area_id}
.clickAction=${this._handleBulkArea}
>
${area.icon
? html`<ha-icon slot="start" .icon=${area.icon}></ha-icon>`
? html`<ha-icon slot="icon" .icon=${area.icon}></ha-icon>`
: html`<ha-svg-icon
slot="start"
slot="icon"
.path=${mdiTextureBox}
></ha-svg-icon>`}
<div slot="headline">${area.name}</div>
</ha-md-menu-item>`
${area.name}
</ha-dropdown-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>`;
<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>`;
protected render(): TemplateResult {
const areasInOverflow =
(this._sizeController.value && this._sizeController.value < 900) ||
(!this._sizeController.value && this.hass.dockedSidebar === "docked");
@@ -527,9 +593,9 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
<hass-tabs-subpage-data-table
.hass=${this.hass}
.narrow=${this.narrow}
.backPath=${
this._searchParms.has("historyBack") ? undefined : "/config"
}
.backPath=${this._searchParms.has("historyBack")
? undefined
: "/config"}
id="entity_id"
.route=${this.route}
.tabs=${configSections.automations}
@@ -541,16 +607,14 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
.selected=${this._selected.length}
@selection-changed=${this._handleSelectionChanged}
has-filters
.filters=${
Object.values(this._filters).filter((filter) =>
Array.isArray(filter.value)
? filter.value.length
: filter.value &&
Object.values(filter.value).some((val) =>
Array.isArray(val) ? val.length : val
)
).length
}
.filters=${Object.values(this._filters).filter((filter) =>
Array.isArray(filter.value)
? filter.value.length
: filter.value &&
Object.values(filter.value).some((val) =>
Array.isArray(val) ? val.length : val
)
).length}
.columns=${this._columns(
this.narrow,
this.hass.localize,
@@ -643,13 +707,31 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
.narrow=${this.narrow}
@expanded-changed=${this._filterExpanded}
></ha-filter-blueprints>
${
!this.narrow
? html`<ha-md-button-menu slot="selection-bar">
${!this.narrow
? html`<ha-dropdown
slot="selection-bar"
@wa-select=${this._handleBulkCategorySelect}
>
<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
slot="trigger"
.label=${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.move_category"
"ui.panel.config.automation.picker.bulk_actions.add_label"
)}
>
<ha-svg-icon
@@ -657,179 +739,123 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-assist-chip>
${categoryItems}
</ha-md-button-menu>
${labelsInOverflow
? nothing
: html`<ha-md-button-menu slot="selection-bar">
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.add_label"
)}
>
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-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"
${this._renderLabelItems()}
</ha-dropdown>`}
${areasInOverflow
? nothing
: html`<ha-dropdown
slot="selection-bar"
@wa-select=${this._handleBulkAreaSelect}
>
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></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-assist-chip
slot="trigger"
.label=${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.move_area"
)}
>
<ha-svg-icon
slot="end"
.path=${mdiChevronRight}
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu slot="menu">${categoryItems}</ha-md-menu>
</ha-sub-menu>`
: nothing
}
${
this.narrow || labelsInOverflow
? html`<ha-sub-menu>
<ha-md-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.add_label"
)}
</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-menu-item .clickAction=${this._handleBulkEnable}>
<ha-svg-icon slot="start" .path=${mdiToggleSwitch}></ha-svg-icon>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.enable"
</ha-assist-chip>
${this._renderAreaItems()}
</ha-dropdown>`}`
: nothing}
<ha-dropdown
has-overflow
slot="selection-bar"
@wa-select=${this._handleOverflowMenuSelect}
>
${this.narrow
? html`<ha-assist-chip
.label=${this.hass.localize(
"ui.panel.config.automation.picker.bulk_action"
)}
</div>
</ha-md-menu-item>
<ha-md-menu-item .clickAction=${this._handleBulkDisable}>
<ha-svg-icon
slot="start"
.path=${mdiToggleSwitchOffOutline}
></ha-svg-icon>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.disable"
slot="trigger"
>
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-assist-chip>`
: html`<ha-icon-button
.path=${mdiDotsVertical}
.label=${this.hass.localize(
"ui.panel.config.automation.picker.bulk_action"
)}
</div>
</ha-md-menu-item>
</ha-md-button-menu>
${
!this.automations.length
? html`<div class="empty" slot="empty">
<ha-svg-icon .path=${mdiRobotHappy}></ha-svg-icon>
<h1>
${this.hass.localize(
"ui.panel.config.automation.picker.empty_header"
)}
</h1>
<p>
${this.hass.localize(
"ui.panel.config.automation.picker.empty_text_1"
)}
</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
}
slot="trigger"
></ha-icon-button>`}
${this.narrow
? html`<ha-dropdown-item>
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.move_category"
)}
${this._renderCategoryItems(true)}
</ha-dropdown-item>`
: nothing}
${this.narrow || labelsInOverflow
? html`<ha-dropdown-item>
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.add_label"
)}
${this._renderLabelItems(true)}
</ha-dropdown-item>`
: nothing}
${this.narrow || areasInOverflow
? html`<ha-dropdown-item>
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.move_area"
)}
${this._renderAreaItems(true)}
</ha-dropdown-item>`
: nothing}
<ha-dropdown-item value="enable">
<ha-svg-icon slot="icon" .path=${mdiToggleSwitch}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.enable"
)}
</ha-dropdown-item>
<ha-dropdown-item value="disable">
<ha-svg-icon
slot="icon"
.path=${mdiToggleSwitchOffOutline}
></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.disable"
)}
</ha-dropdown-item>
</ha-dropdown>
${!this.automations.length
? html`<div class="empty" slot="empty">
<ha-svg-icon .path=${mdiRobotHappy}></ha-svg-icon>
<h1>
${this.hass.localize(
"ui.panel.config.automation.picker.empty_header"
)}
</h1>
<p>
${this.hass.localize(
"ui.panel.config.automation.picker.empty_text_1"
)}
</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
slot="fab"
.label=${this.hass.localize(
@@ -841,80 +867,6 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</ha-fab>
</hass-tabs-subpage-data-table>
<ha-md-menu id="overflow-menu" positioning="fixed">
<ha-md-menu-item .clickAction=${this._showInfo}>
<ha-svg-icon
.path=${mdiInformationOutline}
slot="start"
></ha-svg-icon>
<div slot="headline">
${this.hass.localize("ui.panel.config.automation.editor.show_info")}
</div>
</ha-md-menu-item>
<ha-md-menu-item .clickAction=${this._showSettings}>
<ha-svg-icon .path=${mdiCog} slot="start"></ha-svg-icon>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.picker.show_settings"
)}
</div>
</ha-md-menu-item>
<ha-md-menu-item .clickAction=${this._editCategory}>
<ha-svg-icon .path=${mdiTag} slot="start"></ha-svg-icon>
<div slot="headline">
${this.hass.localize(
`ui.panel.config.automation.picker.${this._overflowAutomation?.category ? "edit_category" : "assign_category"}`
)}
</div>
</ha-md-menu-item>
<ha-md-menu-item .clickAction=${this._runActions}>
<ha-svg-icon .path=${mdiPlay} slot="start"></ha-svg-icon>
<div slot="headline">
${this.hass.localize("ui.panel.config.automation.editor.run")}
</div>
</ha-md-menu-item>
<ha-md-menu-item .clickAction=${this._showTrace}>
<ha-svg-icon .path=${mdiTransitConnection} slot="start"></ha-svg-icon>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.editor.show_trace"
)}
</div>
</ha-md-menu-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item .clickAction=${this._duplicate}>
<ha-svg-icon .path=${mdiContentDuplicate} slot="start"></ha-svg-icon>
<div slot="headline">
${this.hass.localize("ui.panel.config.automation.picker.duplicate")}
</div>
</ha-md-menu-item>
<ha-md-menu-item .clickAction=${this._toggle}>
<ha-svg-icon
.path=${
this._overflowAutomation?.state === "off"
? mdiToggleSwitch
: mdiToggleSwitchOffOutline
}
slot="start"
></ha-svg-icon>
<div slot="headline">
${
this._overflowAutomation?.state === "off"
? this.hass.localize("ui.panel.config.automation.editor.enable")
: this.hass.localize(
"ui.panel.config.automation.editor.disable"
)
}
</div>
</ha-md-menu-item>
<ha-md-menu-item .clickAction=${this._deleteConfirm} class="warning">
<ha-svg-icon .path=${mdiDelete} slot="start"></ha-svg-icon>
<div slot="headline">
${this.hass.localize("ui.panel.config.automation.picker.delete")}
</div>
</ha-md-menu-item>
</ha-md-menu>
`;
}
@@ -1071,33 +1023,22 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
this._applyFilters();
}
private _showInfo = (item: HaMdMenuItem) => {
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
.automation;
private _showInfo = (automation: AutomationEntity) => {
fireEvent(this, "hass-more-info", { entityId: automation.entity_id });
};
private _showSettings = (item: HaMdMenuItem) => {
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
.automation;
private _showSettings = (automation: AutomationEntity) => {
fireEvent(this, "hass-more-info", {
entityId: automation.entity_id,
view: "settings",
});
};
private _runActions = (item: HaMdMenuItem) => {
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
.automation;
private _runActions = (automation: AutomationEntity) => {
triggerAutomationActions(this.hass, automation.entity_id);
};
private _editCategory = (item: HaMdMenuItem) => {
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
.automation;
private _editCategory = (automation: AutomationEntity) => {
const entityReg = this._entityReg.find(
(reg) => reg.entity_id === automation.entity_id
);
@@ -1118,10 +1059,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
});
};
private _showTrace = (item: HaMdMenuItem) => {
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
.automation;
private _showTrace = (automation: AutomationEntity) => {
if (!automation.attributes.id) {
showAlertDialog(this, {
text: this.hass.localize(
@@ -1135,20 +1073,14 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
);
};
private _toggle = async (item: HaMdMenuItem): Promise<void> => {
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
.automation;
private _toggle = async (automation: AutomationEntity): Promise<void> => {
const service = automation.state === "off" ? "turn_on" : "turn_off";
await this.hass.callService("automation", service, {
entity_id: automation.entity_id,
});
};
private _deleteConfirm = async (item: HaMdMenuItem) => {
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
.automation;
private _deleteConfirm = async (automation: AutomationEntity) => {
showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.automation.picker.delete_confirm_title"
@@ -1185,10 +1117,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
}
}
private _duplicate = async (item: HaMdMenuItem) => {
const automation = ((item.parentElement as HaMdMenu)!.anchorElement as any)!
.automation;
private _duplicate = async (automation: AutomationEntity) => {
try {
const config = await fetchAutomationFileConfig(
this.hass,
@@ -1261,9 +1190,16 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
}
}
private _handleBulkCategory = async (item) => {
const category = item.value;
this._bulkAddCategory(category);
private _handleBulkCategorySelect = (ev: CustomEvent) => {
const item = ev.detail.item as HaDropdownItem;
const value = item.value as string;
if (value === "__create_category__") {
this._bulkCreateCategory();
} else if (value === "__no_category__") {
this._bulkAddCategory("");
} else {
this._bulkAddCategory(value);
}
};
private async _bulkAddCategory(category: string) {
@@ -1292,8 +1228,9 @@ ${rejected
}
private async _handleBulkLabel(ev) {
ev.stopPropagation();
const label = ev.currentTarget.value;
const action = ev.currentTarget.action;
const action = ev.currentTarget.dataset.action;
this._bulkLabel(label, action);
}
@@ -1327,9 +1264,16 @@ ${rejected
}
}
private _handleBulkArea = (item) => {
const area = item.value;
this._bulkAddArea(area);
private _handleBulkAreaSelect = (ev: CustomEvent) => {
const item = ev.detail.item as HaDropdownItem;
const value = item.value as string;
if (value === "__create_area__") {
this._bulkCreateArea();
} else if (value === "__no_area__") {
this._bulkAddArea("");
} else {
this._bulkAddArea(value);
}
};
private async _bulkAddArea(area: string) {
@@ -1367,6 +1311,18 @@ ${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;
}
};
private _handleBulkEnable = async () => {
const promises: Promise<ServiceCallResponse>[] = [];
this._selected.forEach((entityId) => {
@@ -1477,7 +1433,7 @@ ${rejected
ha-assist-chip {
--ha-assist-chip-container-shape: 10px;
}
ha-md-button-menu ha-assist-chip {
ha-dropdown ha-assist-chip {
--md-assist-chip-trailing-space: 8px;
}
ha-label {

View File

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

View File

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

View File

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

View File

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

View File

@@ -47,13 +47,14 @@ import "../../../components/ha-check-list-item";
import "../../../components/ha-fab";
import "../../../components/ha-filter-devices";
import "../../../components/ha-filter-floor-areas";
import "@home-assistant/webawesome/dist/components/divider/divider";
import "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import type { HaDropdownItem } from "../../../components/ha-dropdown-item";
import "../../../components/ha-filter-integrations";
import "../../../components/ha-filter-labels";
import "../../../components/ha-filter-states";
import "../../../components/ha-icon-button";
import "../../../components/ha-md-divider";
import "../../../components/ha-md-menu-item";
import "../../../components/ha-sub-menu";
import { createAreaRegistryEntry } from "../../../data/area_registry";
import type { ConfigEntry, SubEntry } from "../../../data/config_entries";
import { getSubEntries, sortConfigEntries } from "../../../data/config_entries";
@@ -720,34 +721,27 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
const areaItems = html`${Object.values(this.hass.areas).map(
(area) =>
html`<ha-md-menu-item
.value=${area.area_id}
.clickAction=${this._handleBulkArea}
>
html`<ha-dropdown-item .value=${area.area_id}>
${area.icon
? html`<ha-icon slot="start" .icon=${area.icon}></ha-icon>`
? html`<ha-icon slot="icon" .icon=${area.icon}></ha-icon>`
: html`<ha-svg-icon
slot="start"
slot="icon"
.path=${mdiTextureBox}
></ha-svg-icon>`}
<div slot="headline">${area.name}</div>
</ha-md-menu-item>`
${area.name}
</ha-dropdown-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>`;
<ha-dropdown-item value="__no_area__">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.no_area"
)}
</ha-dropdown-item>
<wa-divider></wa-divider>
<ha-dropdown-item value="__create_area__">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.add_area"
)}
</ha-dropdown-item>`;
const labelItems = html`${this._labels?.map((label) => {
const color = label.color ? computeCssColor(label.color) : undefined;
@@ -759,14 +753,13 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
this._selected.some((deviceId) =>
this.hass.devices[deviceId]?.labels.includes(label.label_id)
);
return html`<ha-md-menu-item
return html`<ha-dropdown-item
.value=${label.label_id}
.action=${selected ? "remove" : "add"}
@click=${this._handleBulkLabel}
data-action=${selected ? "remove" : "add"}
keep-open
>
<ha-checkbox
slot="start"
slot="icon"
.checked=${selected}
.indeterminate=${partial}
reducedTouchTarget
@@ -780,14 +773,12 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
: nothing}
${label.name}
</ha-label>
</ha-md-menu-item>`;
</ha-dropdown-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
>`;
<wa-divider></wa-divider>
<ha-dropdown-item value="__create_label__">
${this.hass.localize("ui.panel.config.labels.add_label")}
</ha-dropdown-item>`;
return html`
<hass-tabs-subpage-data-table
@@ -906,7 +897,7 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
></ha-filter-labels>
${!this.narrow
? html`<ha-md-button-menu slot="selection-bar">
? html`<ha-dropdown slot="selection-bar">
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
@@ -919,11 +910,14 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
></ha-svg-icon>
</ha-assist-chip>
${labelItems}
</ha-md-button-menu>
</ha-dropdown>
${areasInOverflow
? nothing
: html`<ha-md-button-menu slot="selection-bar">
: html`<ha-dropdown
slot="selection-bar"
@wa-select=${this._handleBulkAreaSelect}
>
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
@@ -936,9 +930,9 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
></ha-svg-icon>
</ha-assist-chip>
${areaItems}
</ha-md-button-menu>`}`
</ha-dropdown>`}`
: nothing}
<ha-md-button-menu has-overflow slot="selection-bar">
<ha-dropdown has-overflow slot="selection-bar" @wa-select=${this._handleOverflowMenuSelect}>
${this.narrow
? html`<ha-assist-chip
.label=${this.hass.localize(
@@ -959,51 +953,104 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
slot="trigger"
></ha-icon-button>`}
${this.narrow
? html` <ha-sub-menu>
<ha-md-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.add_label"
)}
</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>`
? html`<ha-dropdown-item>
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.add_label"
)}
<ha-svg-icon
slot="details"
.path=${mdiChevronRight}
></ha-svg-icon>
${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"
.value=${label.label_id}
data-action=${selected ? "remove" : "add"}
@click=${this._handleBulkLabel}
keep-open
>
<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"></wa-divider>
<ha-dropdown-item
slot="submenu"
value="__create_label__"
@click=${this._bulkCreateLabel}
>
${this.hass.localize("ui.panel.config.labels.add_label")}
</ha-dropdown-item>
</ha-dropdown-item>`
: nothing}
${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>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>`
? html`<ha-dropdown-item>
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.move_area"
)}
<ha-svg-icon
slot="details"
.path=${mdiChevronRight}
></ha-svg-icon>
${Object.values(this.hass.areas).map(
(area) =>
html`<ha-dropdown-item slot="submenu" .value=${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" value="__no_area__">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.no_area"
)}
</ha-dropdown-item>
<wa-divider slot="submenu"></wa-divider>
<ha-dropdown-item slot="submenu" value="__create_area__">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.add_area"
)}
</ha-dropdown-item>
</ha-dropdown-item>
<wa-divider></wa-divider>`
: nothing}
<ha-md-menu-item
.clickAction=${this._deleteSelected}
<ha-dropdown-item
value="delete"
.disabled=${!this._selectedCanDelete.length}
class="warning"
variant="danger"
>
<ha-svg-icon slot="start" .path=${mdiDelete}></ha-svg-icon>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.delete_selected.button"
)}
</div>
</ha-md-menu-item>
</ha-md-button-menu>
<ha-svg-icon slot="icon" .path=${mdiDelete}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.delete_selected.button"
)}
</ha-dropdown-item>
</ha-dropdown>
</hass-tabs-subpage-data-table>
`;
}
@@ -1093,6 +1140,53 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
this._selected = ev.detail.value;
}
private _handleBulkAreaSelect(ev: CustomEvent) {
const item = ev.detail.item as HaDropdownItem;
switch (item.value) {
case "__no_area__":
this._bulkAddArea(null);
break;
case "__create_area__":
this._bulkCreateArea();
break;
default:
this._bulkAddArea(item.value as string);
break;
}
}
private _handleOverflowMenuSelect(ev: CustomEvent) {
const item = ev.detail.item as HaDropdownItem;
// Handle label selections (checkbox items with keep-open)
if (item.hasAttribute("keep-open") && item.hasAttribute("data-action")) {
const label = item.value as string;
const action = (item as HTMLElement).dataset.action as "add" | "remove";
this._bulkLabel(label, action);
return;
}
switch (item.value) {
case "delete":
this._deleteSelected();
break;
case "__create_label__":
this._bulkCreateLabel();
break;
case "__no_area__":
this._bulkAddArea(null);
break;
case "__create_area__":
this._bulkCreateArea();
break;
default:
if (item.value && typeof item.value === "string" && item.value.startsWith("area_")) {
this._bulkAddArea(item.value);
}
break;
}
}
private _handleBulkArea = (item) => {
const area = item.value;
this._bulkAddArea(area);
@@ -1133,12 +1227,6 @@ ${rejected
});
};
private async _handleBulkLabel(ev) {
const label = ev.currentTarget.value;
const action = ev.currentTarget.action;
this._bulkLabel(label, action);
}
private async _bulkLabel(label: string, action: "add" | "remove") {
const promises: Promise<DeviceRegistryEntry>[] = [];
this._selected.forEach((deviceId) => {
@@ -1277,7 +1365,7 @@ ${rejected
ha-assist-chip {
--ha-assist-chip-container-shape: 10px;
}
ha-md-button-menu ha-assist-chip {
ha-dropdown ha-assist-chip {
--md-assist-chip-trailing-space: 8px;
}
ha-label {

View File

@@ -53,10 +53,12 @@ import type {
SelectionChangedEvent,
SortingChangedEvent,
} from "../../../components/data-table/ha-data-table";
import "@home-assistant/webawesome/dist/components/divider/divider";
import "../../../components/data-table/ha-data-table-labels";
import "../../../components/ha-alert";
import "../../../components/ha-button-menu";
import "../../../components/ha-check-list-item";
import "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import type { HaDropdownItem } from "../../../components/ha-dropdown-item";
import "../../../components/ha-filter-devices";
import "../../../components/ha-filter-domains";
import "../../../components/ha-filter-floor-areas";
@@ -65,9 +67,6 @@ import "../../../components/ha-filter-labels";
import "../../../components/ha-filter-states";
import "../../../components/ha-icon";
import "../../../components/ha-icon-button";
import "../../../components/ha-md-divider";
import "../../../components/ha-md-menu-item";
import "../../../components/ha-sub-menu";
import "../../../components/ha-svg-icon";
import "../../../components/ha-tooltip";
import type { ConfigEntry, SubEntry } from "../../../data/config_entries";
@@ -782,14 +781,13 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
this._selected.some((entityId) =>
this.hass.entities[entityId]?.labels.includes(label.label_id)
);
return html`<ha-md-menu-item
return html`<ha-dropdown-item
.value=${label.label_id}
.action=${selected ? "remove" : "add"}
@click=${this._handleBulkLabel}
data-action=${selected ? "remove" : "add"}
keep-open
>
<ha-checkbox
slot="start"
slot="icon"
.checked=${selected}
.indeterminate=${partial}
reducedTouchTarget
@@ -803,22 +801,20 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
: nothing}
${label.name}
</ha-label>
</ha-md-menu-item>`;
</ha-dropdown-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
>`;
<wa-divider></wa-divider>
<ha-dropdown-item value="__create_label__">
${this.hass.localize("ui.panel.config.labels.add_label")}
</ha-dropdown-item>`;
return html`
<hass-tabs-subpage-data-table
.hass=${this.hass}
.narrow=${this.narrow}
.backPath=${
this._searchParms.has("historyBack") ? undefined : "/config"
}
.backPath=${this._searchParms.has("historyBack")
? undefined
: "/config"}
.route=${this.route}
.tabs=${configSections.devices}
.columns=${this._columns(this.hass.localize)}
@@ -828,16 +824,14 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
{ number: filteredEntities.length }
)}
has-filters
.filters=${
Object.values(this._filters).filter((filter) =>
Array.isArray(filter)
? filter.length
: filter &&
Object.values(filter).some((val) =>
Array.isArray(val) ? val.length : val
)
).length
}
.filters=${Object.values(this._filters).filter((filter) =>
Array.isArray(filter)
? filter.length
: filter &&
Object.values(filter).some((val) =>
Array.isArray(val) ? val.length : val
)
).length}
selectable
.selected=${this._selected.length}
.initialGroupColumn=${this._activeGrouping ?? "device_full"}
@@ -864,157 +858,161 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
slot="toolbar-icon"
></ha-integration-overflow-menu>
${
!this.narrow
? html`<ha-md-button-menu slot="selection-bar">
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.add_label"
)}
>
<ha-svg-icon slot="trailing-icon" .path=${mdiMenuDown}></ha-svg-icon>
</ha-assist-chip>
${labelItems}
</ha-md-button-menu>`
: nothing
}
<ha-md-button-menu has-overflow slot="selection-bar">
${
this.narrow
? html`<ha-assist-chip
.label=${this.hass.localize(
"ui.panel.config.automation.picker.bulk_action"
)}
slot="trigger"
>
<ha-svg-icon slot="trailing-icon" .path=${mdiMenuDown}></ha-svg-icon>
</ha-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(
${!this.narrow
? html`<ha-dropdown slot="selection-bar" @wa-select=${this._handleLabelMenuSelect}>
<ha-assist-chip
slot="trigger"
.label=${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>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>`
: nothing
}
<ha-md-menu-item .clickAction=${this._enableSelected}>
<ha-svg-icon slot="start" .path=${mdiToggleSwitch}></ha-svg-icon>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.entities.picker.enable_selected.button"
)}
</div>
</ha-md-menu-item>
<ha-md-menu-item .clickAction=${this._disableSelected}>
<ha-svg-icon
slot="start"
.path=${mdiToggleSwitchOffOutline}
></ha-svg-icon>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.entities.picker.disable_selected.button"
)}
</div>
</ha-md-menu-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item .clickAction=${this._unhideSelected}>
<ha-svg-icon
slot="start"
.path=${mdiEye}
></ha-svg-icon>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.entities.picker.unhide_selected.button"
)}
</div>
</ha-md-menu-item>
<ha-md-menu-item .clickAction=${this._hideSelected}>
<ha-svg-icon
slot="start"
.path=${mdiEyeOff}
></ha-svg-icon>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.entities.picker.hide_selected.button"
)}
</div>
</ha-md-menu-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item .clickAction=${this._restoreEntityIdSelected}>
<ha-svg-icon
slot="start"
.path=${mdiRestore}
></ha-svg-icon>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.entities.picker.restore_entity_id_selected.button"
)}
</div>
</ha-md-menu-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item .clickAction=${this._removeSelected} class="warning">
<ha-svg-icon
slot="start"
.path=${mdiDelete}
></ha-svg-icon>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.entities.picker.delete_selected.button"
)}
</div>
</ha-md-menu-item>
</ha-md-button-menu>
${
Array.isArray(this._filters.config_entry) &&
this._filters.config_entry.length
? html`<ha-alert slot="filter-pane">
${this.hass.localize(
"ui.panel.config.entities.picker.filtering_by_config_entry"
>
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-assist-chip>
${labelItems}
</ha-dropdown>`
: nothing}
<ha-dropdown slot="selection-bar" @wa-select=${this._handleOverflowMenuSelect}>
${this.narrow
? html`<ha-assist-chip
.label=${this.hass.localize(
"ui.panel.config.automation.picker.bulk_action"
)}
${this._entries?.find(
(entry) => entry.entry_id === this._filters.config_entry![0]
)?.title || this._filters.config_entry[0]}${this._filters
.config_entry.length === 1 &&
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
}
slot="trigger"
>
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></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>`}
${this.narrow
? html`<ha-dropdown-item>
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.add_label"
)}
<ha-svg-icon
slot="details"
.path=${mdiChevronRight}
></ha-svg-icon>
${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"
.value=${label.label_id}
data-action=${selected ? "remove" : "add"}
keep-open
>
<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"></wa-divider>
<ha-dropdown-item slot="submenu" value="__create_label__">
${this.hass.localize("ui.panel.config.labels.add_label")}
</ha-dropdown-item>
</ha-dropdown-item>
<wa-divider></wa-divider>`
: nothing}
<ha-dropdown-item value="enable">
<ha-svg-icon slot="icon" .path=${mdiToggleSwitch}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.entities.picker.enable_selected.button"
)}
</ha-dropdown-item>
<ha-dropdown-item value="disable">
<ha-svg-icon
slot="icon"
.path=${mdiToggleSwitchOffOutline}
></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.entities.picker.disable_selected.button"
)}
</ha-dropdown-item>
<wa-divider></wa-divider>
<ha-dropdown-item value="unhide">
<ha-svg-icon slot="icon" .path=${mdiEye}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.entities.picker.unhide_selected.button"
)}
</ha-dropdown-item>
<ha-dropdown-item value="hide">
<ha-svg-icon slot="icon" .path=${mdiEyeOff}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.entities.picker.hide_selected.button"
)}
</ha-dropdown-item>
<wa-divider></wa-divider>
<ha-dropdown-item .clickAction=${this._restoreEntityIdSelected}>
<ha-svg-icon slot="icon" .path=${mdiRestore}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.entities.picker.restore_entity_id_selected.button"
)}
</ha-dropdown-item>
<wa-divider></wa-divider>
<ha-dropdown-item
value="remove"
variant="danger"
>
<ha-svg-icon slot="icon" .path=${mdiDelete}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.entities.picker.delete_selected.button"
)}
</ha-dropdown-item>
</ha-dropdown>
${Array.isArray(this._filters.config_entry) &&
this._filters.config_entry.length
? html`<ha-alert slot="filter-pane">
${this.hass.localize(
"ui.panel.config.entities.picker.filtering_by_config_entry"
)}
${this._entries?.find(
(entry) => entry.entry_id === this._filters.config_entry![0]
)?.title || this._filters.config_entry[0]}${this._filters
.config_entry.length === 1 &&
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
.hass=${this.hass}
type="entity"
@@ -1075,20 +1073,16 @@ ${
.narrow=${this.narrow}
@expanded-changed=${this._filterExpanded}
></ha-filter-labels>
${
includeAddDeviceFab
? html`<ha-fab
.label=${this.hass.localize(
"ui.panel.config.devices.add_device"
)}
extended
@click=${this._addDevice}
slot="fab"
>
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</ha-fab>`
: nothing
}
${includeAddDeviceFab
? html`<ha-fab
.label=${this.hass.localize("ui.panel.config.devices.add_device")}
extended
@click=${this._addDevice}
slot="fab"
>
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</ha-fab>`
: nothing}
</hass-tabs-subpage-data-table>
`;
}
@@ -1222,6 +1216,55 @@ ${
this._selected = ev.detail.value;
}
private _handleLabelMenuSelect(ev: CustomEvent) {
const item = ev.detail.item as HaDropdownItem;
// Handle label selections (checkbox items with keep-open)
if (item.hasAttribute("keep-open") && item.hasAttribute("data-action")) {
const label = item.value as string;
const action = (item as HTMLElement).dataset.action as "add" | "remove";
this._bulkLabel(label, action);
return;
}
if (item.value === "__create_label__") {
this._bulkCreateLabel();
}
}
private _handleOverflowMenuSelect(ev: CustomEvent) {
const item = ev.detail.item as HaDropdownItem;
// Handle label selections in submenu (checkbox items with keep-open)
if (item.hasAttribute("keep-open") && item.hasAttribute("data-action")) {
const label = item.value as string;
const action = (item as HTMLElement).dataset.action as "add" | "remove";
this._bulkLabel(label, action);
return;
}
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 "__create_label__":
this._bulkCreateLabel();
break;
}
}
private _enableSelected = async () => {
showConfirmationDialog(this, {
title: this.hass.localize(
@@ -1345,12 +1388,6 @@ ${
this._clearSelection();
};
private async _handleBulkLabel(ev) {
const label = ev.currentTarget.value;
const action = ev.currentTarget.action;
await this._bulkLabel(label, action);
}
private async _bulkLabel(label: string, action: "add" | "remove") {
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
this._selected.forEach((entityId) => {
@@ -1612,7 +1649,7 @@ ${rejected
ha-assist-chip {
--ha-assist-chip-container-shape: 10px;
}
ha-md-button-menu ha-assist-chip {
ha-dropdown ha-assist-chip {
--md-assist-chip-trailing-space: 8px;
}
ha-label {

View File

@@ -47,11 +47,15 @@ import "../../../components/ha-filter-categories";
import "../../../components/ha-filter-devices";
import "../../../components/ha-filter-entities";
import "../../../components/ha-filter-floor-areas";
import "@home-assistant/webawesome/dist/components/divider/divider";
import "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import "../../../components/ha-filter-labels";
import "../../../components/ha-icon";
import "../../../components/ha-icon-overflow-menu";
import "../../../components/ha-md-divider";
import "../../../components/ha-md-menu";
import "../../../components/ha-state-icon";
import "../../../components/ha-sub-menu";
import "../../../components/ha-svg-icon";
import "../../../components/ha-tooltip";
import type { CategoryRegistryEntry } from "../../../data/category_registry";
@@ -604,29 +608,24 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
const categoryItems = html`${this._categories?.map(
(category) =>
html`<ha-md-menu-item
html`<ha-dropdown-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>`
? 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-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>`;
<ha-dropdown-item value="__no_category__">
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.no_category"
)}
</ha-dropdown-item>
<wa-divider></wa-divider>
<ha-dropdown-item value="__create_category__">
${this.hass.localize("ui.panel.config.category.editor.add")}
</ha-dropdown-item>`;
const labelItems = html`${this._labels?.map((label) => {
const color = label.color ? computeCssColor(label.color) : undefined;
const selected = this._selected.every((entityId) =>
@@ -637,14 +636,13 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
this._selected.some((entityId) =>
this._labelsForEntity(entityId).includes(label.label_id)
);
return html`<ha-md-menu-item
return html`<ha-dropdown-item
.value=${label.label_id}
.action=${selected ? "remove" : "add"}
@click=${this._handleBulkLabel}
data-action=${selected ? "remove" : "add"}
keep-open
>
<ha-checkbox
slot="start"
slot="icon"
.checked=${selected}
.indeterminate=${partial}
reducedTouchTarget
@@ -658,13 +656,11 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
: 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>`;
</ha-dropdown-item> `;
})}<wa-divider></wa-divider>
<ha-dropdown-item value="__create_label__">
${this.hass.localize("ui.panel.config.labels.add_label")}
</ha-dropdown-item>`;
const labelsInOverflow =
(this._sizeController.value && this._sizeController.value < 700) ||
(!this._sizeController.value && this.hass.dockedSidebar === "docked");
@@ -766,7 +762,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
></ha-filter-categories>
${!this.narrow
? html`<ha-md-button-menu slot="selection-bar">
? html`<ha-dropdown slot="selection-bar" @wa-select=${this._handleCategoryMenuSelect}>
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
@@ -779,10 +775,10 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
></ha-svg-icon>
</ha-assist-chip>
${categoryItems}
</ha-md-button-menu>
</ha-dropdown>
${labelsInOverflow
? nothing
: html`<ha-md-button-menu slot="selection-bar">
: html`<ha-dropdown slot="selection-bar" @wa-select=${this._handleLabelMenuSelect}>
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
@@ -795,13 +791,11 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
></ha-svg-icon>
</ha-assist-chip>
${labelItems}
</ha-md-button-menu>`}`
</ha-dropdown>`}`
: nothing}
${this.narrow || labelsInOverflow
? html`
<ha-md-button-menu has-overflow slot="selection-bar">
${
this.narrow
? html` <ha-dropdown slot="selection-bar" @wa-select=${this._handleOverflowMenuSelect}>
${this.narrow
? html`<ha-assist-chip
.label=${this.hass.localize(
"ui.panel.config.automation.picker.bulk_action"
@@ -819,50 +813,88 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
"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
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>`
></ha-icon-button>`}
${this.narrow
? html`<ha-dropdown-item>
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.move_category"
)}
<ha-svg-icon
slot="details"
.path=${mdiChevronRight}
></ha-svg-icon>
${this._categories?.map(
(category) =>
html`<ha-dropdown-item
slot="submenu"
.value=${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" value="__no_category__">
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.no_category"
)}
</ha-dropdown-item>
<wa-divider slot="submenu"></wa-divider>
<ha-dropdown-item slot="submenu" value="__create_category__">
${this.hass.localize("ui.panel.config.category.editor.add")}
</ha-dropdown-item>
</ha-dropdown-item>`
: nothing}
${this.narrow || this.hass.dockedSidebar === "docked"
? html` <ha-dropdown-item>
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.add_label"
)}
<ha-svg-icon
slot="details"
.path=${mdiChevronRight}
></ha-svg-icon>
${this._labels?.map((label) => {
const color = label.color ? computeCssColor(label.color) : undefined;
const selected = this._selected.every((entityId) =>
this._labelsForEntity(entityId).includes(label.label_id)
);
const partial =
!selected &&
this._selected.some((entityId) =>
this._labelsForEntity(entityId).includes(label.label_id)
);
return html`<ha-dropdown-item
slot="submenu"
.value=${label.label_id}
data-action=${selected ? "remove" : "add"}
keep-open
>
<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"></wa-divider>
<ha-dropdown-item slot="submenu" value="__create_label__">
${this.hass.localize("ui.panel.config.labels.add_label")}
</ha-dropdown-item>
</ha-dropdown-item>`
: nothing}
</ha-dropdown>`
: nothing}
<ha-integration-overflow-menu
@@ -1076,6 +1108,71 @@ ${rejected
this._selected = ev.detail.value;
}
private _handleCategoryMenuSelect(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;
default:
if (item.value) {
this._bulkAddCategory(item.value as string);
}
break;
}
}
private _handleLabelMenuSelect(ev: CustomEvent) {
const item = ev.detail.item as HaDropdownItem;
// Handle label selections (checkbox items with keep-open)
if (item.hasAttribute("keep-open") && item.hasAttribute("data-action")) {
const label = item.value as string;
const action = (item as HTMLElement).dataset.action as "add" | "remove";
this._bulkLabel(label, action);
return;
}
if (item.value === "__create_label__") {
this._bulkCreateLabel();
}
}
private _handleOverflowMenuSelect(ev: CustomEvent) {
const item = ev.detail.item as HaDropdownItem;
// Handle label selections in submenu (checkbox items with keep-open)
if (item.hasAttribute("keep-open") && item.hasAttribute("data-action")) {
const label = item.value as string;
const action = (item as HTMLElement).dataset.action as "add" | "remove";
this._bulkLabel(label, action);
return;
}
switch (item.value) {
case "__no_category__":
this._bulkAddCategory(null);
break;
case "__create_category__":
this._bulkCreateCategory();
break;
case "__create_label__":
this._bulkCreateLabel();
break;
default:
if (item.value && typeof item.value === "string" && !item.value.startsWith("__")) {
// Check if it's a category_id or label_id
if (this._categories?.some(c => c.category_id === item.value)) {
this._bulkAddCategory(item.value);
}
}
break;
}
}
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
@@ -1416,7 +1513,7 @@ ${rejected
ha-assist-chip {
--ha-assist-chip-container-shape: 10px;
}
ha-md-button-menu ha-assist-chip {
ha-dropdown ha-assist-chip {
--md-assist-chip-trailing-space: 8px;
}
ha-label {

View File

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

View File

@@ -6,6 +6,7 @@ import {
mdiContentDuplicate,
mdiDelete,
mdiDotsVertical,
mdiFloorPlan,
mdiHelpCircle,
mdiInformationOutline,
mdiMenuDown,
@@ -52,11 +53,11 @@ import "../../../components/ha-filter-floor-areas";
import "../../../components/ha-filter-labels";
import "../../../components/ha-icon-button";
import "../../../components/ha-icon-overflow-menu";
import "../../../components/ha-md-divider";
import "../../../components/ha-md-menu";
import "../../../components/ha-md-menu-item";
import "@home-assistant/webawesome/dist/components/divider/divider";
import "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import type { HaDropdownItem } from "../../../components/ha-dropdown-item";
import "../../../components/ha-state-icon";
import "../../../components/ha-sub-menu";
import "../../../components/ha-svg-icon";
import "../../../components/ha-tooltip";
import { createAreaRegistryEntry } from "../../../data/area_registry";
@@ -437,29 +438,24 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
protected render(): TemplateResult {
const categoryItems = html`${this._categories?.map(
(category) =>
html`<ha-md-menu-item
.value=${category.category_id}
.clickAction=${this._handleBulkCategory}
html`<ha-dropdown-item
value=${category.category_id}
>
${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>`
? 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-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>`;
<ha-dropdown-item value="no-category">
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.no_category"
)}
</ha-dropdown-item>
<wa-divider></wa-divider>
<ha-dropdown-item value="create-category">
${this.hass.localize("ui.panel.config.category.editor.add")}
</ha-dropdown-item>`;
const labelItems = html` ${this._labels?.map((label) => {
const color = label.color ? computeCssColor(label.color) : undefined;
@@ -471,18 +467,12 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
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
return html`<ha-dropdown-item
value=${label.label_id}
data-action=${selected ? "remove" : "add"}
type="checkbox"
.checked=${selected}
>
<ha-checkbox
slot="start"
.checked=${selected}
.indeterminate=${partial}
reducedTouchTarget
></ha-checkbox>
<ha-label
style=${color ? `--color: ${color}` : ""}
.description=${label.description}
@@ -492,45 +482,39 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
: nothing}
${label.name}
</ha-label>
</ha-md-menu-item>`;
</ha-dropdown-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
>`;
<wa-divider></wa-divider>
<ha-dropdown-item value="create-label">
${this.hass.localize("ui.panel.config.labels.add_label")}
</ha-dropdown-item>`;
const areaItems = html`${Object.values(this.hass.areas).map(
(area) =>
html`<ha-md-menu-item
html`<ha-dropdown-item
.value=${area.area_id}
.clickAction=${this._handleBulkArea}
>
${area.icon
? html`<ha-icon slot="start" .icon=${area.icon}></ha-icon>`
? html`<ha-icon slot="icon" .icon=${area.icon}></ha-icon>`
: html`<ha-svg-icon
slot="start"
slot="icon"
.path=${mdiTextureBox}
></ha-svg-icon>`}
<div slot="headline">${area.name}</div>
</ha-md-menu-item>`
${area.name}
</ha-dropdown-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>`;
<ha-dropdown-item value="no-area">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.no_area"
)}
</ha-dropdown-item>
<wa-divider></wa-divider>
<ha-dropdown-item value="create-area">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.add_area"
)}
</ha-dropdown-item>`;
const areasInOverflow =
(this._sizeController.value && this._sizeController.value < 900) ||
@@ -654,7 +638,7 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
></ha-filter-categories>
${!this.narrow
? html`<ha-md-button-menu slot="selection-bar">
? html`<ha-dropdown slot="selection-bar" @wa-select=${this._handleCategoryMenuSelect}>
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
@@ -667,10 +651,10 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
></ha-svg-icon>
</ha-assist-chip>
${categoryItems}
</ha-md-button-menu>
</ha-dropdown>
${labelsInOverflow
? nothing
: html`<ha-md-button-menu slot="selection-bar">
: html`<ha-dropdown slot="selection-bar" @wa-select=${this._handleLabelMenuSelect}>
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
@@ -683,10 +667,10 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
></ha-svg-icon>
</ha-assist-chip>
${labelItems}
</ha-md-button-menu>`}
</ha-dropdown>`}
${areasInOverflow
? nothing
: html`<ha-md-button-menu slot="selection-bar">
: html`<ha-dropdown slot="selection-bar" @wa-select=${this._handleAreaMenuSelect}>
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
@@ -699,13 +683,11 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
></ha-svg-icon>
</ha-assist-chip>
${areaItems}
</ha-md-button-menu>`}`
</ha-dropdown>`}`
: nothing}
${this.narrow || areasInOverflow
? html`
<ha-md-button-menu has-overflow slot="selection-bar">
${
this.narrow
? html` <ha-dropdown slot="selection-bar" @wa-select=${this._handleOverflowMenuSelect}>
${this.narrow
? html`<ha-assist-chip
.label=${this.hass.localize(
"ui.panel.config.automation.picker.bulk_action"
@@ -723,68 +705,119 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
"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
slot="end"
.path=${mdiChevronRight}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu slot="menu">${categoryItems}</ha-md-menu>
</ha-sub-menu>`
: nothing
}
${
this.narrow || labelsInOverflow
? html`<ha-sub-menu>
<ha-md-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.add_label"
)}
</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>`
></ha-icon-button>`}
${this.narrow
? html`<ha-dropdown-item>
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.move_category"
)}
<ha-svg-icon
slot="details"
.path=${mdiChevronRight}
></ha-svg-icon>
${this._categories?.map(
(category) =>
html`<ha-dropdown-item
slot="submenu"
value=${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" value="no-category">
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.no_category"
)}
</ha-dropdown-item>
<wa-divider slot="submenu"></wa-divider>
<ha-dropdown-item slot="submenu" value="create-category">
${this.hass.localize("ui.panel.config.category.editor.add")}
</ha-dropdown-item>
</ha-dropdown-item>`
: nothing}
${this.narrow || labelsInOverflow
? html`<ha-dropdown-item>
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.add_label"
)}
<ha-svg-icon
slot="details"
.path=${mdiChevronRight}
></ha-svg-icon>
${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"
value=${label.label_id}
data-action=${selected ? "remove" : "add"}
>
<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"></wa-divider>
<ha-dropdown-item slot="submenu" value="create-label">
${this.hass.localize("ui.panel.config.label_registry.editor.add")}
</ha-dropdown-item>
</ha-dropdown-item>`
: nothing}
${this.narrow || areasInOverflow
? html`<ha-dropdown-item>
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.move_area"
)}
<ha-svg-icon
slot="details"
.path=${mdiChevronRight}
></ha-svg-icon>
${this._areas?.map(
(area) =>
html`<ha-dropdown-item
slot="submenu"
value=${area.area_id}
>
${area.icon
? html`<ha-icon slot="icon" .icon=${area.icon}></ha-icon>`
: html`<ha-svg-icon slot="icon" .path=${mdiFloorPlan}></ha-svg-icon>`}
${area.name}
</ha-dropdown-item>`
)}
<ha-dropdown-item slot="submenu" value="no-area">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.no_area"
)}
</ha-dropdown-item>
<wa-divider slot="submenu"></wa-divider>
<ha-dropdown-item slot="submenu" value="create-area">
${this.hass.localize("ui.panel.config.area_registry.editor.add")}
</ha-dropdown-item>
</ha-dropdown-item>`
: nothing}
</ha-dropdown>`
: nothing}
${!this.scenes.length
? html`<div class="empty" slot="empty">
@@ -955,9 +988,77 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
}
}
private _handleBulkCategory = (item) => {
const category = item.value;
this._bulkAddCategory(category);
private _handleCategoryMenuSelect = (ev: CustomEvent) => {
const item = ev.detail.item as HaDropdownItem;
if (item.value === "create-category") {
this._bulkCreateCategory();
return;
}
if (item.value === "no-category") {
this._bulkAddCategory(null!);
return;
}
this._bulkAddCategory(item.value);
};
private _handleLabelMenuSelect = (ev: CustomEvent) => {
const item = ev.detail.item as HaDropdownItem & { dataset: { action?: string } };
if (item.value === "create-label") {
this._bulkCreateLabel();
return;
}
const action = item.dataset.action as "add" | "remove";
this._bulkLabel(item.value, action);
};
private _handleAreaMenuSelect = (ev: CustomEvent) => {
const item = ev.detail.item as HaDropdownItem;
if (item.value === "create-area") {
this._bulkCreateArea();
return;
}
if (item.value === "no-area") {
this._bulkAddArea(null!);
return;
}
this._bulkAddArea(item.value);
};
private _handleOverflowMenuSelect = (ev: CustomEvent) => {
const item = ev.detail.item as HaDropdownItem & { dataset: { action?: string } };
if (item.value === "create-category") {
this._bulkCreateCategory();
return;
}
if (item.value === "no-category") {
this._bulkAddCategory(null!);
return;
}
if (item.value === "create-label") {
this._bulkCreateLabel();
return;
}
if (item.value === "create-area") {
this._bulkCreateArea();
return;
}
if (item.value === "no-area") {
this._bulkAddArea(null!);
return;
}
// Check if it's a label action
if (item.dataset.action) {
const action = item.dataset.action as "add" | "remove";
this._bulkLabel(item.value, action);
return;
}
// Check if it's an area
if (this.hass.areas[item.value]) {
this._bulkAddArea(item.value);
return;
}
// Otherwise it's a category
this._bulkAddCategory(item.value);
};
private async _bulkAddCategory(category: string) {
@@ -985,12 +1086,6 @@ ${rejected
}
}
private async _handleBulkLabel(ev) {
const label = ev.currentTarget.value;
const action = ev.currentTarget.action;
this._bulkLabel(label, action);
}
private async _bulkLabel(label: string, action: "add" | "remove") {
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
this._selected.forEach((entityId) => {
@@ -1021,11 +1116,6 @@ ${rejected
}
}
private _handleBulkArea = (item) => {
const area = item.value;
this._bulkAddArea(area);
};
private async _bulkAddArea(area: string) {
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
this._selected.forEach((entityId) => {
@@ -1231,7 +1321,7 @@ ${rejected
ha-assist-chip {
--ha-assist-chip-container-shape: 10px;
}
ha-md-button-menu ha-assist-chip {
ha-dropdown ha-assist-chip {
--md-assist-chip-trailing-space: 8px;
}
ha-label {

View File

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

View File

@@ -53,13 +53,13 @@ import "../../../components/ha-filter-categories";
import "../../../components/ha-filter-devices";
import "../../../components/ha-filter-entities";
import "../../../components/ha-filter-floor-areas";
import "@home-assistant/webawesome/dist/components/divider/divider";
import "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import type { HaDropdownItem } from "../../../components/ha-dropdown-item";
import "../../../components/ha-filter-labels";
import "../../../components/ha-icon-button";
import "../../../components/ha-icon-overflow-menu";
import "../../../components/ha-md-divider";
import "../../../components/ha-md-menu";
import "../../../components/ha-md-menu-item";
import "../../../components/ha-sub-menu";
import "../../../components/ha-svg-icon";
import { createAreaRegistryEntry } from "../../../data/area_registry";
import type { CategoryRegistryEntry } from "../../../data/category_registry";
@@ -422,28 +422,24 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
protected render(): TemplateResult {
const categoryItems = html`${this._categories?.map(
(category) =>
html`<ha-md-menu-item
.value=${category.category_id}
.clickAction=${this._handleBulkCategory}
html`<ha-dropdown-item
value=${category.category_id}
>
${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>`
? 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-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>`;
<ha-dropdown-item value="no-category">
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.no_category"
)}
</ha-dropdown-item>
<wa-divider></wa-divider>
<ha-dropdown-item value="create-category">
${this.hass.localize("ui.panel.config.category.editor.add")}
</ha-dropdown-item>`;
const labelItems = html`${this._labels?.map((label) => {
const color = label.color ? computeCssColor(label.color) : undefined;
@@ -455,18 +451,12 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
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
reducedTouchTarget
return html`<ha-dropdown-item
value=${label.label_id}
data-action=${selected ? "remove" : "add"}
type="checkbox"
.checked=${selected}
>
<ha-checkbox
slot="start"
.checked=${selected}
.indeterminate=${partial}
></ha-checkbox>
<ha-label
style=${color ? `--color: ${color}` : ""}
.description=${label.description}
@@ -476,45 +466,38 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
: nothing}
${label.name}
</ha-label>
</ha-md-menu-item>`;
</ha-dropdown-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
>`;
<wa-divider></wa-divider>
<ha-dropdown-item value="create-label">
${this.hass.localize("ui.panel.config.labels.add_label")}
</ha-dropdown-item>`;
const areaItems = html`${Object.values(this.hass.areas).map(
(area) =>
html`<ha-md-menu-item
.value=${area.area_id}
.clickAction=${this._handleBulkArea}
html`<ha-dropdown-item
value=${area.area_id}
>
${area.icon
? html`<ha-icon slot="start" .icon=${area.icon}></ha-icon>`
? html`<ha-icon slot="icon" .icon=${area.icon}></ha-icon>`
: html`<ha-svg-icon
slot="start"
slot="icon"
.path=${mdiTextureBox}
></ha-svg-icon>`}
<div slot="headline">${area.name}</div>
</ha-md-menu-item>`
${area.name}
</ha-dropdown-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>`;
<ha-dropdown-item value="no-area">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.no_area"
)}
</ha-dropdown-item>
<wa-divider></wa-divider>
<ha-dropdown-item value="create-area">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.add_area"
)}
</ha-dropdown-item>`;
const areasInOverflow =
(this._sizeController.value && this._sizeController.value < 900) ||
@@ -647,7 +630,7 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
></ha-filter-blueprints>
${!this.narrow
? html`<ha-md-button-menu slot="selection-bar">
? html`<ha-dropdown slot="selection-bar" @wa-select=${this._handleCategoryMenuSelect}>
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
@@ -660,10 +643,10 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
></ha-svg-icon>
</ha-assist-chip>
${categoryItems}
</ha-md-button-menu>
</ha-dropdown>
${labelsInOverflow
? nothing
: html`<ha-md-button-menu slot="selection-bar">
: html`<ha-dropdown slot="selection-bar" @wa-select=${this._handleLabelMenuSelect}>
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
@@ -676,10 +659,10 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
></ha-svg-icon>
</ha-assist-chip>
${labelItems}
</ha-md-button-menu>`}
</ha-dropdown>`}
${areasInOverflow
? nothing
: html`<ha-md-button-menu slot="selection-bar">
: html`<ha-dropdown slot="selection-bar" @wa-select=${this._handleAreaMenuSelect}>
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
@@ -692,13 +675,11 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
></ha-svg-icon>
</ha-assist-chip>
${areaItems}
</ha-md-button-menu>`}`
</ha-dropdown>`}`
: nothing}
${this.narrow || areasInOverflow
? html`
<ha-md-button-menu has-overflow slot="selection-bar">
${
this.narrow
? html` <ha-dropdown slot="selection-bar" @wa-select=${this._handleOverflowMenuSelect}>
${this.narrow
? html`<ha-assist-chip
.label=${this.hass.localize(
"ui.panel.config.automation.picker.bulk_action"
@@ -716,68 +697,110 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
"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
slot="end"
.path=${mdiChevronRight}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu slot="menu">${categoryItems}</ha-md-menu>
</ha-sub-menu>`
: nothing
}
${
this.narrow || labelsInOverflow
? html`<ha-sub-menu>
<ha-md-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.add_label"
)}
</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>`
></ha-icon-button>`}
${this.narrow
? html`<ha-dropdown-item>
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.move_category"
)}
<ha-svg-icon
slot="details"
.path=${mdiChevronRight}
></ha-svg-icon>
${this._categories?.map(
(category) =>
html`<ha-dropdown-item
slot="submenu"
value=${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" value="no-category">
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.no_category"
)}
</ha-dropdown-item>
<wa-divider slot="submenu"></wa-divider>
<ha-dropdown-item slot="submenu" value="create-category">
${this.hass.localize("ui.panel.config.category.editor.add")}
</ha-dropdown-item>
</ha-dropdown-item>`
: nothing}
${this.narrow || labelsInOverflow
? html`<ha-dropdown-item>
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.add_label"
)}
<ha-svg-icon
slot="details"
.path=${mdiChevronRight}
></ha-svg-icon>
${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)
);
return html`<ha-dropdown-item
slot="submenu"
value=${label.label_id}
data-action=${selected ? "remove" : "add"}
type="checkbox"
.checked=${selected}
>
<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"></wa-divider>
<ha-dropdown-item slot="submenu" value="create-label">
${this.hass.localize("ui.panel.config.label_registry.editor.add")}
</ha-dropdown-item>
</ha-dropdown-item>`
: nothing}
${this.narrow || areasInOverflow
? html`<ha-dropdown-item>
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.move_area"
)}
<ha-svg-icon
slot="details"
.path=${mdiChevronRight}
></ha-svg-icon>
${Object.values(this.hass.areas).map(
(area) =>
html`<ha-dropdown-item
slot="submenu"
value=${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" value="no-area">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.no_area"
)}
</ha-dropdown-item>
<wa-divider slot="submenu"></wa-divider>
<ha-dropdown-item slot="submenu" value="create-area">
${this.hass.localize("ui.panel.config.area_registry.editor.add")}
</ha-dropdown-item>
</ha-dropdown-item>`
: nothing}
</ha-dropdown>`
: nothing}
${!this.scripts.length
? html` <div class="empty" slot="empty">
@@ -995,9 +1018,77 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
this._selected = ev.detail.value;
}
private _handleBulkCategory = (item) => {
const category = item.value;
this._bulkAddCategory(category);
private _handleCategoryMenuSelect = (ev: CustomEvent) => {
const item = ev.detail.item as HaDropdownItem;
if (item.value === "create-category") {
this._bulkCreateCategory();
return;
}
if (item.value === "no-category") {
this._bulkAddCategory(null!);
return;
}
this._bulkAddCategory(item.value);
};
private _handleLabelMenuSelect = (ev: CustomEvent) => {
const item = ev.detail.item as HaDropdownItem & { dataset: { action?: string } };
if (item.value === "create-label") {
this._bulkCreateLabel();
return;
}
const action = item.dataset.action as "add" | "remove";
this._bulkLabel(item.value, action);
};
private _handleAreaMenuSelect = (ev: CustomEvent) => {
const item = ev.detail.item as HaDropdownItem;
if (item.value === "create-area") {
this._bulkCreateArea();
return;
}
if (item.value === "no-area") {
this._bulkAddArea(null!);
return;
}
this._bulkAddArea(item.value);
};
private _handleOverflowMenuSelect = (ev: CustomEvent) => {
const item = ev.detail.item as HaDropdownItem & { dataset: { action?: string } };
if (item.value === "create-category") {
this._bulkCreateCategory();
return;
}
if (item.value === "no-category") {
this._bulkAddCategory(null!);
return;
}
if (item.value === "create-label") {
this._bulkCreateLabel();
return;
}
if (item.value === "create-area") {
this._bulkCreateArea();
return;
}
if (item.value === "no-area") {
this._bulkAddArea(null!);
return;
}
// Check if it's a label action
if (item.dataset.action) {
const action = item.dataset.action as "add" | "remove";
this._bulkLabel(item.value, action);
return;
}
// Check if it's an area
if (this.hass.areas[item.value]) {
this._bulkAddArea(item.value);
return;
}
// Otherwise it's a category
this._bulkAddCategory(item.value);
};
private async _bulkAddCategory(category: string) {
@@ -1025,12 +1116,6 @@ ${rejected
}
}
private async _handleBulkLabel(ev) {
const label = ev.currentTarget.value;
const action = ev.currentTarget.action;
this._bulkLabel(label, action);
}
private async _bulkLabel(label: string, action: "add" | "remove") {
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
this._selected.forEach((entityId) => {
@@ -1235,11 +1320,6 @@ ${rejected
});
};
private _handleBulkArea = (item) => {
const area = item.value;
this._bulkAddArea(area);
};
private async _bulkAddArea(area: string) {
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
this._selected.forEach((entityId) => {
@@ -1322,7 +1402,7 @@ ${rejected
ha-assist-chip {
--ha-assist-chip-container-shape: 10px;
}
ha-md-button-menu ha-assist-chip {
ha-dropdown ha-assist-chip {
--md-assist-chip-trailing-space: 8px;
}
ha-label {

View File

@@ -4,7 +4,6 @@ import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { navigate } from "../../common/navigate";
import { withViewTransition } from "../../common/util/view-transition";
import "../../components/ha-button-menu";
import "../../components/ha-icon-button";
import "../../components/ha-list-item";
@@ -117,9 +116,7 @@ class PanelDeveloperTools extends LitElement {
return;
}
if (newPage !== this._page) {
withViewTransition(() => {
navigate(`/developer-tools/${newPage}`);
});
navigate(`/developer-tools/${newPage}`);
} else {
scrollTo({ behavior: "smooth", top: 0 });
}
@@ -128,9 +125,7 @@ class PanelDeveloperTools extends LitElement {
private async _handleMenuAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
withViewTransition(() => {
navigate(`/developer-tools/debug`);
});
navigate(`/developer-tools/debug`);
break;
}
}

View File

@@ -99,7 +99,6 @@ import type { HUIView } from "./views/hui-view";
import "./views/hui-view-background";
import "./views/hui-view-container";
import { UndoRedoController } from "../../common/controllers/undo-redo-controller";
import { withViewTransition } from "../../common/util/view-transition";
interface ActionItem {
icon: string;
@@ -1246,9 +1245,7 @@ class HUIRoot extends LitElement {
const viewIndex = Number(ev.detail.name);
if (viewIndex !== this._curView) {
const path = this.config.views[viewIndex].path || viewIndex;
withViewTransition(() => {
this._navigateToView(path);
});
this._navigateToView(path);
} else if (!this._editMode) {
scrollTo({ behavior: "smooth", top: 0 });
}