mirror of
				https://github.com/home-assistant/frontend.git
				synced 2025-10-31 06:29:43 +00:00 
			
		
		
		
	Compare commits
	
		
			1 Commits
		
	
	
		
			20240403.1
			...
			fix-menu-o
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 29a103e884 | 
| @@ -187,7 +187,7 @@ export class DemoHaControlSelect extends LitElement { | ||||
|         --mdc-icon-size: 24px; | ||||
|         --control-select-color: var(--state-fan-active-color); | ||||
|         --control-select-thickness: 130px; | ||||
|         --control-select-border-radius: 36px; | ||||
|         --control-select-border-radius: 48px; | ||||
|       } | ||||
|       .vertical-selects { | ||||
|         height: 300px; | ||||
|   | ||||
| @@ -151,7 +151,7 @@ export class DemoHaBarSlider extends LitElement { | ||||
|         --control-slider-background: #ffcf4c; | ||||
|         --control-slider-background-opacity: 0.2; | ||||
|         --control-slider-thickness: 130px; | ||||
|         --control-slider-border-radius: 36px; | ||||
|         --control-slider-border-radius: 48px; | ||||
|       } | ||||
|       .vertical-sliders { | ||||
|         height: 300px; | ||||
|   | ||||
| @@ -118,7 +118,7 @@ export class DemoHaControlSwitch extends LitElement { | ||||
|         --control-switch-on-color: var(--green-color); | ||||
|         --control-switch-off-color: var(--red-color); | ||||
|         --control-switch-thickness: 130px; | ||||
|         --control-switch-border-radius: 36px; | ||||
|         --control-switch-border-radius: 48px; | ||||
|         --control-switch-padding: 6px; | ||||
|         --mdc-icon-size: 24px; | ||||
|       } | ||||
|   | ||||
| @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" | ||||
|  | ||||
| [project] | ||||
| name         = "home-assistant-frontend" | ||||
| version      = "20240403.1" | ||||
| version      = "20240402.0" | ||||
| license      = {text = "Apache-2.0"} | ||||
| description  = "The Home Assistant frontend" | ||||
| readme       = "README.md" | ||||
|   | ||||
| @@ -45,8 +45,8 @@ export class HaAssistChip extends MdAssistChip { | ||||
|         margin-inline-start: var(--_icon-label-space); | ||||
|       } | ||||
|       ::before { | ||||
|         background: var(--ha-assist-chip-container-color, transparent); | ||||
|         opacity: var(--ha-assist-chip-container-opacity, 1); | ||||
|         background: var(--ha-assist-chip-container-color); | ||||
|         opacity: var(--ha-assist-chip-container-opacity); | ||||
|       } | ||||
|       :where(.active)::before { | ||||
|         background: var(--ha-assist-chip-active-container-color); | ||||
|   | ||||
| @@ -33,7 +33,6 @@ import "../ha-svg-icon"; | ||||
| import "../search-input"; | ||||
| import { filterData, sortData } from "./sort-filter"; | ||||
| import { groupBy } from "../../common/util/group-by"; | ||||
| import { stringCompare } from "../../common/string/compare"; | ||||
|  | ||||
| declare global { | ||||
|   // for fire event | ||||
| @@ -530,13 +529,7 @@ export class HaDataTable extends LitElement { | ||||
|         const sorted: { | ||||
|           [key: string]: DataTableRowData[]; | ||||
|         } = Object.keys(grouped) | ||||
|           .sort((a, b) => | ||||
|             stringCompare( | ||||
|               ["", "-", "—"].includes(a) ? "zzz" : a, | ||||
|               ["", "-", "—"].includes(b) ? "zzz" : b, | ||||
|               this.hass.locale.language | ||||
|             ) | ||||
|           ) | ||||
|           .sort() | ||||
|           .reduce((obj, key) => { | ||||
|             obj[key] = grouped[key]; | ||||
|             return obj; | ||||
|   | ||||
| @@ -1,9 +1,8 @@ | ||||
| import { mdiTextureBox } from "@mdi/js"; | ||||
| import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit"; | ||||
| import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; | ||||
| import { LitElement, PropertyValues, TemplateResult, html, nothing } from "lit"; | ||||
| import { LitElement, PropertyValues, TemplateResult, html } from "lit"; | ||||
| import { customElement, property, query, state } from "lit/decorators"; | ||||
| import { styleMap } from "lit/directives/style-map"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { fireEvent } from "../common/dom/fire_event"; | ||||
| import { computeDomain } from "../common/entity/compute_domain"; | ||||
| @@ -12,7 +11,6 @@ import { | ||||
|   ScorableTextItem, | ||||
|   fuzzyFilterSort, | ||||
| } from "../common/string/filter/sequence-matching"; | ||||
| import { computeRTL } from "../common/util/compute_rtl"; | ||||
| import { AreaRegistryEntry } from "../data/area_registry"; | ||||
| import { | ||||
|   DeviceEntityDisplayLookup, | ||||
| @@ -34,7 +32,6 @@ import "./ha-floor-icon"; | ||||
| import "./ha-icon-button"; | ||||
| import "./ha-list-item"; | ||||
| import "./ha-svg-icon"; | ||||
| import "./ha-tree-indicator"; | ||||
|  | ||||
| type ScorableAreaFloorEntry = ScorableTextItem & FloorAreaEntry; | ||||
|  | ||||
| @@ -44,11 +41,28 @@ interface FloorAreaEntry { | ||||
|   icon: string | null; | ||||
|   strings: string[]; | ||||
|   type: "floor" | "area"; | ||||
|   level: number | null; | ||||
|   hasFloor?: boolean; | ||||
|   lastArea?: boolean; | ||||
|   level: number | null; | ||||
| } | ||||
|  | ||||
| const rowRenderer: ComboBoxLitRenderer<FloorAreaEntry> = (item) => | ||||
|   html`<ha-list-item | ||||
|     graphic="icon" | ||||
|     style=${item.type === "area" && item.hasFloor | ||||
|       ? "--mdc-list-side-padding-left: 48px;" | ||||
|       : ""} | ||||
|   > | ||||
|     ${item.type === "floor" | ||||
|       ? html`<ha-floor-icon slot="graphic" .floor=${item}></ha-floor-icon>` | ||||
|       : item.icon | ||||
|         ? html`<ha-icon slot="graphic" .icon=${item.icon}></ha-icon>` | ||||
|         : html`<ha-svg-icon | ||||
|             slot="graphic" | ||||
|             .path=${mdiTextureBox} | ||||
|           ></ha-svg-icon>`} | ||||
|     ${item.name} | ||||
|   </ha-list-item>`; | ||||
|  | ||||
| @customElement("ha-area-floor-picker") | ||||
| export class HaAreaFloorPicker extends SubscribeMixin(LitElement) { | ||||
|   @property({ attribute: false }) public hass!: HomeAssistant; | ||||
| @@ -137,44 +151,6 @@ export class HaAreaFloorPicker extends SubscribeMixin(LitElement) { | ||||
|     await this.comboBox?.focus(); | ||||
|   } | ||||
|  | ||||
|   private _rowRenderer: ComboBoxLitRenderer<FloorAreaEntry> = (item) => { | ||||
|     const rtl = computeRTL(this.hass); | ||||
|     return html` | ||||
|       <ha-list-item | ||||
|         graphic="icon" | ||||
|         style=${item.type === "area" && item.hasFloor | ||||
|           ? rtl | ||||
|             ? "--mdc-list-side-padding-right: 48px;" | ||||
|             : "--mdc-list-side-padding-left: 48px;" | ||||
|           : ""} | ||||
|       > | ||||
|         ${item.type === "area" && item.hasFloor | ||||
|           ? html`<ha-tree-indicator | ||||
|               style=${styleMap({ | ||||
|                 width: "48px", | ||||
|                 position: "absolute", | ||||
|                 top: "0px", | ||||
|                 left: rtl ? undefined : "8px", | ||||
|                 right: rtl ? "8px" : undefined, | ||||
|                 transform: rtl ? "scaleX(-1)" : "", | ||||
|               })} | ||||
|               .end=${item.lastArea} | ||||
|               slot="graphic" | ||||
|             ></ha-tree-indicator>` | ||||
|           : nothing} | ||||
|         ${item.type === "floor" | ||||
|           ? html`<ha-floor-icon slot="graphic" .floor=${item}></ha-floor-icon>` | ||||
|           : item.icon | ||||
|             ? html`<ha-icon slot="graphic" .icon=${item.icon}></ha-icon>` | ||||
|             : html`<ha-svg-icon | ||||
|                 slot="graphic" | ||||
|                 .path=${mdiTextureBox} | ||||
|               ></ha-svg-icon>`} | ||||
|         ${item.name} | ||||
|       </ha-list-item> | ||||
|     `; | ||||
|   }; | ||||
|  | ||||
|   private _getAreas = memoizeOne( | ||||
|     ( | ||||
|       floors: FloorRegistryEntry[], | ||||
| @@ -388,7 +364,7 @@ export class HaAreaFloorPicker extends SubscribeMixin(LitElement) { | ||||
|           }); | ||||
|         } | ||||
|         output.push( | ||||
|           ...floorAreas.map((area, index, array) => ({ | ||||
|           ...floorAreas.map((area) => ({ | ||||
|             id: area.area_id, | ||||
|             type: "area" as const, | ||||
|             name: area.name, | ||||
| @@ -396,7 +372,6 @@ export class HaAreaFloorPicker extends SubscribeMixin(LitElement) { | ||||
|             strings: [area.area_id, ...area.aliases, area.name], | ||||
|             hasFloor: true, | ||||
|             level: null, | ||||
|             lastArea: index === array.length - 1, | ||||
|           })) | ||||
|         ); | ||||
|       }); | ||||
| @@ -470,7 +445,7 @@ export class HaAreaFloorPicker extends SubscribeMixin(LitElement) { | ||||
|         .placeholder=${this.placeholder | ||||
|           ? this.hass.areas[this.placeholder]?.name | ||||
|           : undefined} | ||||
|         .renderer=${this._rowRenderer} | ||||
|         .renderer=${rowRenderer} | ||||
|         @filter-changed=${this._filterChanged} | ||||
|         @opened-changed=${this._openedChanged} | ||||
|         @value-changed=${this._areaChanged} | ||||
|   | ||||
| @@ -428,8 +428,6 @@ export class HaAreaPicker extends LitElement { | ||||
|  | ||||
|     (ev.target as any).value = this._value; | ||||
|  | ||||
|     this.hass.loadFragmentTranslation("config"); | ||||
|  | ||||
|     showAreaRegistryDetailDialog(this, { | ||||
|       suggestedName: newValue === ADD_NEW_SUGGESTION_ID ? this._suggestion : "", | ||||
|       createEntry: async (values) => { | ||||
|   | ||||
| @@ -1,13 +1,12 @@ | ||||
| import { SelectedDetail } from "@material/mwc-list"; | ||||
| import "@material/mwc-menu/mwc-menu-surface"; | ||||
| import { mdiFilterVariantRemove } from "@mdi/js"; | ||||
| import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { fireEvent } from "../common/dom/fire_event"; | ||||
| import { Blueprints, fetchBlueprints } from "../data/blueprint"; | ||||
| import { findRelated, RelatedResult } from "../data/search"; | ||||
| import { haStyleScrollbar } from "../resources/styles"; | ||||
| import type { HomeAssistant } from "../types"; | ||||
| import { haStyleScrollbar } from "../resources/styles"; | ||||
| import { Blueprints, fetchBlueprints } from "../data/blueprint"; | ||||
|  | ||||
| @customElement("ha-filter-blueprints") | ||||
| export class HaFilterBlueprints extends LitElement { | ||||
| @@ -36,11 +35,7 @@ export class HaFilterBlueprints extends LitElement { | ||||
|         <div slot="header" class="header"> | ||||
|           ${this.hass.localize("ui.panel.config.blueprint.caption")} | ||||
|           ${this.value?.length | ||||
|             ? html`<div class="badge">${this.value?.length}</div> | ||||
|                 <ha-icon-button | ||||
|                   .path=${mdiFilterVariantRemove} | ||||
|                   @click=${this._clearFilter} | ||||
|                 ></ha-icon-button>` | ||||
|             ? html`<div class="badge">${this.value?.length}</div>` | ||||
|             : nothing} | ||||
|         </div> | ||||
|         ${this._blueprints && this._shouldRender | ||||
| @@ -133,15 +128,6 @@ export class HaFilterBlueprints extends LitElement { | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private _clearFilter(ev) { | ||||
|     ev.preventDefault(); | ||||
|     this.value = undefined; | ||||
|     fireEvent(this, "data-table-filter-changed", { | ||||
|       value: undefined, | ||||
|       items: undefined, | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   static get styles(): CSSResultGroup { | ||||
|     return [ | ||||
|       haStyleScrollbar, | ||||
| @@ -161,10 +147,6 @@ export class HaFilterBlueprints extends LitElement { | ||||
|           display: flex; | ||||
|           align-items: center; | ||||
|         } | ||||
|         .header ha-icon-button { | ||||
|           margin-inline-start: auto; | ||||
|           margin-inline-end: 8px; | ||||
|         } | ||||
|         .badge { | ||||
|           display: inline-block; | ||||
|           margin-left: 8px; | ||||
|   | ||||
| @@ -2,7 +2,6 @@ import { ActionDetail, SelectedDetail } from "@material/mwc-list"; | ||||
| import { | ||||
|   mdiDelete, | ||||
|   mdiDotsVertical, | ||||
|   mdiFilterVariantRemove, | ||||
|   mdiPencil, | ||||
|   mdiPlus, | ||||
|   mdiTag, | ||||
| @@ -69,11 +68,7 @@ export class HaFilterCategories extends SubscribeMixin(LitElement) { | ||||
|         <div slot="header" class="header"> | ||||
|           ${this.hass.localize("ui.panel.config.category.caption")} | ||||
|           ${this.value?.length | ||||
|             ? html`<div class="badge">${this.value?.length}</div> | ||||
|                 <ha-icon-button | ||||
|                   .path=${mdiFilterVariantRemove} | ||||
|                   @click=${this._clearFilter} | ||||
|                 ></ha-icon-button>` | ||||
|             ? html`<div class="badge">${this.value?.length}</div>` | ||||
|             : nothing} | ||||
|         </div> | ||||
|         ${this._shouldRender | ||||
| @@ -259,15 +254,6 @@ export class HaFilterCategories extends SubscribeMixin(LitElement) { | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private _clearFilter(ev) { | ||||
|     ev.preventDefault(); | ||||
|     this.value = undefined; | ||||
|     fireEvent(this, "data-table-filter-changed", { | ||||
|       value: undefined, | ||||
|       items: undefined, | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   static get styles(): CSSResultGroup { | ||||
|     return [ | ||||
|       haStyleScrollbar, | ||||
| @@ -288,10 +274,6 @@ export class HaFilterCategories extends SubscribeMixin(LitElement) { | ||||
|           display: flex; | ||||
|           align-items: center; | ||||
|         } | ||||
|         .header ha-icon-button { | ||||
|           margin-inline-start: auto; | ||||
|           margin-inline-end: 8px; | ||||
|         } | ||||
|         .badge { | ||||
|           display: inline-block; | ||||
|           margin-left: 8px; | ||||
|   | ||||
| @@ -1,4 +1,3 @@ | ||||
| import { mdiFilterVariantRemove } from "@mdi/js"; | ||||
| import { | ||||
|   css, | ||||
|   CSSResultGroup, | ||||
| @@ -14,11 +13,10 @@ import { stringCompare } from "../common/string/compare"; | ||||
| import { computeDeviceName } from "../data/device_registry"; | ||||
| import { findRelated, RelatedResult } from "../data/search"; | ||||
| import { haStyleScrollbar } from "../resources/styles"; | ||||
| import { loadVirtualizer } from "../resources/virtualizer"; | ||||
| import type { HomeAssistant } from "../types"; | ||||
| import "./ha-check-list-item"; | ||||
| import "./ha-expansion-panel"; | ||||
| import "./search-input-outlined"; | ||||
| import "./ha-check-list-item"; | ||||
| import { loadVirtualizer } from "../resources/virtualizer"; | ||||
|  | ||||
| @customElement("ha-filter-devices") | ||||
| export class HaFilterDevices extends LitElement { | ||||
| @@ -34,8 +32,6 @@ export class HaFilterDevices extends LitElement { | ||||
|  | ||||
|   @state() private _shouldRender = false; | ||||
|  | ||||
|   @state() private _filter?: string; | ||||
|  | ||||
|   public willUpdate(properties: PropertyValues) { | ||||
|     super.willUpdate(properties); | ||||
|  | ||||
| @@ -55,33 +51,19 @@ export class HaFilterDevices extends LitElement { | ||||
|         <div slot="header" class="header"> | ||||
|           ${this.hass.localize("ui.panel.config.devices.caption")} | ||||
|           ${this.value?.length | ||||
|             ? html`<div class="badge">${this.value?.length}</div> | ||||
|                 <ha-icon-button | ||||
|                   .path=${mdiFilterVariantRemove} | ||||
|                   @click=${this._clearFilter} | ||||
|                 ></ha-icon-button>` | ||||
|             ? html`<div class="badge">${this.value?.length}</div>` | ||||
|             : nothing} | ||||
|         </div> | ||||
|         ${this._shouldRender | ||||
|           ? html`<search-input-outlined | ||||
|                 .hass=${this.hass} | ||||
|                 .filter=${this._filter} | ||||
|                 @value-changed=${this._handleSearchChange} | ||||
|           ? html`<mwc-list class="ha-scrollbar"> | ||||
|               <lit-virtualizer | ||||
|                 .items=${this._devices(this.hass.devices, this.value)} | ||||
|                 .keyFunction=${this._keyFunction} | ||||
|                 .renderItem=${this._renderItem} | ||||
|                 @click=${this._handleItemClick} | ||||
|               > | ||||
|               </search-input-outlined> | ||||
|               <mwc-list class="ha-scrollbar"> | ||||
|                 <lit-virtualizer | ||||
|                   .items=${this._devices( | ||||
|                     this.hass.devices, | ||||
|                     this._filter || "", | ||||
|                     this.value | ||||
|                   )} | ||||
|                   .keyFunction=${this._keyFunction} | ||||
|                   .renderItem=${this._renderItem} | ||||
|                   @click=${this._handleItemClick} | ||||
|                 > | ||||
|                 </lit-virtualizer> | ||||
|               </mwc-list>` | ||||
|               </lit-virtualizer> | ||||
|             </mwc-list>` | ||||
|           : nothing} | ||||
|       </ha-expansion-panel> | ||||
|     `; | ||||
| @@ -90,14 +72,12 @@ export class HaFilterDevices extends LitElement { | ||||
|   private _keyFunction = (device) => device?.id; | ||||
|  | ||||
|   private _renderItem = (device) => | ||||
|     !device | ||||
|       ? nothing | ||||
|       : html`<ha-check-list-item | ||||
|           .value=${device.id} | ||||
|           .selected=${this.value?.includes(device.id)} | ||||
|         > | ||||
|           ${computeDeviceName(device, this.hass)} | ||||
|         </ha-check-list-item>`; | ||||
|     html`<ha-check-list-item | ||||
|       .value=${device.id} | ||||
|       .selected=${this.value?.includes(device.id)} | ||||
|     > | ||||
|       ${computeDeviceName(device, this.hass)} | ||||
|     </ha-check-list-item>`; | ||||
|  | ||||
|   private _handleItemClick(ev) { | ||||
|     const listItem = ev.target.closest("ha-check-list-item"); | ||||
| @@ -119,7 +99,7 @@ export class HaFilterDevices extends LitElement { | ||||
|       setTimeout(() => { | ||||
|         if (!this.expanded) return; | ||||
|         this.renderRoot.querySelector("mwc-list")!.style.height = | ||||
|           `${this.clientHeight - 49 - 32}px`; // 32px is the height of the search input | ||||
|           `${this.clientHeight - 49}px`; | ||||
|       }, 300); | ||||
|     } | ||||
|   } | ||||
| @@ -132,28 +112,16 @@ export class HaFilterDevices extends LitElement { | ||||
|     this.expanded = ev.detail.expanded; | ||||
|   } | ||||
|  | ||||
|   private _handleSearchChange(ev: CustomEvent) { | ||||
|     this._filter = ev.detail.value.toLowerCase(); | ||||
|   } | ||||
|  | ||||
|   private _devices = memoizeOne( | ||||
|     (devices: HomeAssistant["devices"], filter: string, _value) => { | ||||
|       const values = Object.values(devices); | ||||
|       return values | ||||
|         .filter( | ||||
|           (device) => | ||||
|             !filter || | ||||
|             computeDeviceName(device, this.hass).toLowerCase().includes(filter) | ||||
|         ) | ||||
|         .sort((a, b) => | ||||
|           stringCompare( | ||||
|             computeDeviceName(a, this.hass), | ||||
|             computeDeviceName(b, this.hass), | ||||
|             this.hass.locale.language | ||||
|           ) | ||||
|         ); | ||||
|     } | ||||
|   ); | ||||
|   private _devices = memoizeOne((devices: HomeAssistant["devices"], _value) => { | ||||
|     const values = Object.values(devices); | ||||
|     return values.sort((a, b) => | ||||
|       stringCompare( | ||||
|         a.name_by_user || a.name || "", | ||||
|         b.name_by_user || b.name || "", | ||||
|         this.hass.locale.language | ||||
|       ) | ||||
|     ); | ||||
|   }); | ||||
|  | ||||
|   private async _findRelated() { | ||||
|     const relatedPromises: Promise<RelatedResult>[] = []; | ||||
| @@ -190,15 +158,6 @@ export class HaFilterDevices extends LitElement { | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private _clearFilter(ev) { | ||||
|     ev.preventDefault(); | ||||
|     this.value = undefined; | ||||
|     fireEvent(this, "data-table-filter-changed", { | ||||
|       value: undefined, | ||||
|       items: undefined, | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   static get styles(): CSSResultGroup { | ||||
|     return [ | ||||
|       haStyleScrollbar, | ||||
| @@ -219,10 +178,6 @@ export class HaFilterDevices extends LitElement { | ||||
|           display: flex; | ||||
|           align-items: center; | ||||
|         } | ||||
|         .header ha-icon-button { | ||||
|           margin-inline-start: auto; | ||||
|           margin-inline-end: 8px; | ||||
|         } | ||||
|         .badge { | ||||
|           display: inline-block; | ||||
|           margin-left: 8px; | ||||
| @@ -242,10 +197,6 @@ export class HaFilterDevices extends LitElement { | ||||
|         ha-check-list-item { | ||||
|           width: 100%; | ||||
|         } | ||||
|         search-input-outlined { | ||||
|           display: block; | ||||
|           padding: 0 8px; | ||||
|         } | ||||
|       `, | ||||
|     ]; | ||||
|   } | ||||
|   | ||||
| @@ -1,4 +1,3 @@ | ||||
| import { mdiFilterVariantRemove } from "@mdi/js"; | ||||
| import { | ||||
|   css, | ||||
|   CSSResultGroup, | ||||
| @@ -15,11 +14,10 @@ import { computeStateName } from "../common/entity/compute_state_name"; | ||||
| import { stringCompare } from "../common/string/compare"; | ||||
| import { findRelated, RelatedResult } from "../data/search"; | ||||
| import { haStyleScrollbar } from "../resources/styles"; | ||||
| import { loadVirtualizer } from "../resources/virtualizer"; | ||||
| import type { HomeAssistant } from "../types"; | ||||
| import "./ha-check-list-item"; | ||||
| import "./ha-state-icon"; | ||||
| import "./search-input-outlined"; | ||||
| import "./ha-check-list-item"; | ||||
| import { loadVirtualizer } from "../resources/virtualizer"; | ||||
|  | ||||
| @customElement("ha-filter-entities") | ||||
| export class HaFilterEntities extends LitElement { | ||||
| @@ -35,8 +33,6 @@ export class HaFilterEntities extends LitElement { | ||||
|  | ||||
|   @state() private _shouldRender = false; | ||||
|  | ||||
|   @state() private _filter?: string; | ||||
|  | ||||
|   public willUpdate(properties: PropertyValues) { | ||||
|     super.willUpdate(properties); | ||||
|  | ||||
| @@ -56,27 +52,16 @@ export class HaFilterEntities extends LitElement { | ||||
|         <div slot="header" class="header"> | ||||
|           ${this.hass.localize("ui.panel.config.entities.caption")} | ||||
|           ${this.value?.length | ||||
|             ? html`<div class="badge">${this.value?.length}</div> | ||||
|                 <ha-icon-button | ||||
|                   .path=${mdiFilterVariantRemove} | ||||
|                   @click=${this._clearFilter} | ||||
|                 ></ha-icon-button>` | ||||
|             ? html`<div class="badge">${this.value?.length}</div>` | ||||
|             : nothing} | ||||
|         </div> | ||||
|         ${this._shouldRender | ||||
|           ? html` | ||||
|               <search-input-outlined | ||||
|                 .hass=${this.hass} | ||||
|                 .filter=${this._filter} | ||||
|                 @value-changed=${this._handleSearchChange} | ||||
|               > | ||||
|               </search-input-outlined> | ||||
|               <mwc-list class="ha-scrollbar"> | ||||
|                 <lit-virtualizer | ||||
|                   .items=${this._entities( | ||||
|                     this.hass.states, | ||||
|                     this.type, | ||||
|                     this._filter || "", | ||||
|                     this.value | ||||
|                   )} | ||||
|                   .keyFunction=${this._keyFunction} | ||||
| @@ -96,7 +81,7 @@ export class HaFilterEntities extends LitElement { | ||||
|       setTimeout(() => { | ||||
|         if (!this.expanded) return; | ||||
|         this.renderRoot.querySelector("mwc-list")!.style.height = | ||||
|           `${this.clientHeight - 49 - 32}px`; // 32px is the height of the search input | ||||
|           `${this.clientHeight - 49}px`; | ||||
|       }, 300); | ||||
|     } | ||||
|   } | ||||
| @@ -104,20 +89,18 @@ export class HaFilterEntities extends LitElement { | ||||
|   private _keyFunction = (entity) => entity?.entity_id; | ||||
|  | ||||
|   private _renderItem = (entity) => | ||||
|     !entity | ||||
|       ? nothing | ||||
|       : html`<ha-check-list-item | ||||
|           .value=${entity.entity_id} | ||||
|           .selected=${this.value?.includes(entity.entity_id)} | ||||
|           graphic="icon" | ||||
|         > | ||||
|           <ha-state-icon | ||||
|             slot="graphic" | ||||
|             .hass=${this.hass} | ||||
|             .stateObj=${entity} | ||||
|           ></ha-state-icon> | ||||
|           ${computeStateName(entity)} | ||||
|         </ha-check-list-item>`; | ||||
|     html`<ha-check-list-item | ||||
|       .value=${entity.entity_id} | ||||
|       .selected=${this.value?.includes(entity.entity_id)} | ||||
|       graphic="icon" | ||||
|     > | ||||
|       <ha-state-icon | ||||
|         slot="graphic" | ||||
|         .hass=${this.hass} | ||||
|         .stateObj=${entity} | ||||
|       ></ha-state-icon> | ||||
|       ${computeStateName(entity)} | ||||
|     </ha-check-list-item>`; | ||||
|  | ||||
|   private _handleItemClick(ev) { | ||||
|     const listItem = ev.target.closest("ha-check-list-item"); | ||||
| @@ -142,27 +125,12 @@ export class HaFilterEntities extends LitElement { | ||||
|     this.expanded = ev.detail.expanded; | ||||
|   } | ||||
|  | ||||
|   private _handleSearchChange(ev: CustomEvent) { | ||||
|     this._filter = ev.detail.value.toLowerCase(); | ||||
|   } | ||||
|  | ||||
|   private _entities = memoizeOne( | ||||
|     ( | ||||
|       states: HomeAssistant["states"], | ||||
|       type: this["type"], | ||||
|       filter: string, | ||||
|       _value | ||||
|     ) => { | ||||
|     (states: HomeAssistant["states"], type: this["type"], _value) => { | ||||
|       const values = Object.values(states); | ||||
|       return values | ||||
|         .filter( | ||||
|           (entityState) => | ||||
|             (!type || computeStateDomain(entityState) !== type) && | ||||
|             (!filter || | ||||
|               entityState.entity_id.toLowerCase().includes(filter) || | ||||
|               entityState.attributes.friendly_name | ||||
|                 ?.toLowerCase() | ||||
|                 .includes(filter)) | ||||
|           (entityState) => !type || computeStateDomain(entityState) !== type | ||||
|         ) | ||||
|         .sort((a, b) => | ||||
|           stringCompare( | ||||
| @@ -209,15 +177,6 @@ export class HaFilterEntities extends LitElement { | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private _clearFilter(ev) { | ||||
|     ev.preventDefault(); | ||||
|     this.value = undefined; | ||||
|     fireEvent(this, "data-table-filter-changed", { | ||||
|       value: undefined, | ||||
|       items: undefined, | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   static get styles(): CSSResultGroup { | ||||
|     return [ | ||||
|       haStyleScrollbar, | ||||
| @@ -237,10 +196,6 @@ export class HaFilterEntities extends LitElement { | ||||
|           display: flex; | ||||
|           align-items: center; | ||||
|         } | ||||
|         .header ha-icon-button { | ||||
|           margin-inline-start: auto; | ||||
|           margin-inline-end: 8px; | ||||
|         } | ||||
|         .badge { | ||||
|           display: inline-block; | ||||
|           margin-left: 8px; | ||||
| @@ -261,10 +216,6 @@ export class HaFilterEntities extends LitElement { | ||||
|           --mdc-list-item-graphic-margin: 16px; | ||||
|           width: 100%; | ||||
|         } | ||||
|         search-input-outlined { | ||||
|           display: block; | ||||
|           padding: 0 8px; | ||||
|         } | ||||
|       `, | ||||
|     ]; | ||||
|   } | ||||
|   | ||||
| @@ -1,19 +1,17 @@ | ||||
| import "@material/mwc-menu/mwc-menu-surface"; | ||||
| import { mdiFilterVariantRemove, mdiTextureBox } from "@mdi/js"; | ||||
| import { mdiTextureBox } from "@mdi/js"; | ||||
| import { UnsubscribeFunc } from "home-assistant-js-websocket"; | ||||
| import { CSSResultGroup, LitElement, css, html, nothing } from "lit"; | ||||
| import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { classMap } from "lit/directives/class-map"; | ||||
| import { repeat } from "lit/directives/repeat"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { fireEvent } from "../common/dom/fire_event"; | ||||
| import { computeRTL } from "../common/util/compute_rtl"; | ||||
| import { | ||||
|   FloorRegistryEntry, | ||||
|   getFloorAreaLookup, | ||||
|   subscribeFloorRegistry, | ||||
| } from "../data/floor_registry"; | ||||
| import { RelatedResult, findRelated } from "../data/search"; | ||||
| import { findRelated, RelatedResult } from "../data/search"; | ||||
| import { SubscribeMixin } from "../mixins/subscribe-mixin"; | ||||
| import { haStyleScrollbar } from "../resources/styles"; | ||||
| import type { HomeAssistant } from "../types"; | ||||
| @@ -21,7 +19,6 @@ import "./ha-check-list-item"; | ||||
| import "./ha-floor-icon"; | ||||
| import "./ha-icon"; | ||||
| import "./ha-svg-icon"; | ||||
| import "./ha-tree-indicator"; | ||||
|  | ||||
| @customElement("ha-filter-floor-areas") | ||||
| export class HaFilterFloorAreas extends SubscribeMixin(LitElement) { | ||||
| @@ -56,13 +53,9 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) { | ||||
|           ${this.hass.localize("ui.panel.config.areas.caption")} | ||||
|           ${this.value?.areas?.length || this.value?.floors?.length | ||||
|             ? html`<div class="badge"> | ||||
|                   ${(this.value?.areas?.length || 0) + | ||||
|                   (this.value?.floors?.length || 0)} | ||||
|                 </div> | ||||
|                 <ha-icon-button | ||||
|                   .path=${mdiFilterVariantRemove} | ||||
|                   @click=${this._clearFilter} | ||||
|                 ></ha-icon-button>` | ||||
|                 ${(this.value?.areas?.length || 0) + | ||||
|                 (this.value?.floors?.length || 0)} | ||||
|               </div>` | ||||
|             : nothing} | ||||
|         </div> | ||||
|         ${this._shouldRender | ||||
| @@ -89,10 +82,8 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) { | ||||
|                     </ha-check-list-item> | ||||
|                     ${repeat( | ||||
|                       floor.areas, | ||||
|                       (area, index) => | ||||
|                         `${area.area_id}${index === floor.areas.length - 1 ? "___last" : ""}`, | ||||
|                       (area, index) => | ||||
|                         this._renderArea(area, index === floor.areas.length - 1) | ||||
|                       (area) => area.area_id, | ||||
|                       (area) => this._renderArea(area) | ||||
|                     )} | ||||
|                   ` | ||||
|                 )} | ||||
| @@ -108,37 +99,23 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) { | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   private _renderArea(area, last: boolean = false) { | ||||
|     const hasFloor = !!area.floor_id; | ||||
|     return html` | ||||
|       <ha-check-list-item | ||||
|         .value=${area.area_id} | ||||
|         .selected=${this.value?.areas?.includes(area.area_id) || false} | ||||
|         .type=${"areas"} | ||||
|         graphic="icon" | ||||
|         @request-selected=${this._handleItemClick} | ||||
|         class=${classMap({ | ||||
|           rtl: computeRTL(this.hass), | ||||
|           floor: hasFloor, | ||||
|         })} | ||||
|       > | ||||
|         ${hasFloor | ||||
|           ? html` | ||||
|               <ha-tree-indicator | ||||
|                 .end=${last} | ||||
|                 slot="graphic" | ||||
|               ></ha-tree-indicator> | ||||
|             ` | ||||
|           : nothing} | ||||
|         ${area.icon | ||||
|           ? html`<ha-icon slot="graphic" .icon=${area.icon}></ha-icon>` | ||||
|           : html`<ha-svg-icon | ||||
|               slot="graphic" | ||||
|               .path=${mdiTextureBox} | ||||
|             ></ha-svg-icon>`} | ||||
|         ${area.name} | ||||
|       </ha-check-list-item> | ||||
|     `; | ||||
|   private _renderArea(area) { | ||||
|     return html`<ha-check-list-item | ||||
|       .value=${area.area_id} | ||||
|       .selected=${this.value?.areas?.includes(area.area_id) || false} | ||||
|       .type=${"areas"} | ||||
|       graphic="icon" | ||||
|       class=${area.floor_id ? "floor" : ""} | ||||
|       @request-selected=${this._handleItemClick} | ||||
|     > | ||||
|       ${area.icon | ||||
|         ? html`<ha-icon slot="graphic" .icon=${area.icon}></ha-icon>` | ||||
|         : html`<ha-svg-icon | ||||
|             slot="graphic" | ||||
|             .path=${mdiTextureBox} | ||||
|           ></ha-svg-icon>`} | ||||
|       ${area.name} | ||||
|     </ha-check-list-item>`; | ||||
|   } | ||||
|  | ||||
|   private _handleItemClick(ev) { | ||||
| @@ -261,15 +238,6 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) { | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private _clearFilter(ev) { | ||||
|     ev.preventDefault(); | ||||
|     this.value = undefined; | ||||
|     fireEvent(this, "data-table-filter-changed", { | ||||
|       value: undefined, | ||||
|       items: undefined, | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   static get styles(): CSSResultGroup { | ||||
|     return [ | ||||
|       haStyleScrollbar, | ||||
| @@ -289,10 +257,6 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) { | ||||
|           display: flex; | ||||
|           align-items: center; | ||||
|         } | ||||
|         .header ha-icon-button { | ||||
|           margin-inline-start: auto; | ||||
|           margin-inline-end: 8px; | ||||
|         } | ||||
|         .badge { | ||||
|           display: inline-block; | ||||
|           margin-left: 8px; | ||||
| @@ -313,26 +277,9 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) { | ||||
|           --mdc-list-item-graphic-margin: 16px; | ||||
|         } | ||||
|         .floor { | ||||
|           padding-left: 48px; | ||||
|           padding-inline-start: 48px; | ||||
|           padding-inline-end: 16px; | ||||
|           padding-left: 32px; | ||||
|           padding-inline-start: 32px; | ||||
|         } | ||||
|         ha-tree-indicator { | ||||
|           width: 56px; | ||||
|           position: absolute; | ||||
|           top: 0px; | ||||
|           left: 0px; | ||||
|         } | ||||
|         .rtl ha-tree-indicator { | ||||
|           right: 0px; | ||||
|           left: initial; | ||||
|           transform: scaleX(-1); | ||||
|         } | ||||
|         .subdir { | ||||
|           margin-inline-end: 8px; | ||||
|           opacity: .6; | ||||
|         } | ||||
|         . | ||||
|       `, | ||||
|     ]; | ||||
|   } | ||||
|   | ||||
| @@ -1,5 +1,4 @@ | ||||
| import { SelectedDetail } from "@material/mwc-list"; | ||||
| import { mdiFilterVariantRemove } from "@mdi/js"; | ||||
| import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { repeat } from "lit/directives/repeat"; | ||||
| @@ -13,7 +12,6 @@ import { | ||||
| import { haStyleScrollbar } from "../resources/styles"; | ||||
| import type { HomeAssistant } from "../types"; | ||||
| import "./ha-domain-icon"; | ||||
| import "./search-input-outlined"; | ||||
|  | ||||
| @customElement("ha-filter-integrations") | ||||
| export class HaFilterIntegrations extends LitElement { | ||||
| @@ -29,8 +27,6 @@ export class HaFilterIntegrations extends LitElement { | ||||
|  | ||||
|   @state() private _shouldRender = false; | ||||
|  | ||||
|   @state() private _filter?: string; | ||||
|  | ||||
|   protected render() { | ||||
|     return html` | ||||
|       <ha-expansion-panel | ||||
| @@ -42,27 +38,18 @@ export class HaFilterIntegrations extends LitElement { | ||||
|         <div slot="header" class="header"> | ||||
|           ${this.hass.localize("ui.panel.config.integrations.caption")} | ||||
|           ${this.value?.length | ||||
|             ? html`<div class="badge">${this.value?.length}</div> | ||||
|                 <ha-icon-button | ||||
|                   .path=${mdiFilterVariantRemove} | ||||
|                   @click=${this._clearFilter} | ||||
|                 ></ha-icon-button>` | ||||
|             ? html`<div class="badge">${this.value?.length}</div>` | ||||
|             : nothing} | ||||
|         </div> | ||||
|         ${this._manifests && this._shouldRender | ||||
|           ? html`<search-input-outlined | ||||
|                 .hass=${this.hass} | ||||
|                 .filter=${this._filter} | ||||
|                 @value-changed=${this._handleSearchChange} | ||||
|               > | ||||
|               </search-input-outlined> | ||||
|           ? html` | ||||
|               <mwc-list | ||||
|                 @selected=${this._integrationsSelected} | ||||
|                 multi | ||||
|                 class="ha-scrollbar" | ||||
|               > | ||||
|                 ${repeat( | ||||
|                   this._integrations(this._manifests, this._filter, this.value), | ||||
|                   this._integrations(this._manifests, this.value), | ||||
|                   (i) => i.domain, | ||||
|                   (integration) => | ||||
|                     html`<ha-check-list-item | ||||
| @@ -81,7 +68,8 @@ export class HaFilterIntegrations extends LitElement { | ||||
|                       ${integration.name || integration.domain} | ||||
|                     </ha-check-list-item>` | ||||
|                 )} | ||||
|               </mwc-list> ` | ||||
|               </mwc-list> | ||||
|             ` | ||||
|           : nothing} | ||||
|       </ha-expansion-panel> | ||||
|     `; | ||||
| @@ -110,17 +98,12 @@ export class HaFilterIntegrations extends LitElement { | ||||
|   } | ||||
|  | ||||
|   private _integrations = memoizeOne( | ||||
|     (manifest: IntegrationManifest[], filter: string | undefined, _value) => | ||||
|     (manifest: IntegrationManifest[], _value) => | ||||
|       manifest | ||||
|         .filter( | ||||
|           (mnfst) => | ||||
|             (!mnfst.integration_type || | ||||
|               !["entity", "system", "hardware"].includes( | ||||
|                 mnfst.integration_type | ||||
|               )) && | ||||
|             (!filter || | ||||
|               mnfst.name.toLowerCase().includes(filter) || | ||||
|               mnfst.domain.toLowerCase().includes(filter)) | ||||
|             !mnfst.integration_type || | ||||
|             !["entity", "system", "hardware"].includes(mnfst.integration_type) | ||||
|         ) | ||||
|         .sort((a, b) => | ||||
|           stringCompare( | ||||
| @@ -134,11 +117,7 @@ export class HaFilterIntegrations extends LitElement { | ||||
|   private async _integrationsSelected( | ||||
|     ev: CustomEvent<SelectedDetail<Set<number>>> | ||||
|   ) { | ||||
|     const integrations = this._integrations( | ||||
|       this._manifests!, | ||||
|       this._filter, | ||||
|       this.value | ||||
|     ); | ||||
|     const integrations = this._integrations(this._manifests!, this.value); | ||||
|  | ||||
|     if (!ev.detail.index.size) { | ||||
|       fireEvent(this, "data-table-filter-changed", { | ||||
| @@ -163,19 +142,6 @@ export class HaFilterIntegrations extends LitElement { | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private _clearFilter(ev) { | ||||
|     ev.preventDefault(); | ||||
|     this.value = undefined; | ||||
|     fireEvent(this, "data-table-filter-changed", { | ||||
|       value: undefined, | ||||
|       items: undefined, | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private _handleSearchChange(ev: CustomEvent) { | ||||
|     this._filter = ev.detail.value.toLowerCase(); | ||||
|   } | ||||
|  | ||||
|   static get styles(): CSSResultGroup { | ||||
|     return [ | ||||
|       haStyleScrollbar, | ||||
| @@ -195,10 +161,6 @@ export class HaFilterIntegrations extends LitElement { | ||||
|           display: flex; | ||||
|           align-items: center; | ||||
|         } | ||||
|         .header ha-icon-button { | ||||
|           margin-inline-start: auto; | ||||
|           margin-inline-end: 8px; | ||||
|         } | ||||
|         .badge { | ||||
|           display: inline-block; | ||||
|           margin-left: 8px; | ||||
| @@ -215,10 +177,6 @@ export class HaFilterIntegrations extends LitElement { | ||||
|           padding: 0px 2px; | ||||
|           color: var(--text-primary-color); | ||||
|         } | ||||
|         search-input-outlined { | ||||
|           display: block; | ||||
|           padding: 0 8px; | ||||
|         } | ||||
|       `, | ||||
|     ]; | ||||
|   } | ||||
|   | ||||
| @@ -1,18 +1,19 @@ | ||||
| import { SelectedDetail } from "@material/mwc-list"; | ||||
| import "@material/mwc-menu/mwc-menu-surface"; | ||||
| import { mdiCog, mdiFilterVariantRemove } from "@mdi/js"; | ||||
| import { mdiPlus } from "@mdi/js"; | ||||
| import { UnsubscribeFunc } from "home-assistant-js-websocket"; | ||||
| import { CSSResultGroup, LitElement, css, html, nothing } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { repeat } from "lit/directives/repeat"; | ||||
| import { computeCssColor } from "../common/color/compute-color"; | ||||
| import { fireEvent } from "../common/dom/fire_event"; | ||||
| import { navigate } from "../common/navigate"; | ||||
| import { | ||||
|   LabelRegistryEntry, | ||||
|   createLabelRegistryEntry, | ||||
|   subscribeLabelRegistry, | ||||
| } from "../data/label_registry"; | ||||
| import { SubscribeMixin } from "../mixins/subscribe-mixin"; | ||||
| import { showLabelDetailDialog } from "../panels/config/labels/show-dialog-label-detail"; | ||||
| import { haStyleScrollbar } from "../resources/styles"; | ||||
| import type { HomeAssistant } from "../types"; | ||||
| import "./ha-check-list-item"; | ||||
| @@ -53,11 +54,7 @@ export class HaFilterLabels extends SubscribeMixin(LitElement) { | ||||
|         <div slot="header" class="header"> | ||||
|           ${this.hass.localize("ui.panel.config.labels.caption")} | ||||
|           ${this.value?.length | ||||
|             ? html`<div class="badge">${this.value?.length}</div> | ||||
|                 <ha-icon-button | ||||
|                   .path=${mdiFilterVariantRemove} | ||||
|                   @click=${this._clearFilter} | ||||
|                 ></ha-icon-button>` | ||||
|             ? html`<div class="badge">${this.value?.length}</div>` | ||||
|             : nothing} | ||||
|         </div> | ||||
|         ${this._shouldRender | ||||
| @@ -98,11 +95,11 @@ export class HaFilterLabels extends SubscribeMixin(LitElement) { | ||||
|       ${this.expanded | ||||
|         ? html`<ha-list-item | ||||
|             graphic="icon" | ||||
|             @click=${this._manageLabels} | ||||
|             @click=${this._addLabel} | ||||
|             class="add" | ||||
|           > | ||||
|             <ha-svg-icon slot="graphic" .path=${mdiCog}></ha-svg-icon> | ||||
|             ${this.hass.localize("ui.panel.config.labels.manage_labels")} | ||||
|             <ha-svg-icon slot="graphic" .path=${mdiPlus}></ha-svg-icon> | ||||
|             ${this.hass.localize("ui.panel.config.labels.add_label")} | ||||
|           </ha-list-item>` | ||||
|         : nothing} | ||||
|     `; | ||||
| @@ -118,8 +115,10 @@ export class HaFilterLabels extends SubscribeMixin(LitElement) { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private _manageLabels() { | ||||
|     navigate("/config/labels"); | ||||
|   private _addLabel() { | ||||
|     showLabelDetailDialog(this, { | ||||
|       createEntry: (values) => createLabelRegistryEntry(this.hass, values), | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private _expandedWillChange(ev) { | ||||
| @@ -154,15 +153,6 @@ export class HaFilterLabels extends SubscribeMixin(LitElement) { | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private _clearFilter(ev) { | ||||
|     ev.preventDefault(); | ||||
|     this.value = undefined; | ||||
|     fireEvent(this, "data-table-filter-changed", { | ||||
|       value: undefined, | ||||
|       items: undefined, | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   static get styles(): CSSResultGroup { | ||||
|     return [ | ||||
|       haStyleScrollbar, | ||||
| @@ -183,10 +173,6 @@ export class HaFilterLabels extends SubscribeMixin(LitElement) { | ||||
|           display: flex; | ||||
|           align-items: center; | ||||
|         } | ||||
|         .header ha-icon-button { | ||||
|           margin-inline-start: auto; | ||||
|           margin-inline-end: 8px; | ||||
|         } | ||||
|         .badge { | ||||
|           display: inline-block; | ||||
|           margin-left: 8px; | ||||
|   | ||||
| @@ -1,12 +1,11 @@ | ||||
| import { SelectedDetail } from "@material/mwc-list"; | ||||
| import { mdiFilterVariantRemove } from "@mdi/js"; | ||||
| import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { fireEvent } from "../common/dom/fire_event"; | ||||
| import { haStyleScrollbar } from "../resources/styles"; | ||||
| import type { HomeAssistant } from "../types"; | ||||
| import "./ha-check-list-item"; | ||||
| import "./ha-expansion-panel"; | ||||
| import "./ha-check-list-item"; | ||||
| import "./ha-icon"; | ||||
|  | ||||
| @customElement("ha-filter-states") | ||||
| @@ -44,11 +43,7 @@ export class HaFilterStates extends LitElement { | ||||
|         <div slot="header" class="header"> | ||||
|           ${this.label} | ||||
|           ${this.value?.length | ||||
|             ? html`<div class="badge">${this.value?.length}</div> | ||||
|                 <ha-icon-button | ||||
|                   .path=${mdiFilterVariantRemove} | ||||
|                   @click=${this._clearFilter} | ||||
|                 ></ha-icon-button>` | ||||
|             ? html`<div class="badge">${this.value?.length}</div>` | ||||
|             : nothing} | ||||
|         </div> | ||||
|         ${this._shouldRender | ||||
| @@ -123,15 +118,6 @@ export class HaFilterStates extends LitElement { | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private _clearFilter(ev) { | ||||
|     ev.preventDefault(); | ||||
|     this.value = undefined; | ||||
|     fireEvent(this, "data-table-filter-changed", { | ||||
|       value: undefined, | ||||
|       items: undefined, | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   static get styles(): CSSResultGroup { | ||||
|     return [ | ||||
|       haStyleScrollbar, | ||||
| @@ -151,10 +137,6 @@ export class HaFilterStates extends LitElement { | ||||
|           display: flex; | ||||
|           align-items: center; | ||||
|         } | ||||
|         .header ha-icon-button { | ||||
|           margin-inline-start: auto; | ||||
|           margin-inline-end: 8px; | ||||
|         } | ||||
|         .badge { | ||||
|           display: inline-block; | ||||
|           margin-left: 8px; | ||||
|   | ||||
| @@ -10,10 +10,7 @@ import { | ||||
|   ScorableTextItem, | ||||
|   fuzzyFilterSort, | ||||
| } from "../common/string/filter/sequence-matching"; | ||||
| import { | ||||
|   AreaRegistryEntry, | ||||
|   updateAreaRegistryEntry, | ||||
| } from "../data/area_registry"; | ||||
| import { AreaRegistryEntry } from "../data/area_registry"; | ||||
| import { | ||||
|   DeviceEntityDisplayLookup, | ||||
|   DeviceRegistryEntry, | ||||
| @@ -440,18 +437,11 @@ export class HaFloorPicker extends SubscribeMixin(LitElement) { | ||||
|  | ||||
|     (ev.target as any).value = this._value; | ||||
|  | ||||
|     this.hass.loadFragmentTranslation("config"); | ||||
|  | ||||
|     showFloorRegistryDetailDialog(this, { | ||||
|       suggestedName: newValue === ADD_NEW_SUGGESTION_ID ? this._suggestion : "", | ||||
|       createEntry: async (values, addedAreas) => { | ||||
|       createEntry: async (values) => { | ||||
|         try { | ||||
|           const floor = await createFloorRegistryEntry(this.hass, values); | ||||
|           addedAreas.forEach((areaId) => { | ||||
|             updateAreaRegistryEntry(this.hass, areaId, { | ||||
|               floor_id: floor.floor_id, | ||||
|             }); | ||||
|           }); | ||||
|           const floors = [...this._floors!, floor]; | ||||
|           this.comboBox.filteredItems = this._getFloors( | ||||
|             floors, | ||||
|   | ||||
| @@ -445,8 +445,6 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) { | ||||
|  | ||||
|     (ev.target as any).value = this._value; | ||||
|  | ||||
|     this.hass.loadFragmentTranslation("config"); | ||||
|  | ||||
|     showLabelDetailDialog(this, { | ||||
|       entry: undefined, | ||||
|       suggestedName: newValue === ADD_NEW_SUGGESTION_ID ? this._suggestion : "", | ||||
|   | ||||
| @@ -2,10 +2,8 @@ import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; | ||||
| import { LitElement, TemplateResult, css, html, nothing } from "lit"; | ||||
| import { customElement, property, query, state } from "lit/decorators"; | ||||
| import { repeat } from "lit/directives/repeat"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { computeCssColor } from "../common/color/compute-color"; | ||||
| import { fireEvent } from "../common/dom/fire_event"; | ||||
| import { stringCompare } from "../common/string/compare"; | ||||
| import { | ||||
|   LabelRegistryEntry, | ||||
|   subscribeLabelRegistry, | ||||
| @@ -19,6 +17,7 @@ import "./chips/ha-input-chip"; | ||||
| import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker"; | ||||
| import "./ha-label-picker"; | ||||
| import type { HaLabelPicker } from "./ha-label-picker"; | ||||
| import { stringCompare } from "../common/string/compare"; | ||||
|  | ||||
| @customElement("ha-labels-picker") | ||||
| export class HaLabelsPicker extends SubscribeMixin(LitElement) { | ||||
| @@ -103,35 +102,25 @@ export class HaLabelsPicker extends SubscribeMixin(LitElement) { | ||||
|     ]; | ||||
|   } | ||||
|  | ||||
|   private _sortedLabels = memoizeOne( | ||||
|     ( | ||||
|       value: string[] | undefined, | ||||
|       labels: { [id: string]: LabelRegistryEntry } | undefined, | ||||
|       language: string | ||||
|     ) => | ||||
|       value | ||||
|         ?.map((id) => labels?.[id]) | ||||
|         .sort((a, b) => stringCompare(a?.name || "", b?.name || "", language)) | ||||
|   ); | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|     const labels = this._sortedLabels( | ||||
|       this.value, | ||||
|       this._labels, | ||||
|       this.hass.locale.language | ||||
|     ); | ||||
|     const labels = this.value | ||||
|       ?.map((id) => this._labels?.[id]) | ||||
|       .sort((a, b) => | ||||
|         stringCompare(a?.name || "", b?.name || "", this.hass.locale.language) | ||||
|       ); | ||||
|     return html` | ||||
|       ${labels?.length | ||||
|         ? html`<ha-chip-set> | ||||
|             ${repeat( | ||||
|               labels, | ||||
|               (label) => label?.label_id, | ||||
|               (label) => { | ||||
|               (label, idx) => { | ||||
|                 const color = label?.color | ||||
|                   ? computeCssColor(label.color) | ||||
|                   : undefined; | ||||
|                 return html` | ||||
|                   <ha-input-chip | ||||
|                     .idx=${idx} | ||||
|                     .item=${label} | ||||
|                     @remove=${this._removeItem} | ||||
|                     @click=${this._openDetail} | ||||
| @@ -172,12 +161,12 @@ export class HaLabelsPicker extends SubscribeMixin(LitElement) { | ||||
|   } | ||||
|  | ||||
|   private _removeItem(ev) { | ||||
|     const label = ev.currentTarget.item; | ||||
|     this._setValue(this._value.filter((id) => id !== label.label_id)); | ||||
|     this._value.splice(ev.target.idx, 1); | ||||
|     this._setValue([...this._value]); | ||||
|   } | ||||
|  | ||||
|   private _openDetail(ev) { | ||||
|     const label = ev.currentTarget.item; | ||||
|     const label = ev.target.item; | ||||
|     showLabelDetailDialog(this, { | ||||
|       entry: label, | ||||
|       updateEntry: async (values) => { | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import { MdMenuItem } from "@material/web/menu/menu-item"; | ||||
| import { customElement } from "lit/decorators"; | ||||
| import "element-internals-polyfill"; | ||||
| import { CSSResult, css } from "lit"; | ||||
| import { customElement } from "lit/decorators"; | ||||
| import { MdMenuItem } from "@material/web/menu/menu-item"; | ||||
|  | ||||
| @customElement("ha-menu-item") | ||||
| export class HaMenuItem extends MdMenuItem { | ||||
|   | ||||
| @@ -27,10 +27,6 @@ export class HaOutlinedTextField extends MdOutlinedTextField { | ||||
|         --md-outlined-field-focus-outline-width: 1px; | ||||
|         --mdc-icon-size: var(--md-input-chip-icon-size, 18px); | ||||
|       } | ||||
|       md-outlined-field { | ||||
|         background: var(--ha-outlined-text-field-container-color, transparent); | ||||
|         opacity: var(--ha-outlined-text-field-container-opacity, 1); | ||||
|       } | ||||
|       .input { | ||||
|         font-family: Roboto, sans-serif; | ||||
|       } | ||||
|   | ||||
| @@ -30,7 +30,6 @@ export class HaLabelSelector extends LitElement { | ||||
|     if (this.selector.label.multiple) { | ||||
|       return html` | ||||
|         <ha-labels-picker | ||||
|           no-add | ||||
|           .hass=${this.hass} | ||||
|           .value=${ensureArray(this.value ?? [])} | ||||
|           .disabled=${this.disabled} | ||||
| @@ -42,7 +41,6 @@ export class HaLabelSelector extends LitElement { | ||||
|     } | ||||
|     return html` | ||||
|       <ha-label-picker | ||||
|         no-add | ||||
|         .hass=${this.hass} | ||||
|         .value=${this.value} | ||||
|         .disabled=${this.disabled} | ||||
|   | ||||
| @@ -1,36 +0,0 @@ | ||||
| import { LitElement, TemplateResult, css, html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators"; | ||||
|  | ||||
| @customElement("ha-tree-indicator") | ||||
| export class HaTreeIndicator extends LitElement { | ||||
|   @property({ type: Boolean, reflect: true }) | ||||
|   public end?: boolean = false; | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|     return html` | ||||
|       <svg width="100%" height="100%" viewBox="0 0 48 48"> | ||||
|         <line x1="24" y1="0" x2="24" y2=${this.end ? "24" : "48"}></line> | ||||
|         <line x1="24" y1="24" x2="36" y2="24"></line> | ||||
|       </svg> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   static styles = css` | ||||
|     :host { | ||||
|       display: block; | ||||
|       width: 48px; | ||||
|       height: 48px; | ||||
|     } | ||||
|     line { | ||||
|       stroke: var(--divider-color); | ||||
|       stroke-width: 2; | ||||
|       stroke-dasharray: 2; | ||||
|     } | ||||
|   `; | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   interface HTMLElementTagNameMap { | ||||
|     "ha-tree-indicator": HaTreeIndicator; | ||||
|   } | ||||
| } | ||||
| @@ -1,12 +1,5 @@ | ||||
| import { mdiClose, mdiMagnify } from "@mdi/js"; | ||||
| import { | ||||
|   CSSResultGroup, | ||||
|   LitElement, | ||||
|   TemplateResult, | ||||
|   css, | ||||
|   html, | ||||
|   nothing, | ||||
| } from "lit"; | ||||
| import { mdiMagnify } from "@mdi/js"; | ||||
| import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit"; | ||||
| import { customElement, property, query } from "lit/decorators"; | ||||
| import { fireEvent } from "../common/dom/fire_event"; | ||||
| import { HomeAssistant } from "../types"; | ||||
| @@ -61,15 +54,6 @@ class SearchInputOutlined extends LitElement { | ||||
|             .path=${mdiMagnify} | ||||
|           ></ha-svg-icon> | ||||
|         </slot> | ||||
|         ${this.filter | ||||
|           ? html`<ha-icon-button | ||||
|               aria-label="Clear input" | ||||
|               slot="trailing-icon" | ||||
|               @click=${this._clearSearch} | ||||
|               .path=${mdiClose} | ||||
|             > | ||||
|             </ha-icon-button>` | ||||
|           : nothing} | ||||
|       </ha-outlined-text-field> | ||||
|     `; | ||||
|   } | ||||
| @@ -82,22 +66,16 @@ class SearchInputOutlined extends LitElement { | ||||
|     this._filterChanged(e.target.value); | ||||
|   } | ||||
|  | ||||
|   private async _clearSearch() { | ||||
|     this._filterChanged(""); | ||||
|   } | ||||
|  | ||||
|   static get styles(): CSSResultGroup { | ||||
|     return css` | ||||
|       :host { | ||||
|         display: inline-flex; | ||||
|         /* For iOS */ | ||||
|         z-index: 0; | ||||
|         --mdc-icon-button-size: 24px; | ||||
|       } | ||||
|       ha-outlined-text-field { | ||||
|         display: block; | ||||
|         width: 100%; | ||||
|         --ha-outlined-text-field-container-color: var(--card-background-color); | ||||
|       } | ||||
|       ha-svg-icon, | ||||
|       ha-icon-button { | ||||
|   | ||||
| @@ -28,7 +28,6 @@ export type ItemType = | ||||
|   | "entity" | ||||
|   | "floor" | ||||
|   | "group" | ||||
|   | "label" | ||||
|   | "scene" | ||||
|   | "script" | ||||
|   | "automation_blueprint" | ||||
|   | ||||
| @@ -190,7 +190,7 @@ class LightColorTempPicker extends LitElement { | ||||
|           max-height: 320px; | ||||
|           min-height: 200px; | ||||
|           --control-slider-thickness: 130px; | ||||
|           --control-slider-border-radius: 36px; | ||||
|           --control-slider-border-radius: 48px; | ||||
|           --control-slider-color: var(--primary-color); | ||||
|           --control-slider-background: -webkit-linear-gradient( | ||||
|             top, | ||||
|   | ||||
| @@ -1,8 +1,9 @@ | ||||
| import { mdiShieldOff } from "@mdi/js"; | ||||
| import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; | ||||
| import { customElement, property } from "lit/decorators"; | ||||
| import { styleMap } from "lit/directives/style-map"; | ||||
| import { stateColorCss } from "../../../common/entity/state_color"; | ||||
| import "../../../components/ha-control-button"; | ||||
| import "../../../components/ha-outlined-button"; | ||||
| import "../../../components/ha-state-icon"; | ||||
| import { AlarmControlPanelEntity } from "../../../data/alarm_control_panel"; | ||||
| import "../../../state-control/alarm_control_panel/ha-state-control-alarm_control_panel-modes"; | ||||
| @@ -56,10 +57,15 @@ class MoreInfoAlarmControlPanel extends LitElement { | ||||
|         ${["triggered", "arming", "pending"].includes(this.stateObj.state) | ||||
|           ? html` | ||||
|               <div class="status"> | ||||
|                 <span></span> | ||||
|                 <div class="icon"> | ||||
|                   <ha-state-icon .hass=${this.hass} .stateObj=${this.stateObj}> | ||||
|                   </ha-state-icon> | ||||
|                 </div> | ||||
|                 <ha-outlined-button @click=${this._disarm}> | ||||
|                   ${this.hass.localize("ui.card.alarm_control_panel.disarm")} | ||||
|                   <ha-svg-icon slot="icon" .path=${mdiShieldOff}></ha-svg-icon> | ||||
|                 </ha-outlined-button> | ||||
|               </div> | ||||
|             ` | ||||
|           : html` | ||||
| @@ -70,15 +76,7 @@ class MoreInfoAlarmControlPanel extends LitElement { | ||||
|               </ha-state-control-alarm_control_panel-modes> | ||||
|             `} | ||||
|       </div> | ||||
|       <div> | ||||
|         ${["triggered", "arming", "pending"].includes(this.stateObj.state) | ||||
|           ? html` | ||||
|               <ha-control-button @click=${this._disarm} class="disarm"> | ||||
|                 ${this.hass.localize("ui.card.alarm_control_panel.disarm")} | ||||
|               </ha-control-button> | ||||
|             ` | ||||
|           : nothing} | ||||
|       </div> | ||||
|       <span></span> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
| @@ -129,12 +127,8 @@ class MoreInfoAlarmControlPanel extends LitElement { | ||||
|           transition: background-color 180ms ease-in-out; | ||||
|           opacity: 0.2; | ||||
|         } | ||||
|         ha-control-button.disarm { | ||||
|           height: 60px; | ||||
|           min-width: 130px; | ||||
|           max-width: 200px; | ||||
|           margin: 0 auto; | ||||
|           --control-button-border-radius: 24px; | ||||
|         .status ha-outlined-button { | ||||
|           margin-top: 32px; | ||||
|         } | ||||
|       `, | ||||
|     ]; | ||||
|   | ||||
| @@ -170,7 +170,7 @@ class MoreInfoLock extends LitElement { | ||||
|           --control-button-border-radius: 24px; | ||||
|         } | ||||
|         .open-button { | ||||
|           width: 130px; | ||||
|           width: 100px; | ||||
|           --control-button-background-color: var(--state-color); | ||||
|         } | ||||
|         .open-button.confirm { | ||||
|   | ||||
| @@ -321,28 +321,19 @@ export class HaTabsSubpageDataTable extends LitElement { | ||||
|                       .path=${mdiMenuDown} | ||||
|                     ></ha-svg-icon | ||||
|                   ></ha-assist-chip> | ||||
|                   <ha-menu-item .value=${undefined} @click=${this._selectAll}> | ||||
|                     <div slot="headline"> | ||||
|                       ${localize("ui.components.subpage-data-table.select_all")} | ||||
|                     </div> | ||||
|                   <ha-menu-item .value=${undefined} @click=${this._selectAll} | ||||
|                     >${localize("ui.components.subpage-data-table.select_all")} | ||||
|                   </ha-menu-item> | ||||
|                   <ha-menu-item .value=${undefined} @click=${this._selectNone}> | ||||
|                     <div slot="headline"> | ||||
|                       ${localize( | ||||
|                         "ui.components.subpage-data-table.select_none" | ||||
|                       )} | ||||
|                     </div> | ||||
|                   <ha-menu-item .value=${undefined} @click=${this._selectNone} | ||||
|                     >${localize("ui.components.subpage-data-table.select_none")} | ||||
|                   </ha-menu-item> | ||||
|                   <md-divider role="separator" tabindex="-1"></md-divider> | ||||
|                   <ha-menu-item | ||||
|                     .value=${undefined} | ||||
|                     @click=${this._disableSelectMode} | ||||
|                   > | ||||
|                     <div slot="headline"> | ||||
|                       ${localize( | ||||
|                         "ui.components.subpage-data-table.close_select_mode" | ||||
|                       )} | ||||
|                     </div> | ||||
|                     >${localize( | ||||
|                       "ui.components.subpage-data-table.close_select_mode" | ||||
|                     )} | ||||
|                   </ha-menu-item> | ||||
|                 </ha-button-menu-new> | ||||
|                 <p> | ||||
| @@ -358,7 +349,37 @@ export class HaTabsSubpageDataTable extends LitElement { | ||||
|           : nothing} | ||||
|         ${this.showFilters | ||||
|           ? !showPane | ||||
|             ? nothing | ||||
|             ? html`<ha-dialog | ||||
|                 open | ||||
|                 hideActions | ||||
|                 .heading=${localize("ui.components.subpage-data-table.filters")} | ||||
|               > | ||||
|                 <ha-dialog-header slot="heading"> | ||||
|                   <ha-icon-button | ||||
|                     slot="navigationIcon" | ||||
|                     .path=${mdiClose} | ||||
|                     @click=${this._toggleFilters} | ||||
|                     .label=${localize( | ||||
|                       "ui.components.subpage-data-table.close_filter" | ||||
|                     )} | ||||
|                   ></ha-icon-button> | ||||
|                   <span slot="title" | ||||
|                     >${localize( | ||||
|                       "ui.components.subpage-data-table.filters" | ||||
|                     )}</span | ||||
|                   > | ||||
|                   <ha-icon-button | ||||
|                     slot="actionItems" | ||||
|                     @click=${this._clearFilters} | ||||
|                     .path=${mdiFilterVariantRemove} | ||||
|                     .label=${localize( | ||||
|                       "ui.components.subpage-data-table.clear_filter" | ||||
|                     )} | ||||
|                   ></ha-icon-button> | ||||
|                 </ha-dialog-header> | ||||
|                 <div class="filter-dialog-content"> | ||||
|                   <slot name="filter-pane"></slot></div | ||||
|               ></ha-dialog>` | ||||
|             : html`<div class="pane" slot="pane"> | ||||
|                 <div class="table-header"> | ||||
|                   <ha-assist-chip | ||||
| @@ -373,15 +394,13 @@ export class HaTabsSubpageDataTable extends LitElement { | ||||
|                       .path=${mdiFilterVariant} | ||||
|                     ></ha-svg-icon> | ||||
|                   </ha-assist-chip> | ||||
|                   ${this.filters | ||||
|                     ? html`<ha-icon-button | ||||
|                         .path=${mdiFilterVariantRemove} | ||||
|                         @click=${this._clearFilters} | ||||
|                         .label=${localize( | ||||
|                           "ui.components.subpage-data-table.clear_filter" | ||||
|                         )} | ||||
|                       ></ha-icon-button>` | ||||
|                     : nothing} | ||||
|                   <ha-icon-button | ||||
|                     .path=${mdiFilterVariantRemove} | ||||
|                     @click=${this._clearFilters} | ||||
|                     .label=${localize( | ||||
|                       "ui.components.subpage-data-table.clear_filter" | ||||
|                     )} | ||||
|                   ></ha-icon-button> | ||||
|                 </div> | ||||
|                 <div class="pane-content"> | ||||
|                   <slot name="filter-pane"></slot> | ||||
| @@ -493,39 +512,6 @@ export class HaTabsSubpageDataTable extends LitElement { | ||||
|             : nothing | ||||
|         )} | ||||
|       </ha-menu> | ||||
|       ${this.showFilters && !showPane | ||||
|         ? html`<ha-dialog | ||||
|             open | ||||
|             hideActions | ||||
|             .heading=${localize("ui.components.subpage-data-table.filters")} | ||||
|           > | ||||
|             <ha-dialog-header slot="heading"> | ||||
|               <ha-icon-button | ||||
|                 slot="navigationIcon" | ||||
|                 .path=${mdiClose} | ||||
|                 @click=${this._toggleFilters} | ||||
|                 .label=${localize( | ||||
|                   "ui.components.subpage-data-table.close_filter" | ||||
|                 )} | ||||
|               ></ha-icon-button> | ||||
|               <span slot="title" | ||||
|                 >${localize("ui.components.subpage-data-table.filters")}</span | ||||
|               > | ||||
|               ${this.filters | ||||
|                 ? html`<ha-icon-button | ||||
|                     slot="actionItems" | ||||
|                     @click=${this._clearFilters} | ||||
|                     .path=${mdiFilterVariantRemove} | ||||
|                     .label=${localize( | ||||
|                       "ui.components.subpage-data-table.clear_filter" | ||||
|                     )} | ||||
|                   ></ha-icon-button>` | ||||
|                 : nothing} | ||||
|             </ha-dialog-header> | ||||
|             <div class="filter-dialog-content"> | ||||
|               <slot name="filter-pane"></slot></div | ||||
|           ></ha-dialog>` | ||||
|         : nothing} | ||||
|     `; | ||||
|   } | ||||
|  | ||||
| @@ -587,7 +573,6 @@ export class HaTabsSubpageDataTable extends LitElement { | ||||
|     return css` | ||||
|       :host { | ||||
|         display: block; | ||||
|         height: 100%; | ||||
|       } | ||||
|  | ||||
|       ha-data-table { | ||||
| @@ -743,7 +728,7 @@ export class HaTabsSubpageDataTable extends LitElement { | ||||
|         padding: 8px 12px; | ||||
|         box-sizing: border-box; | ||||
|         font-size: 14px; | ||||
|         --ha-assist-chip-container-color: var(--card-background-color); | ||||
|         --ha-assist-chip-container-color: var(--primary-background-color); | ||||
|       } | ||||
|  | ||||
|       .selection-controls { | ||||
| @@ -770,7 +755,6 @@ export class HaTabsSubpageDataTable extends LitElement { | ||||
|  | ||||
|       ha-assist-chip { | ||||
|         --ha-assist-chip-container-shape: 10px; | ||||
|         --ha-assist-chip-container-color: var(--card-background-color); | ||||
|       } | ||||
|  | ||||
|       .select-mode-chip { | ||||
| @@ -779,7 +763,6 @@ export class HaTabsSubpageDataTable extends LitElement { | ||||
|       } | ||||
|  | ||||
|       ha-dialog { | ||||
|         --dialog-z-index: 100; | ||||
|         --mdc-dialog-min-width: calc( | ||||
|           100vw - env(safe-area-inset-right) - env(safe-area-inset-left) | ||||
|         ); | ||||
|   | ||||
| @@ -1,13 +1,8 @@ | ||||
| import "@material/mwc-button"; | ||||
| import "@material/mwc-list/mwc-list"; | ||||
| import { mdiTextureBox } from "@mdi/js"; | ||||
| import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; | ||||
| import { property, state } from "lit/decorators"; | ||||
| import { repeat } from "lit/directives/repeat"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { fireEvent } from "../../../common/dom/fire_event"; | ||||
| import "../../../components/chips/ha-chip-set"; | ||||
| import "../../../components/chips/ha-input-chip"; | ||||
| import "../../../components/ha-alert"; | ||||
| import "../../../components/ha-aliases-editor"; | ||||
| import { createCloseHeading } from "../../../components/ha-dialog"; | ||||
| @@ -16,15 +11,10 @@ import "../../../components/ha-picture-upload"; | ||||
| import "../../../components/ha-settings-row"; | ||||
| import "../../../components/ha-svg-icon"; | ||||
| import "../../../components/ha-textfield"; | ||||
| import { | ||||
|   FloorRegistryEntry, | ||||
|   FloorRegistryEntryMutableParams, | ||||
| } from "../../../data/floor_registry"; | ||||
| import { haStyle, haStyleDialog } from "../../../resources/styles"; | ||||
| import { FloorRegistryEntryMutableParams } from "../../../data/floor_registry"; | ||||
| import { haStyleDialog } from "../../../resources/styles"; | ||||
| import { HomeAssistant } from "../../../types"; | ||||
| import { FloorRegistryDetailDialogParams } from "./show-dialog-floor-registry-detail"; | ||||
| import { showAreaRegistryDetailDialog } from "./show-dialog-area-registry-detail"; | ||||
| import { updateAreaRegistryEntry } from "../../../data/area_registry"; | ||||
|  | ||||
| class DialogFloorDetail extends LitElement { | ||||
|   @property({ attribute: false }) public hass!: HomeAssistant; | ||||
| @@ -43,11 +33,9 @@ class DialogFloorDetail extends LitElement { | ||||
|  | ||||
|   @state() private _submitting?: boolean; | ||||
|  | ||||
|   @state() private _addedAreas = new Set<string>(); | ||||
|  | ||||
|   @state() private _removedAreas = new Set<string>(); | ||||
|  | ||||
|   public showDialog(params: FloorRegistryDetailDialogParams): void { | ||||
|   public async showDialog( | ||||
|     params: FloorRegistryDetailDialogParams | ||||
|   ): Promise<void> { | ||||
|     this._params = params; | ||||
|     this._error = undefined; | ||||
|     this._name = this._params.entry | ||||
| @@ -56,40 +44,16 @@ class DialogFloorDetail extends LitElement { | ||||
|     this._aliases = this._params.entry?.aliases || []; | ||||
|     this._icon = this._params.entry?.icon || null; | ||||
|     this._level = this._params.entry?.level ?? null; | ||||
|     this._addedAreas.clear(); | ||||
|     this._removedAreas.clear(); | ||||
|     await this.updateComplete; | ||||
|   } | ||||
|  | ||||
|   public closeDialog(): void { | ||||
|     this._error = ""; | ||||
|     this._params = undefined; | ||||
|     this._addedAreas.clear(); | ||||
|     this._removedAreas.clear(); | ||||
|     fireEvent(this, "dialog-closed", { dialog: this.localName }); | ||||
|   } | ||||
|  | ||||
|   private _floorAreas = memoizeOne( | ||||
|     ( | ||||
|       entry: FloorRegistryEntry | undefined, | ||||
|       areas: HomeAssistant["areas"], | ||||
|       added: Set<string>, | ||||
|       removed: Set<string> | ||||
|     ) => | ||||
|       Object.values(areas).filter( | ||||
|         (area) => | ||||
|           (area.floor_id === entry?.floor_id || added.has(area.area_id)) && | ||||
|           !removed.has(area.area_id) | ||||
|       ) | ||||
|   ); | ||||
|  | ||||
|   protected render() { | ||||
|     const areas = this._floorAreas( | ||||
|       this._params?.entry, | ||||
|       this.hass.areas, | ||||
|       this._addedAreas, | ||||
|       this._removedAreas | ||||
|     ); | ||||
|  | ||||
|     if (!this._params) { | ||||
|       return nothing; | ||||
|     } | ||||
| @@ -161,52 +125,6 @@ class DialogFloorDetail extends LitElement { | ||||
|                 : nothing} | ||||
|             </ha-icon-picker> | ||||
|  | ||||
|             <h3 class="header"> | ||||
|               ${this.hass.localize( | ||||
|                 "ui.panel.config.floors.editor.areas_section" | ||||
|               )} | ||||
|             </h3> | ||||
|  | ||||
|             <p class="description"> | ||||
|               ${this.hass.localize( | ||||
|                 "ui.panel.config.floors.editor.areas_description" | ||||
|               )} | ||||
|             </p> | ||||
|             ${areas.length | ||||
|               ? html`<ha-chip-set> | ||||
|                   ${repeat( | ||||
|                     areas, | ||||
|                     (area) => area.area_id, | ||||
|                     (area) => | ||||
|                       html`<ha-input-chip | ||||
|                         .area=${area} | ||||
|                         @click=${this._openArea} | ||||
|                         @remove=${this._removeArea} | ||||
|                         .label=${area?.name} | ||||
|                       > | ||||
|                         ${area.icon | ||||
|                           ? html`<ha-icon | ||||
|                               slot="icon" | ||||
|                               .icon=${area.icon} | ||||
|                             ></ha-icon>` | ||||
|                           : html`<ha-svg-icon | ||||
|                               slot="icon" | ||||
|                               .path=${mdiTextureBox} | ||||
|                             ></ha-svg-icon>`} | ||||
|                       </ha-input-chip>` | ||||
|                   )} | ||||
|                 </ha-chip-set>` | ||||
|               : nothing} | ||||
|             <ha-area-picker | ||||
|               no-add | ||||
|               .hass=${this.hass} | ||||
|               @value-changed=${this._addArea} | ||||
|               .excludeAreas=${areas.map((a) => a.area_id)} | ||||
|               .label=${this.hass.localize( | ||||
|                 "ui.panel.config.floors.editor.add_area" | ||||
|               )} | ||||
|             ></ha-area-picker> | ||||
|  | ||||
|             <h3 class="header"> | ||||
|               ${this.hass.localize( | ||||
|                 "ui.panel.config.floors.editor.aliases_section" | ||||
| @@ -241,41 +159,6 @@ class DialogFloorDetail extends LitElement { | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   private _openArea(ev) { | ||||
|     const area = ev.target.area; | ||||
|     showAreaRegistryDetailDialog(this, { | ||||
|       entry: area, | ||||
|       updateEntry: (values) => | ||||
|         updateAreaRegistryEntry(this.hass!, area.area_id, values), | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private _removeArea(ev) { | ||||
|     const areaId = ev.target.area.area_id; | ||||
|     if (this._addedAreas.has(areaId)) { | ||||
|       this._addedAreas.delete(areaId); | ||||
|       this._addedAreas = new Set(this._addedAreas); | ||||
|       return; | ||||
|     } | ||||
|     this._removedAreas.add(areaId); | ||||
|     this._removedAreas = new Set(this._removedAreas); | ||||
|   } | ||||
|  | ||||
|   private _addArea(ev) { | ||||
|     const areaId = ev.detail.value; | ||||
|     if (!areaId) { | ||||
|       return; | ||||
|     } | ||||
|     ev.target.value = ""; | ||||
|     if (this._removedAreas.has(areaId)) { | ||||
|       this._removedAreas.delete(areaId); | ||||
|       this._removedAreas = new Set(this._removedAreas); | ||||
|       return; | ||||
|     } | ||||
|     this._addedAreas.add(areaId); | ||||
|     this._addedAreas = new Set(this._addedAreas); | ||||
|   } | ||||
|  | ||||
|   private _isNameValid() { | ||||
|     return this._name.trim() !== ""; | ||||
|   } | ||||
| @@ -306,13 +189,9 @@ class DialogFloorDetail extends LitElement { | ||||
|         aliases: this._aliases, | ||||
|       }; | ||||
|       if (create) { | ||||
|         await this._params!.createEntry!(values, this._addedAreas); | ||||
|         await this._params!.createEntry!(values); | ||||
|       } else { | ||||
|         await this._params!.updateEntry!( | ||||
|           values, | ||||
|           this._addedAreas, | ||||
|           this._removedAreas | ||||
|         ); | ||||
|         await this._params!.updateEntry!(values); | ||||
|       } | ||||
|       this.closeDialog(); | ||||
|     } catch (err: any) { | ||||
| @@ -330,7 +209,6 @@ class DialogFloorDetail extends LitElement { | ||||
|  | ||||
|   static get styles(): CSSResultGroup { | ||||
|     return [ | ||||
|       haStyle, | ||||
|       haStyleDialog, | ||||
|       css` | ||||
|         ha-textfield { | ||||
| @@ -340,9 +218,6 @@ class DialogFloorDetail extends LitElement { | ||||
|         ha-floor-icon { | ||||
|           color: var(--secondary-text-color); | ||||
|         } | ||||
|         ha-chip-set { | ||||
|           margin-bottom: 8px; | ||||
|         } | ||||
|       `, | ||||
|     ]; | ||||
|   } | ||||
|   | ||||
| @@ -271,14 +271,7 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) { | ||||
|             ? html`<ha-icon .icon=${area.icon}></ha-icon>` | ||||
|             : ""} | ||||
|         </div> | ||||
|         <div class="card-header"> | ||||
|           ${area.name} | ||||
|           <ha-icon-button | ||||
|             .area=${area} | ||||
|             .path=${mdiPencil} | ||||
|             @click=${this._openAreaDetails} | ||||
|           ></ha-icon-button> | ||||
|         </div> | ||||
|         <h1 class="card-header">${area.name}</h1> | ||||
|         <div class="card-content"> | ||||
|           <div> | ||||
|             ${formatListWithAnds( | ||||
| @@ -312,16 +305,6 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) { | ||||
|     loadAreaRegistryDetailDialog(); | ||||
|   } | ||||
|  | ||||
|   private _openAreaDetails(ev) { | ||||
|     ev.preventDefault(); | ||||
|     const area = ev.currentTarget.area; | ||||
|     showAreaRegistryDetailDialog(this, { | ||||
|       entry: area, | ||||
|       updateEntry: async (values) => | ||||
|         updateAreaRegistryEntry(this.hass!, area.area_id, values), | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private async _areaMoved(ev) { | ||||
|     const areasAndFloors = this._processAreas( | ||||
|       this.hass.areas, | ||||
| @@ -414,31 +397,10 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) { | ||||
|   private _openFloorDialog(entry?: FloorRegistryEntry) { | ||||
|     showFloorRegistryDetailDialog(this, { | ||||
|       entry, | ||||
|       createEntry: async (values, addedAreas) => { | ||||
|         const floor = await createFloorRegistryEntry(this.hass!, values); | ||||
|         addedAreas.forEach((areaId) => { | ||||
|           updateAreaRegistryEntry(this.hass, areaId, { | ||||
|             floor_id: floor.floor_id, | ||||
|           }); | ||||
|         }); | ||||
|       }, | ||||
|       updateEntry: async (values, addedAreas, removedAreas) => { | ||||
|         const floor = await updateFloorRegistryEntry( | ||||
|           this.hass!, | ||||
|           entry!.floor_id, | ||||
|           values | ||||
|         ); | ||||
|         addedAreas.forEach((areaId) => { | ||||
|           updateAreaRegistryEntry(this.hass, areaId, { | ||||
|             floor_id: floor.floor_id, | ||||
|           }); | ||||
|         }); | ||||
|         removedAreas.forEach((areaId) => { | ||||
|           updateAreaRegistryEntry(this.hass, areaId, { | ||||
|             floor_id: null, | ||||
|           }); | ||||
|         }); | ||||
|       }, | ||||
|       createEntry: async (values) => | ||||
|         createFloorRegistryEntry(this.hass!, values), | ||||
|       updateEntry: async (values) => | ||||
|         updateFloorRegistryEntry(this.hass!, entry!.floor_id, values), | ||||
|     }); | ||||
|   } | ||||
|  | ||||
| @@ -507,10 +469,8 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) { | ||||
|         min-height: 16px; | ||||
|         color: var(--secondary-text-color); | ||||
|       } | ||||
|       .card-header { | ||||
|         display: flex; | ||||
|         justify-content: space-between; | ||||
|         align-items: center; | ||||
|       .floor { | ||||
|         --primary-color: var(--secondary-text-color); | ||||
|       } | ||||
|       .warning { | ||||
|         color: var(--error-color); | ||||
|   | ||||
| @@ -7,14 +7,9 @@ import { | ||||
| export interface FloorRegistryDetailDialogParams { | ||||
|   entry?: FloorRegistryEntry; | ||||
|   suggestedName?: string; | ||||
|   createEntry?: ( | ||||
|     values: FloorRegistryEntryMutableParams, | ||||
|     addedAreas: Set<string> | ||||
|   ) => Promise<unknown>; | ||||
|   createEntry?: (values: FloorRegistryEntryMutableParams) => Promise<unknown>; | ||||
|   updateEntry?: ( | ||||
|     updates: Partial<FloorRegistryEntryMutableParams>, | ||||
|     addedAreas: Set<string>, | ||||
|     removedAreas: Set<string> | ||||
|     updates: Partial<FloorRegistryEntryMutableParams> | ||||
|   ) => Promise<unknown>; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1,5 +1,4 @@ | ||||
| import { consume } from "@lit-labs/context"; | ||||
| import { ResizeController } from "@lit-labs/observers/resize-controller"; | ||||
| import "@lrnwebcomponents/simple-tooltip/simple-tooltip"; | ||||
| import "@material/web/divider/divider"; | ||||
| import { | ||||
| @@ -74,7 +73,6 @@ import { | ||||
| } from "../../../data/automation"; | ||||
| import { | ||||
|   CategoryRegistryEntry, | ||||
|   createCategoryRegistryEntry, | ||||
|   subscribeCategoryRegistry, | ||||
| } from "../../../data/category_registry"; | ||||
| import { fullEntitiesContext } from "../../../data/context"; | ||||
| @@ -86,7 +84,6 @@ import { | ||||
| } from "../../../data/entity_registry"; | ||||
| import { | ||||
|   LabelRegistryEntry, | ||||
|   createLabelRegistryEntry, | ||||
|   subscribeLabelRegistry, | ||||
| } from "../../../data/label_registry"; | ||||
| import { findRelated } from "../../../data/search"; | ||||
| @@ -101,14 +98,11 @@ import { HomeAssistant, Route, ServiceCallResponse } from "../../../types"; | ||||
| import { documentationUrl } from "../../../util/documentation-url"; | ||||
| import { turnOnOffEntity } from "../../lovelace/common/entity/turn-on-off-entity"; | ||||
| import { showAssignCategoryDialog } from "../category/show-dialog-assign-category"; | ||||
| import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail"; | ||||
| import { configSections } from "../ha-panel-config"; | ||||
| import { showLabelDetailDialog } from "../labels/show-dialog-label-detail"; | ||||
| import { showNewAutomationDialog } from "./show-dialog-new-automation"; | ||||
|  | ||||
| type AutomationItem = AutomationEntity & { | ||||
|   name: string; | ||||
|   area: string | undefined; | ||||
|   last_triggered?: string | undefined; | ||||
|   formatted_state: string; | ||||
|   category: string | undefined; | ||||
| @@ -154,15 +148,10 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { | ||||
|  | ||||
|   @query("#overflow-menu") private _overflowMenu!: HaMenu; | ||||
|  | ||||
|   private _sizeController = new ResizeController(this, { | ||||
|     callback: (entries) => entries[0]?.contentRect.width, | ||||
|   }); | ||||
|  | ||||
|   private _automations = memoizeOne( | ||||
|     ( | ||||
|       automations: AutomationEntity[], | ||||
|       entityReg: EntityRegistryEntry[], | ||||
|       areas: HomeAssistant["areas"], | ||||
|       categoryReg?: CategoryRegistryEntry[], | ||||
|       labelReg?: LabelRegistryEntry[], | ||||
|       filteredAutomations?: string[] | null | ||||
| @@ -185,9 +174,6 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { | ||||
|         return { | ||||
|           ...automation, | ||||
|           name: computeStateName(automation), | ||||
|           area: entityRegEntry?.area_id | ||||
|             ? areas[entityRegEntry?.area_id]?.name | ||||
|             : undefined, | ||||
|           last_triggered: automation.attributes.last_triggered || undefined, | ||||
|           formatted_state: this.hass.formatEntityState(automation), | ||||
|           category: category | ||||
| @@ -256,13 +242,6 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { | ||||
|             `; | ||||
|           }, | ||||
|         }, | ||||
|         area: { | ||||
|           title: localize("ui.panel.config.automation.picker.headers.area"), | ||||
|           hidden: true, | ||||
|           groupable: true, | ||||
|           filterable: true, | ||||
|           sortable: true, | ||||
|         }, | ||||
|         category: { | ||||
|           title: localize("ui.panel.config.automation.picker.headers.category"), | ||||
|           hidden: true, | ||||
| @@ -277,32 +256,33 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { | ||||
|           template: (automation) => | ||||
|             automation.labels.map((lbl) => lbl.name).join(" "), | ||||
|         }, | ||||
|         last_triggered: { | ||||
|           sortable: true, | ||||
|           width: "130px", | ||||
|           title: localize("ui.card.automation.last_triggered"), | ||||
|           hidden: narrow, | ||||
|           template: (automation) => { | ||||
|             if (!automation.last_triggered) { | ||||
|               return this.hass.localize("ui.components.relative_time.never"); | ||||
|             } | ||||
|             const date = new Date(automation.last_triggered); | ||||
|             const now = new Date(); | ||||
|             const dayDifference = differenceInDays(now, date); | ||||
|             return html` | ||||
|               ${dayDifference > 3 | ||||
|                 ? formatShortDateTime(date, locale, this.hass.config) | ||||
|                 : relativeTime(date, locale)} | ||||
|             `; | ||||
|           }, | ||||
|       }; | ||||
|       columns.last_triggered = { | ||||
|         sortable: true, | ||||
|         width: "130px", | ||||
|         title: localize("ui.card.automation.last_triggered"), | ||||
|         hidden: narrow, | ||||
|         template: (automation) => { | ||||
|           if (!automation.last_triggered) { | ||||
|             return this.hass.localize("ui.components.relative_time.never"); | ||||
|           } | ||||
|           const date = new Date(automation.last_triggered); | ||||
|           const now = new Date(); | ||||
|           const dayDifference = differenceInDays(now, date); | ||||
|           return html` | ||||
|             ${dayDifference > 3 | ||||
|               ? formatShortDateTime(date, locale, this.hass.config) | ||||
|               : relativeTime(date, locale)} | ||||
|           `; | ||||
|         }, | ||||
|         formatted_state: { | ||||
|       }; | ||||
|  | ||||
|       if (!this.narrow) { | ||||
|         columns.formatted_state = { | ||||
|           width: "82px", | ||||
|           sortable: true, | ||||
|           groupable: true, | ||||
|           title: "", | ||||
|           type: "overflow", | ||||
|           hidden: narrow, | ||||
|           label: this.hass.localize("ui.panel.config.automation.picker.state"), | ||||
|           template: (automation) => html` | ||||
|             <ha-entity-toggle | ||||
| @@ -310,20 +290,21 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { | ||||
|               .hass=${this.hass} | ||||
|             ></ha-entity-toggle> | ||||
|           `, | ||||
|         }, | ||||
|         actions: { | ||||
|           title: "", | ||||
|           width: "64px", | ||||
|           type: "icon-button", | ||||
|           template: (automation) => html` | ||||
|             <ha-icon-button | ||||
|               .automation=${automation} | ||||
|               .label=${this.hass.localize("ui.common.overflow_menu")} | ||||
|               .path=${mdiDotsVertical} | ||||
|               @click=${this._showOverflowMenu} | ||||
|             ></ha-icon-button> | ||||
|           `, | ||||
|         }, | ||||
|         }; | ||||
|       } | ||||
|  | ||||
|       columns.actions = { | ||||
|         title: "", | ||||
|         width: "64px", | ||||
|         type: "icon-button", | ||||
|         template: (automation) => html` | ||||
|           <ha-icon-button | ||||
|             .automation=${automation} | ||||
|             .label=${this.hass.localize("ui.common.overflow_menu")} | ||||
|             .path=${mdiDotsVertical} | ||||
|             @click=${this._showOverflowMenu} | ||||
|           ></ha-icon-button> | ||||
|         `, | ||||
|       }; | ||||
|       return columns; | ||||
|     } | ||||
| @@ -376,52 +357,22 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { | ||||
|             "ui.panel.config.automation.picker.bulk_actions.no_category" | ||||
|           )} | ||||
|         </div> | ||||
|       </ha-menu-item> | ||||
|       <md-divider role="separator" tabindex="-1"></md-divider> | ||||
|       <ha-menu-item @click=${this._bulkCreateCategory}> | ||||
|         <div slot="headline"> | ||||
|           ${this.hass.localize("ui.panel.config.category.editor.add")} | ||||
|         </div> | ||||
|       </ha-menu-item>`; | ||||
|     const labelItems = html`${this._labels?.map((label) => { | ||||
|         const color = label.color ? computeCssColor(label.color) : undefined; | ||||
|         const selected = this._selected.every((entityId) => | ||||
|           this.hass.entities[entityId]?.labels.includes(label.label_id) | ||||
|         ); | ||||
|         const partial = | ||||
|           !selected && | ||||
|           this._selected.some((entityId) => | ||||
|             this.hass.entities[entityId]?.labels.includes(label.label_id) | ||||
|           ); | ||||
|         return html`<ha-menu-item | ||||
|           .value=${label.label_id} | ||||
|           .action=${selected ? "remove" : "add"} | ||||
|           @click=${this._handleBulkLabel} | ||||
|           keep-open | ||||
|         > | ||||
|           <ha-checkbox | ||||
|             slot="start" | ||||
|             .checked=${selected} | ||||
|             .indeterminate=${partial} | ||||
|             reducedTouchTarget | ||||
|           ></ha-checkbox> | ||||
|           <ha-label style=${color ? `--color: ${color}` : ""}> | ||||
|             ${label.icon | ||||
|               ? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>` | ||||
|               : nothing} | ||||
|             ${label.name} | ||||
|           </ha-label> | ||||
|         </ha-menu-item>`; | ||||
|       })} | ||||
|       <md-divider role="separator" tabindex="-1"></md-divider> | ||||
|       <ha-menu-item @click=${this._bulkCreateLabel}> | ||||
|         <div slot="headline"> | ||||
|           ${this.hass.localize("ui.panel.config.labels.add_label")} | ||||
|         </div></ha-menu-item | ||||
|       >`; | ||||
|     const labelsInOverflow = | ||||
|       (this._sizeController.value && this._sizeController.value < 700) || | ||||
|       (!this._sizeController.value && this.hass.dockedSidebar === "docked"); | ||||
|     const labelItems = html` ${this._labels?.map((label) => { | ||||
|       const color = label.color ? computeCssColor(label.color) : undefined; | ||||
|       return html`<ha-menu-item | ||||
|         .value=${label.label_id} | ||||
|         @click=${this._handleBulkLabel} | ||||
|       > | ||||
|         <ha-label style=${color ? `--color: ${color}` : ""}> | ||||
|           ${label.icon | ||||
|             ? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>` | ||||
|             : nothing} | ||||
|           ${label.name} | ||||
|         </ha-label> | ||||
|       </ha-menu-item>`; | ||||
|     })}`; | ||||
|  | ||||
|     return html` | ||||
|       <hass-tabs-subpage-data-table | ||||
|         .hass=${this.hass} | ||||
| @@ -449,7 +400,6 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { | ||||
|         .data=${this._automations( | ||||
|           this.automations, | ||||
|           this._entityReg, | ||||
|           this.hass.areas, | ||||
|           this._categories, | ||||
|           this._labels, | ||||
|           this._filteredAutomations | ||||
| @@ -545,7 +495,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { | ||||
|                     </ha-assist-chip> | ||||
|                     ${categoryItems} | ||||
|                   </ha-button-menu-new> | ||||
|                   ${labelsInOverflow | ||||
|                   ${this.hass.dockedSidebar === "docked" | ||||
|                     ? nothing | ||||
|                     : html`<ha-button-menu-new slot="selection-bar"> | ||||
|                         <ha-assist-chip | ||||
| @@ -607,7 +557,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { | ||||
|                 : nothing | ||||
|             } | ||||
|             ${ | ||||
|               this.narrow || labelsInOverflow | ||||
|               this.narrow || this.hass.dockedSidebar === "docked" | ||||
|                 ? html`<ha-sub-menu> | ||||
|                     <ha-menu-item slot="item"> | ||||
|                       <div slot="headline"> | ||||
| @@ -1089,10 +1039,6 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { | ||||
|  | ||||
|   private async _handleBulkCategory(ev) { | ||||
|     const category = ev.currentTarget.value; | ||||
|     this._bulkAddCategory(category); | ||||
|   } | ||||
|  | ||||
|   private async _bulkAddCategory(category: string) { | ||||
|     const promises: Promise<UpdateEntityRegistryEntryResult>[] = []; | ||||
|     this._selected.forEach((entityId) => { | ||||
|       promises.push( | ||||
| @@ -1106,21 +1052,11 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { | ||||
|  | ||||
|   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) => { | ||||
|       promises.push( | ||||
|         updateEntityRegistryEntry(this.hass, entityId, { | ||||
|           labels: | ||||
|             action === "add" | ||||
|               ? this.hass.entities[entityId].labels.concat(label) | ||||
|               : this.hass.entities[entityId].labels.filter( | ||||
|                   (lbl) => lbl !== label | ||||
|                 ), | ||||
|           labels: this.hass.entities[entityId].labels.concat(label), | ||||
|         }) | ||||
|       ); | ||||
|     }); | ||||
| @@ -1143,38 +1079,10 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { | ||||
|     await Promise.all(promises); | ||||
|   } | ||||
|  | ||||
|   private async _bulkCreateCategory() { | ||||
|     showCategoryRegistryDetailDialog(this, { | ||||
|       scope: "automation", | ||||
|       createEntry: async (values) => { | ||||
|         const category = await createCategoryRegistryEntry( | ||||
|           this.hass, | ||||
|           "automation", | ||||
|           values | ||||
|         ); | ||||
|         this._bulkAddCategory(category.category_id); | ||||
|         return category; | ||||
|       }, | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private _bulkCreateLabel() { | ||||
|     showLabelDetailDialog(this, { | ||||
|       createEntry: async (values) => { | ||||
|         const label = await createLabelRegistryEntry(this.hass, values); | ||||
|         this._bulkLabel(label.label_id, "add"); | ||||
|         return label; | ||||
|       }, | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   static get styles(): CSSResultGroup { | ||||
|     return [ | ||||
|       haStyle, | ||||
|       css` | ||||
|         :host { | ||||
|           display: block; | ||||
|         } | ||||
|         hass-tabs-subpage-data-table { | ||||
|           --data-table-row-height: 60px; | ||||
|         } | ||||
|   | ||||
| @@ -237,8 +237,6 @@ export class HaCategoryPicker extends SubscribeMixin(LitElement) { | ||||
|  | ||||
|     (ev.target as any).value = this._value; | ||||
|  | ||||
|     this.hass.loadFragmentTranslation("config"); | ||||
|  | ||||
|     showCategoryRegistryDetailDialog(this, { | ||||
|       scope: this.scope!, | ||||
|       suggestedName: newValue === ADD_NEW_SUGGESTION_ID ? this._suggestion : "", | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import { consume } from "@lit-labs/context"; | ||||
| import "@lrnwebcomponents/simple-tooltip/simple-tooltip"; | ||||
| import { mdiChevronRight, mdiMenuDown, mdiPlus } from "@mdi/js"; | ||||
| import { mdiPlus } from "@mdi/js"; | ||||
| import { | ||||
|   CSSResultGroup, | ||||
|   LitElement, | ||||
| @@ -13,7 +13,6 @@ import { | ||||
| import { UnsubscribeFunc } from "home-assistant-js-websocket"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { computeCssColor } from "../../../common/color/compute-color"; | ||||
| import { HASSDomEvent } from "../../../common/dom/fire_event"; | ||||
| import { computeStateDomain } from "../../../common/entity/compute_state_domain"; | ||||
| import { | ||||
| @@ -25,7 +24,6 @@ import { LocalizeFunc } from "../../../common/translations/localize"; | ||||
| import { | ||||
|   DataTableColumnContainer, | ||||
|   RowClickedEvent, | ||||
|   SelectionChangedEvent, | ||||
| } from "../../../components/data-table/ha-data-table"; | ||||
| import "../../../components/data-table/ha-data-table-labels"; | ||||
| import "../../../components/entity/ha-battery-icon"; | ||||
| @@ -39,15 +37,12 @@ import "../../../components/ha-filter-integrations"; | ||||
| import "../../../components/ha-filter-labels"; | ||||
| import "../../../components/ha-filter-states"; | ||||
| import "../../../components/ha-icon-button"; | ||||
| import "../../../components/ha-menu-item"; | ||||
| import "../../../components/ha-sub-menu"; | ||||
| import { ConfigEntry, sortConfigEntries } from "../../../data/config_entries"; | ||||
| import { fullEntitiesContext } from "../../../data/context"; | ||||
| import { | ||||
|   DeviceEntityLookup, | ||||
|   DeviceRegistryEntry, | ||||
|   computeDeviceName, | ||||
|   updateDeviceRegistryEntry, | ||||
| } from "../../../data/device_registry"; | ||||
| import { | ||||
|   EntityRegistryEntry, | ||||
| @@ -57,7 +52,6 @@ import { | ||||
| import { IntegrationManifest } from "../../../data/integration"; | ||||
| import { | ||||
|   LabelRegistryEntry, | ||||
|   createLabelRegistryEntry, | ||||
|   subscribeLabelRegistry, | ||||
| } from "../../../data/label_registry"; | ||||
| import "../../../layouts/hass-tabs-subpage-data-table"; | ||||
| @@ -68,7 +62,6 @@ import { brandsUrl } from "../../../util/brands-url"; | ||||
| import { configSections } from "../ha-panel-config"; | ||||
| import "../integrations/ha-integration-overflow-menu"; | ||||
| import { showAddIntegrationDialog } from "../integrations/show-add-integration-dialog"; | ||||
| import { showLabelDetailDialog } from "../labels/show-dialog-label-detail"; | ||||
|  | ||||
| interface DeviceRowData extends DeviceRegistryEntry { | ||||
|   device?: DeviceRowData; | ||||
| @@ -98,8 +91,6 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) { | ||||
|  | ||||
|   @state() private _searchParms = new URLSearchParams(window.location.search); | ||||
|  | ||||
|   @state() private _selected: string[] = []; | ||||
|  | ||||
|   @state() private _filter: string = history.state?.filter || ""; | ||||
|  | ||||
|   @state() private _filters: Record< | ||||
| @@ -544,43 +535,6 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) { | ||||
|       this._labels | ||||
|     ); | ||||
|  | ||||
|     const labelItems = html`${this._labels?.map((label) => { | ||||
|         const color = label.color ? computeCssColor(label.color) : undefined; | ||||
|         const selected = this._selected.every((deviceId) => | ||||
|           this.hass.devices[deviceId]?.labels.includes(label.label_id) | ||||
|         ); | ||||
|         const partial = | ||||
|           !selected && | ||||
|           this._selected.some((deviceId) => | ||||
|             this.hass.devices[deviceId]?.labels.includes(label.label_id) | ||||
|           ); | ||||
|         return html`<ha-menu-item | ||||
|           .value=${label.label_id} | ||||
|           .action=${selected ? "remove" : "add"} | ||||
|           @click=${this._handleBulkLabel} | ||||
|           keep-open | ||||
|         > | ||||
|           <ha-checkbox | ||||
|             slot="start" | ||||
|             .checked=${selected} | ||||
|             .indeterminate=${partial} | ||||
|             reducedTouchTarget | ||||
|           ></ha-checkbox> | ||||
|           <ha-label style=${color ? `--color: ${color}` : ""}> | ||||
|             ${label.icon | ||||
|               ? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>` | ||||
|               : nothing} | ||||
|             ${label.name} | ||||
|           </ha-label> | ||||
|         </ha-menu-item>`; | ||||
|       })} | ||||
|       <md-divider role="separator" tabindex="-1"></md-divider> | ||||
|       <ha-menu-item @click=${this._bulkCreateLabel}> | ||||
|         <div slot="headline"> | ||||
|           ${this.hass.localize("ui.panel.config.labels.add_label")} | ||||
|         </div></ha-menu-item | ||||
|       >`; | ||||
|  | ||||
|     return html` | ||||
|       <hass-tabs-subpage-data-table | ||||
|         .hass=${this.hass} | ||||
| @@ -595,9 +549,6 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) { | ||||
|         )} | ||||
|         .columns=${this._columns(this.hass.localize, this.narrow)} | ||||
|         .data=${devicesOutput} | ||||
|         selectable | ||||
|         .selected=${this._selected.length} | ||||
|         @selection-changed=${this._handleSelectionChanged} | ||||
|         .filter=${this._filter} | ||||
|         hasFilters | ||||
|         .filters=${Object.values(this._filters).filter( | ||||
| @@ -670,49 +621,6 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) { | ||||
|           .narrow=${this.narrow} | ||||
|           @expanded-changed=${this._filterExpanded} | ||||
|         ></ha-filter-labels> | ||||
|  | ||||
|         ${!this.narrow | ||||
|           ? html`<ha-button-menu-new 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-button-menu-new>` | ||||
|           : html` <ha-button-menu-new has-overflow slot="selection-bar" | ||||
|               ><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> | ||||
|               <ha-sub-menu> | ||||
|                 <ha-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-menu-item> | ||||
|                 <ha-menu slot="menu">${labelItems}</ha-menu> | ||||
|               </ha-sub-menu> | ||||
|             </ha-button-menu-new>`} | ||||
|       </hass-tabs-subpage-data-table> | ||||
|     `; | ||||
|   } | ||||
| @@ -792,45 +700,6 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) { | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private _handleSelectionChanged( | ||||
|     ev: HASSDomEvent<SelectionChangedEvent> | ||||
|   ): void { | ||||
|     this._selected = ev.detail.value; | ||||
|   } | ||||
|  | ||||
|   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) => { | ||||
|       promises.push( | ||||
|         updateDeviceRegistryEntry(this.hass, deviceId, { | ||||
|           labels: | ||||
|             action === "add" | ||||
|               ? this.hass.devices[deviceId].labels.concat(label) | ||||
|               : this.hass.devices[deviceId].labels.filter( | ||||
|                   (lbl) => lbl !== label | ||||
|                 ), | ||||
|         }) | ||||
|       ); | ||||
|     }); | ||||
|     await Promise.all(promises); | ||||
|   } | ||||
|  | ||||
|   private _bulkCreateLabel() { | ||||
|     showLabelDetailDialog(this, { | ||||
|       createEntry: async (values) => { | ||||
|         const label = await createLabelRegistryEntry(this.hass, values); | ||||
|         this._bulkLabel(label.label_id, "add"); | ||||
|         return label; | ||||
|       }, | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   static get styles(): CSSResultGroup { | ||||
|     return [ | ||||
|       css` | ||||
| @@ -852,16 +721,6 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) { | ||||
|           text-transform: uppercase; | ||||
|           direction: var(--direction); | ||||
|         } | ||||
|         ha-assist-chip { | ||||
|           --ha-assist-chip-container-shape: 10px; | ||||
|         } | ||||
|         ha-button-menu-new ha-assist-chip { | ||||
|           --md-assist-chip-trailing-space: 8px; | ||||
|         } | ||||
|         ha-label { | ||||
|           --ha-label-background-color: var(--color, var(--grey-color)); | ||||
|           --ha-label-background-opacity: 0.5; | ||||
|         } | ||||
|       `, | ||||
|       haStyle, | ||||
|     ]; | ||||
|   | ||||
| @@ -3,17 +3,12 @@ import "@lrnwebcomponents/simple-tooltip/simple-tooltip"; | ||||
| import { | ||||
|   mdiAlertCircle, | ||||
|   mdiCancel, | ||||
|   mdiChevronRight, | ||||
|   mdiDelete, | ||||
|   mdiDotsVertical, | ||||
|   mdiEye, | ||||
|   mdiEyeOff, | ||||
|   mdiMenuDown, | ||||
|   mdiPencilOff, | ||||
|   mdiPlus, | ||||
|   mdiRestoreAlert, | ||||
|   mdiToggleSwitch, | ||||
|   mdiToggleSwitchOffOutline, | ||||
|   mdiUndo, | ||||
| } from "@mdi/js"; | ||||
| import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; | ||||
| import { | ||||
| @@ -29,7 +24,6 @@ import { ifDefined } from "lit/directives/if-defined"; | ||||
| import { styleMap } from "lit/directives/style-map"; | ||||
| import { until } from "lit/directives/until"; | ||||
| import memoize from "memoize-one"; | ||||
| import { computeCssColor } from "../../../common/color/compute-color"; | ||||
| import type { HASSDomEvent } from "../../../common/dom/fire_event"; | ||||
| import { computeDomain } from "../../../common/entity/compute_domain"; | ||||
| import { computeStateName } from "../../../common/entity/compute_state_name"; | ||||
| @@ -50,19 +44,16 @@ import "../../../components/ha-check-list-item"; | ||||
| import "../../../components/ha-filter-devices"; | ||||
| import "../../../components/ha-filter-floor-areas"; | ||||
| import "../../../components/ha-filter-integrations"; | ||||
| import "../../../components/ha-filter-labels"; | ||||
| import "../../../components/ha-filter-states"; | ||||
| import "../../../components/ha-filter-labels"; | ||||
| import "../../../components/ha-icon"; | ||||
| import "../../../components/ha-icon-button"; | ||||
| import "../../../components/ha-menu-item"; | ||||
| import "../../../components/ha-sub-menu"; | ||||
| import "../../../components/ha-svg-icon"; | ||||
| import { ConfigEntry, getConfigEntries } from "../../../data/config_entries"; | ||||
| import { fullEntitiesContext } from "../../../data/context"; | ||||
| import { UNAVAILABLE } from "../../../data/entity"; | ||||
| import { | ||||
|   EntityRegistryEntry, | ||||
|   UpdateEntityRegistryEntryResult, | ||||
|   computeEntityRegistryName, | ||||
|   removeEntityRegistryEntry, | ||||
|   updateEntityRegistryEntry, | ||||
| @@ -70,7 +61,6 @@ import { | ||||
| import { entryIcon } from "../../../data/icons"; | ||||
| import { | ||||
|   LabelRegistryEntry, | ||||
|   createLabelRegistryEntry, | ||||
|   subscribeLabelRegistry, | ||||
| } from "../../../data/label_registry"; | ||||
| import { | ||||
| @@ -87,11 +77,6 @@ import type { HomeAssistant, Route } from "../../../types"; | ||||
| import { configSections } from "../ha-panel-config"; | ||||
| import "../integrations/ha-integration-overflow-menu"; | ||||
| import { showAddIntegrationDialog } from "../integrations/show-add-integration-dialog"; | ||||
| import { showLabelDetailDialog } from "../labels/show-dialog-label-detail"; | ||||
| import { | ||||
|   EntitySources, | ||||
|   fetchEntitySourcesWithCache, | ||||
| } from "../../../data/entity_sources"; | ||||
|  | ||||
| export interface StateEntity | ||||
|   extends Omit<EntityRegistryEntry, "id" | "unique_id"> { | ||||
| @@ -138,15 +123,13 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { | ||||
|     { value: string[] | undefined; items: Set<string> | undefined } | ||||
|   > = {}; | ||||
|  | ||||
|   @state() private _selected: string[] = []; | ||||
|   @state() private _selectedEntities: string[] = []; | ||||
|  | ||||
|   @state() private _expandedFilter?: string; | ||||
|  | ||||
|   @state() | ||||
|   _labels!: LabelRegistryEntry[]; | ||||
|  | ||||
|   @state() private _entitySources?: EntitySources; | ||||
|  | ||||
|   @query("hass-tabs-subpage-data-table", true) | ||||
|   private _dataTable!: HaTabsSubpageDataTable; | ||||
|  | ||||
| @@ -411,12 +394,10 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { | ||||
|           const entryIds = entries | ||||
|             .filter((entry) => filter.value!.includes(entry.domain)) | ||||
|             .map((entry) => entry.entry_id); | ||||
|  | ||||
|           filteredEntities = filteredEntities.filter( | ||||
|             (entity) => | ||||
|               filter.value?.includes(entity.platform) || | ||||
|               (entity.config_entry_id && | ||||
|                 entryIds.includes(entity.config_entry_id)) | ||||
|               entity.config_entry_id && | ||||
|               entryIds.includes(entity.config_entry_id) | ||||
|           ); | ||||
|           filter.value!.forEach((domain) => filteredDomains.add(domain)); | ||||
|         } else if (key === "ha-filter-labels" && filter.value?.length) { | ||||
| @@ -524,50 +505,13 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { | ||||
|         [...filteredDomains][0] | ||||
|       ); | ||||
|  | ||||
|     const labelItems = html` ${this._labels?.map((label) => { | ||||
|         const color = label.color ? computeCssColor(label.color) : undefined; | ||||
|         const selected = this._selected.every((entityId) => | ||||
|           this.hass.entities[entityId]?.labels.includes(label.label_id) | ||||
|         ); | ||||
|         const partial = | ||||
|           !selected && | ||||
|           this._selected.some((entityId) => | ||||
|             this.hass.entities[entityId]?.labels.includes(label.label_id) | ||||
|           ); | ||||
|         return html`<ha-menu-item | ||||
|           .value=${label.label_id} | ||||
|           .action=${selected ? "remove" : "add"} | ||||
|           @click=${this._handleBulkLabel} | ||||
|           keep-open | ||||
|         > | ||||
|           <ha-checkbox | ||||
|             slot="start" | ||||
|             .checked=${selected} | ||||
|             .indeterminate=${partial} | ||||
|             reducedTouchTarget | ||||
|           ></ha-checkbox> | ||||
|           <ha-label style=${color ? `--color: ${color}` : ""}> | ||||
|             ${label.icon | ||||
|               ? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>` | ||||
|               : nothing} | ||||
|             ${label.name} | ||||
|           </ha-label> | ||||
|         </ha-menu-item>`; | ||||
|       })} | ||||
|       <md-divider role="separator" tabindex="-1"></md-divider> | ||||
|       <ha-menu-item @click=${this._bulkCreateLabel}> | ||||
|         <div slot="headline"> | ||||
|           ${this.hass.localize("ui.panel.config.labels.add_label")} | ||||
|         </div></ha-menu-item | ||||
|       >`; | ||||
|  | ||||
|     return html` | ||||
|       <hass-tabs-subpage-data-table | ||||
|         .hass=${this.hass} | ||||
|         .narrow=${this.narrow} | ||||
|         .backPath=${ | ||||
|           this._searchParms.has("historyBack") ? undefined : "/config" | ||||
|         } | ||||
|         .backPath=${this._searchParms.has("historyBack") | ||||
|           ? undefined | ||||
|           : "/config"} | ||||
|         .route=${this.route} | ||||
|         .tabs=${configSections.devices} | ||||
|         .columns=${this._columns( | ||||
| @@ -580,13 +524,12 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { | ||||
|           "ui.panel.config.entities.picker.search" | ||||
|         )} | ||||
|         hasFilters | ||||
|         .filters=${ | ||||
|           Object.values(this._filters).filter((filter) => filter.value?.length) | ||||
|             .length | ||||
|         } | ||||
|         .filters=${Object.values(this._filters).filter( | ||||
|           (filter) => filter.value?.length | ||||
|         ).length} | ||||
|         .filter=${this._filter} | ||||
|         selectable | ||||
|         .selected=${this._selected.length} | ||||
|         .selected=${this._selectedEntities.length} | ||||
|         @selection-changed=${this._handleSelectionChanged} | ||||
|         clickable | ||||
|         @clear-filter=${this._clearFilter} | ||||
| @@ -600,131 +543,100 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { | ||||
|           .hass=${this.hass} | ||||
|           slot="toolbar-icon" | ||||
|         ></ha-integration-overflow-menu> | ||||
|  | ||||
|  | ||||
| ${ | ||||
|   !this.narrow | ||||
|     ? html`<ha-button-menu-new 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-button-menu-new>` | ||||
|     : nothing | ||||
| } | ||||
| <ha-button-menu-new 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=${"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-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-menu-item> | ||||
|             <ha-menu slot="menu">${labelItems}</ha-menu> | ||||
|           </ha-sub-menu> | ||||
|           <md-divider role="separator" tabindex="-1"></md-divider>` | ||||
|       : nothing | ||||
|   } | ||||
|  | ||||
|   <ha-menu-item @click=${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-menu-item> | ||||
|   <ha-menu-item @click=${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-menu-item> | ||||
|   <md-divider role="separator" tabindex="-1"></md-divider> | ||||
|  | ||||
|   <ha-menu-item @click=${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-menu-item> | ||||
|   <ha-menu-item @click=${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-menu-item> | ||||
|   <md-divider role="separator" tabindex="-1"></md-divider> | ||||
|  | ||||
|   <ha-menu-item @click=${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.remove_selected.button" | ||||
|       )} | ||||
|     </div> | ||||
|   </ha-menu-item> | ||||
|  | ||||
| </ha-button-menu-new> | ||||
|         ${ | ||||
|           this._filters.config_entry?.value?.length | ||||
|             ? html`<ha-alert slot="filter-pane"> | ||||
|                 Filtering by config entry | ||||
|                 ${this._entries?.find( | ||||
|                   (entry) => | ||||
|                     entry.entry_id === this._filters.config_entry!.value![0] | ||||
|                 )?.title || this._filters.config_entry.value[0]} | ||||
|               </ha-alert>` | ||||
|             : nothing | ||||
|         } | ||||
|         <div class="header-btns" slot="selection-bar"> | ||||
|           ${!this.narrow | ||||
|             ? html` | ||||
|                 <mwc-button | ||||
|                   @click=${this._enableSelected} | ||||
|                   .disabled=${!this._selectedEntities.length} | ||||
|                   >${this.hass.localize( | ||||
|                     "ui.panel.config.entities.picker.enable_selected.button" | ||||
|                   )}</mwc-button | ||||
|                 > | ||||
|                 <mwc-button | ||||
|                   @click=${this._disableSelected} | ||||
|                   .disabled=${!this._selectedEntities.length} | ||||
|                   >${this.hass.localize( | ||||
|                     "ui.panel.config.entities.picker.disable_selected.button" | ||||
|                   )}</mwc-button | ||||
|                 > | ||||
|                 <mwc-button | ||||
|                   @click=${this._hideSelected} | ||||
|                   .disabled=${!this._selectedEntities.length} | ||||
|                   >${this.hass.localize( | ||||
|                     "ui.panel.config.entities.picker.hide_selected.button" | ||||
|                   )}</mwc-button | ||||
|                 > | ||||
|                 <mwc-button | ||||
|                   @click=${this._removeSelected} | ||||
|                   .disabled=${!this._selectedEntities.length} | ||||
|                   class="warning" | ||||
|                   >${this.hass.localize( | ||||
|                     "ui.panel.config.entities.picker.remove_selected.button" | ||||
|                   )}</mwc-button | ||||
|                 > | ||||
|               ` | ||||
|             : html` | ||||
|                 <ha-icon-button | ||||
|                   id="enable-btn" | ||||
|                   .disabled=${!this._selectedEntities.length} | ||||
|                   @click=${this._enableSelected} | ||||
|                   .path=${mdiUndo} | ||||
|                   .label=${this.hass.localize("ui.common.enable")} | ||||
|                 ></ha-icon-button> | ||||
|                 <simple-tooltip animation-delay="0" for="enable-btn"> | ||||
|                   ${this.hass.localize( | ||||
|                     "ui.panel.config.entities.picker.enable_selected.button" | ||||
|                   )} | ||||
|                 </simple-tooltip> | ||||
|                 <ha-icon-button | ||||
|                   id="disable-btn" | ||||
|                   .disabled=${!this._selectedEntities.length} | ||||
|                   @click=${this._disableSelected} | ||||
|                   .path=${mdiCancel} | ||||
|                   .label=${this.hass.localize("ui.common.disable")} | ||||
|                 ></ha-icon-button> | ||||
|                 <simple-tooltip animation-delay="0" for="disable-btn"> | ||||
|                   ${this.hass.localize( | ||||
|                     "ui.panel.config.entities.picker.disable_selected.button" | ||||
|                   )} | ||||
|                 </simple-tooltip> | ||||
|                 <ha-icon-button | ||||
|                   id="hide-btn" | ||||
|                   .disabled=${!this._selectedEntities.length} | ||||
|                   @click=${this._hideSelected} | ||||
|                   .path=${mdiEyeOff} | ||||
|                   .label=${this.hass.localize("ui.common.hide")} | ||||
|                 ></ha-icon-button> | ||||
|                 <simple-tooltip animation-delay="0" for="hide-btn"> | ||||
|                   ${this.hass.localize( | ||||
|                     "ui.panel.config.entities.picker.hide_selected.button" | ||||
|                   )} | ||||
|                 </simple-tooltip> | ||||
|                 <ha-icon-button | ||||
|                   class="warning" | ||||
|                   id="remove-btn" | ||||
|                   .disabled=${!this._selectedEntities.length} | ||||
|                   @click=${this._removeSelected} | ||||
|                   .path=${mdiDelete} | ||||
|                   .label=${this.hass.localize("ui.common.remove")} | ||||
|                 ></ha-icon-button> | ||||
|                 <simple-tooltip animation-delay="0" for="remove-btn"> | ||||
|                   ${this.hass.localize( | ||||
|                     "ui.panel.config.entities.picker.remove_selected.button" | ||||
|                   )} | ||||
|                 </simple-tooltip> | ||||
|               `} | ||||
|         </div> | ||||
|         ${this._filters.config_entry?.value?.length | ||||
|           ? html`<ha-alert slot="filter-pane"> | ||||
|               Filtering by config entry | ||||
|               ${this._entries?.find( | ||||
|                 (entry) => | ||||
|                   entry.entry_id === this._filters.config_entry!.value![0] | ||||
|               )?.title || this._filters.config_entry.value[0]} | ||||
|             </ha-alert>` | ||||
|           : nothing} | ||||
|         <ha-filter-floor-areas | ||||
|           .hass=${this.hass} | ||||
|           type="entity" | ||||
| @@ -776,20 +688,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> | ||||
|     `; | ||||
|   } | ||||
| @@ -815,9 +723,6 @@ ${ | ||||
|       }, | ||||
|     }; | ||||
|     this._setFiltersFromUrl(); | ||||
|     fetchEntitySourcesWithCache(this.hass).then((sources) => { | ||||
|       this._entitySources = sources; | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private _setFiltersFromUrl() { | ||||
| @@ -876,18 +781,14 @@ ${ | ||||
|     this._filters = {}; | ||||
|   } | ||||
|  | ||||
|   public willUpdate(changedProps: PropertyValues): void { | ||||
|   public willUpdate(changedProps: PropertyValues<this>): void { | ||||
|     super.willUpdate(changedProps); | ||||
|     const oldHass = changedProps.get("hass"); | ||||
|     let changed = false; | ||||
|     if (!this.hass || !this._entities) { | ||||
|       return; | ||||
|     } | ||||
|     if ( | ||||
|       changedProps.has("hass") || | ||||
|       changedProps.has("_entities") || | ||||
|       changedProps.has("_entitySources") | ||||
|     ) { | ||||
|     if (changedProps.has("hass") || changedProps.has("_entities")) { | ||||
|       const stateEntities: StateEntity[] = []; | ||||
|       const regEntityIds = new Set( | ||||
|         this._entities.map((entity) => entity.entity_id) | ||||
| @@ -898,7 +799,6 @@ ${ | ||||
|         } | ||||
|         if ( | ||||
|           !oldHass || | ||||
|           changedProps.has("_entitySources") || | ||||
|           this.hass.states[entityId] !== oldHass.states[entityId] | ||||
|         ) { | ||||
|           changed = true; | ||||
| @@ -906,8 +806,7 @@ ${ | ||||
|         stateEntities.push({ | ||||
|           name: computeStateName(this.hass.states[entityId]), | ||||
|           entity_id: entityId, | ||||
|           platform: | ||||
|             this._entitySources?.[entityId]?.domain || computeDomain(entityId), | ||||
|           platform: computeDomain(entityId), | ||||
|           disabled_by: null, | ||||
|           hidden_by: null, | ||||
|           area_id: null, | ||||
| @@ -937,14 +836,14 @@ ${ | ||||
|   private _handleSelectionChanged( | ||||
|     ev: HASSDomEvent<SelectionChangedEvent> | ||||
|   ): void { | ||||
|     this._selected = ev.detail.value; | ||||
|     this._selectedEntities = ev.detail.value; | ||||
|   } | ||||
|  | ||||
|   private async _enableSelected() { | ||||
|     showConfirmationDialog(this, { | ||||
|       title: this.hass.localize( | ||||
|         "ui.panel.config.entities.picker.enable_selected.confirm_title", | ||||
|         { number: this._selected.length } | ||||
|         { number: this._selectedEntities.length } | ||||
|       ), | ||||
|       text: this.hass.localize( | ||||
|         "ui.panel.config.entities.picker.enable_selected.confirm_text" | ||||
| @@ -955,7 +854,7 @@ ${ | ||||
|         let require_restart = false; | ||||
|         let reload_delay = 0; | ||||
|         await Promise.all( | ||||
|           this._selected.map(async (entity) => { | ||||
|           this._selectedEntities.map(async (entity) => { | ||||
|             const result = await updateEntityRegistryEntry(this.hass, entity, { | ||||
|               disabled_by: null, | ||||
|             }); | ||||
| @@ -992,7 +891,7 @@ ${ | ||||
|     showConfirmationDialog(this, { | ||||
|       title: this.hass.localize( | ||||
|         "ui.panel.config.entities.picker.disable_selected.confirm_title", | ||||
|         { number: this._selected.length } | ||||
|         { number: this._selectedEntities.length } | ||||
|       ), | ||||
|       text: this.hass.localize( | ||||
|         "ui.panel.config.entities.picker.disable_selected.confirm_text" | ||||
| @@ -1000,7 +899,7 @@ ${ | ||||
|       confirmText: this.hass.localize("ui.common.disable"), | ||||
|       dismissText: this.hass.localize("ui.common.cancel"), | ||||
|       confirm: () => { | ||||
|         this._selected.forEach((entity) => | ||||
|         this._selectedEntities.forEach((entity) => | ||||
|           updateEntityRegistryEntry(this.hass, entity, { | ||||
|             disabled_by: "user", | ||||
|           }) | ||||
| @@ -1014,7 +913,7 @@ ${ | ||||
|     showConfirmationDialog(this, { | ||||
|       title: this.hass.localize( | ||||
|         "ui.panel.config.entities.picker.hide_selected.confirm_title", | ||||
|         { number: this._selected.length } | ||||
|         { number: this._selectedEntities.length } | ||||
|       ), | ||||
|       text: this.hass.localize( | ||||
|         "ui.panel.config.entities.picker.hide_selected.confirm_text" | ||||
| @@ -1022,7 +921,7 @@ ${ | ||||
|       confirmText: this.hass.localize("ui.common.hide"), | ||||
|       dismissText: this.hass.localize("ui.common.cancel"), | ||||
|       confirm: () => { | ||||
|         this._selected.forEach((entity) => | ||||
|         this._selectedEntities.forEach((entity) => | ||||
|           updateEntityRegistryEntry(this.hass, entity, { | ||||
|             hidden_by: "user", | ||||
|           }) | ||||
| @@ -1032,66 +931,22 @@ ${ | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private _unhideSelected() { | ||||
|     this._selected.forEach((entity) => | ||||
|       updateEntityRegistryEntry(this.hass, entity, { | ||||
|         hidden_by: null, | ||||
|       }) | ||||
|     ); | ||||
|     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) => { | ||||
|       const entityReg = | ||||
|         this.hass.entities[entityId] || | ||||
|         this._entities.find((entReg) => entReg.entity_id === entityId); | ||||
|       if (!entityReg) { | ||||
|         return; | ||||
|       } | ||||
|       promises.push( | ||||
|         updateEntityRegistryEntry(this.hass, entityId, { | ||||
|           labels: | ||||
|             action === "add" | ||||
|               ? entityReg.labels.concat(label) | ||||
|               : entityReg.labels.filter((lbl) => lbl !== label), | ||||
|         }) | ||||
|       ); | ||||
|     }); | ||||
|     await Promise.all(promises); | ||||
|   } | ||||
|  | ||||
|   private _bulkCreateLabel() { | ||||
|     showLabelDetailDialog(this, { | ||||
|       createEntry: async (values) => { | ||||
|         const label = await createLabelRegistryEntry(this.hass, values); | ||||
|         this._bulkLabel(label.label_id, "add"); | ||||
|         return label; | ||||
|       }, | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private _removeSelected() { | ||||
|     const removeableEntities = this._selected.filter((entity) => { | ||||
|     const removeableEntities = this._selectedEntities.filter((entity) => { | ||||
|       const stateObj = this.hass.states[entity]; | ||||
|       return stateObj?.attributes.restored; | ||||
|     }); | ||||
|     showConfirmationDialog(this, { | ||||
|       title: this.hass.localize( | ||||
|         `ui.panel.config.entities.picker.remove_selected.confirm_${ | ||||
|           removeableEntities.length !== this._selected.length ? "partly_" : "" | ||||
|           removeableEntities.length !== this._selectedEntities.length | ||||
|             ? "partly_" | ||||
|             : "" | ||||
|         }title`, | ||||
|         { number: removeableEntities.length } | ||||
|       ), | ||||
|       text: | ||||
|         removeableEntities.length === this._selected.length | ||||
|         removeableEntities.length === this._selectedEntities.length | ||||
|           ? this.hass.localize( | ||||
|               "ui.panel.config.entities.picker.remove_selected.confirm_text" | ||||
|             ) | ||||
| @@ -1099,7 +954,7 @@ ${ | ||||
|               "ui.panel.config.entities.picker.remove_selected.confirm_partly_text", | ||||
|               { | ||||
|                 removable: removeableEntities.length, | ||||
|                 selected: this._selected.length, | ||||
|                 selected: this._selectedEntities.length, | ||||
|               } | ||||
|             ), | ||||
|       confirmText: this.hass.localize("ui.common.remove"), | ||||
| @@ -1225,17 +1080,6 @@ ${ | ||||
|           text-transform: uppercase; | ||||
|           direction: var(--direction); | ||||
|         } | ||||
|  | ||||
|         ha-assist-chip { | ||||
|           --ha-assist-chip-container-shape: 10px; | ||||
|         } | ||||
|         ha-button-menu-new ha-assist-chip { | ||||
|           --md-assist-chip-trailing-space: 8px; | ||||
|         } | ||||
|         ha-label { | ||||
|           --ha-label-background-color: var(--color, var(--grey-color)); | ||||
|           --ha-label-background-opacity: 0.5; | ||||
|         } | ||||
|       `, | ||||
|     ]; | ||||
|   } | ||||
|   | ||||
| @@ -1,16 +1,5 @@ | ||||
| import { consume } from "@lit-labs/context"; | ||||
| import { ResizeController } from "@lit-labs/observers/resize-controller"; | ||||
| import "@lrnwebcomponents/simple-tooltip/simple-tooltip"; | ||||
| import { | ||||
|   mdiAlertCircle, | ||||
|   mdiChevronRight, | ||||
|   mdiCog, | ||||
|   mdiDotsVertical, | ||||
|   mdiMenuDown, | ||||
|   mdiPencilOff, | ||||
|   mdiPlus, | ||||
|   mdiTag, | ||||
| } from "@mdi/js"; | ||||
| import { mdiAlertCircle, mdiPencilOff, mdiPlus } from "@mdi/js"; | ||||
| import { HassEntity } from "home-assistant-js-websocket"; | ||||
| import { | ||||
|   CSSResultGroup, | ||||
| @@ -22,9 +11,8 @@ import { | ||||
|   nothing, | ||||
| } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { consume } from "@lit-labs/context"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { computeCssColor } from "../../../common/color/compute-color"; | ||||
| import { HASSDomEvent } from "../../../common/dom/fire_event"; | ||||
| import { computeStateDomain } from "../../../common/entity/compute_state_domain"; | ||||
| import { navigate } from "../../../common/navigate"; | ||||
| import { | ||||
| @@ -35,42 +23,22 @@ import { extractSearchParam } from "../../../common/url/search-params"; | ||||
| import { | ||||
|   DataTableColumnContainer, | ||||
|   RowClickedEvent, | ||||
|   SelectionChangedEvent, | ||||
| } from "../../../components/data-table/ha-data-table"; | ||||
| import "../../../components/data-table/ha-data-table-labels"; | ||||
| import "../../../components/ha-fab"; | ||||
| import "../../../components/ha-filter-categories"; | ||||
| import "../../../components/ha-filter-devices"; | ||||
| import "../../../components/ha-filter-entities"; | ||||
| import "../../../components/ha-filter-floor-areas"; | ||||
| import "../../../components/ha-filter-labels"; | ||||
| import "../../../components/ha-icon"; | ||||
| import "../../../components/ha-icon-overflow-menu"; | ||||
| import "../../../components/ha-state-icon"; | ||||
| import "../../../components/ha-svg-icon"; | ||||
| import { | ||||
|   CategoryRegistryEntry, | ||||
|   createCategoryRegistryEntry, | ||||
|   subscribeCategoryRegistry, | ||||
| } from "../../../data/category_registry"; | ||||
| import { | ||||
|   ConfigEntry, | ||||
|   subscribeConfigEntries, | ||||
| } from "../../../data/config_entries"; | ||||
| import { getConfigFlowHandlers } from "../../../data/config_flow"; | ||||
| import { fullEntitiesContext } from "../../../data/context"; | ||||
| import { | ||||
|   EntityRegistryEntry, | ||||
|   UpdateEntityRegistryEntryResult, | ||||
|   subscribeEntityRegistry, | ||||
|   updateEntityRegistryEntry, | ||||
| } from "../../../data/entity_registry"; | ||||
| import { domainToName } from "../../../data/integration"; | ||||
| import { | ||||
|   LabelRegistryEntry, | ||||
|   createLabelRegistryEntry, | ||||
|   subscribeLabelRegistry, | ||||
| } from "../../../data/label_registry"; | ||||
| import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow"; | ||||
| import { showOptionsFlowDialog } from "../../../dialogs/config-flow/show-dialog-options-flow"; | ||||
| import { | ||||
| @@ -81,15 +49,18 @@ import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info | ||||
| import "../../../layouts/hass-loading-screen"; | ||||
| import "../../../layouts/hass-tabs-subpage-data-table"; | ||||
| import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; | ||||
| import { haStyle } from "../../../resources/styles"; | ||||
| import { HomeAssistant, Route } from "../../../types"; | ||||
| import { showAssignCategoryDialog } from "../category/show-dialog-assign-category"; | ||||
| import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail"; | ||||
| import { configSections } from "../ha-panel-config"; | ||||
| import "../integrations/ha-integration-overflow-menu"; | ||||
| import { showLabelDetailDialog } from "../labels/show-dialog-label-detail"; | ||||
| import { isHelperDomain } from "./const"; | ||||
| import { showHelperDetailDialog } from "./show-dialog-helper-detail"; | ||||
| import { | ||||
|   LabelRegistryEntry, | ||||
|   subscribeLabelRegistry, | ||||
| } from "../../../data/label_registry"; | ||||
| import { fullEntitiesContext } from "../../../data/context"; | ||||
| import "../../../components/ha-filter-labels"; | ||||
| import { haStyle } from "../../../resources/styles"; | ||||
|  | ||||
| type HelperItem = { | ||||
|   id: string; | ||||
| @@ -100,7 +71,6 @@ type HelperItem = { | ||||
|   type: string; | ||||
|   configEntry?: ConfigEntry; | ||||
|   entity?: HassEntity; | ||||
|   category: string | undefined; | ||||
|   label_entries: LabelRegistryEntry[]; | ||||
| }; | ||||
|  | ||||
| @@ -141,8 +111,6 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { | ||||
|  | ||||
|   @state() private _configEntries?: Record<string, ConfigEntry>; | ||||
|  | ||||
|   @state() private _selected: string[] = []; | ||||
|  | ||||
|   @state() private _activeFilters?: string[]; | ||||
|  | ||||
|   @state() private _filters: Record< | ||||
| @@ -152,9 +120,6 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { | ||||
|  | ||||
|   @state() private _expandedFilter?: string; | ||||
|  | ||||
|   @state() | ||||
|   _categories!: CategoryRegistryEntry[]; | ||||
|  | ||||
|   @state() | ||||
|   _labels!: LabelRegistryEntry[]; | ||||
|  | ||||
| @@ -164,10 +129,6 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { | ||||
|  | ||||
|   @state() private _filteredStateItems?: string[] | null; | ||||
|  | ||||
|   private _sizeController = new ResizeController(this, { | ||||
|     callback: (entries) => entries[0]?.contentRect.width, | ||||
|   }); | ||||
|  | ||||
|   public hassSubscribe() { | ||||
|     return [ | ||||
|       subscribeConfigEntries( | ||||
| @@ -195,86 +156,65 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { | ||||
|       subscribeLabelRegistry(this.hass.connection, (labels) => { | ||||
|         this._labels = labels; | ||||
|       }), | ||||
|       subscribeCategoryRegistry( | ||||
|         this.hass.connection, | ||||
|         "helpers", | ||||
|         (categories) => { | ||||
|           this._categories = categories; | ||||
|         } | ||||
|       ), | ||||
|     ]; | ||||
|   } | ||||
|  | ||||
|   private _columns = memoizeOne( | ||||
|     ( | ||||
|       narrow: boolean, | ||||
|       localize: LocalizeFunc | ||||
|     ): DataTableColumnContainer<HelperItem> => ({ | ||||
|       icon: { | ||||
|         title: "", | ||||
|         label: localize("ui.panel.config.helpers.picker.headers.icon"), | ||||
|         type: "icon", | ||||
|         template: (helper) => | ||||
|           helper.entity | ||||
|             ? html`<ha-state-icon | ||||
|                 .hass=${this.hass} | ||||
|                 .stateObj=${helper.entity} | ||||
|               ></ha-state-icon>` | ||||
|             : html`<ha-svg-icon | ||||
|                 .path=${helper.icon} | ||||
|                 style="color: var(--error-color)" | ||||
|               ></ha-svg-icon>`, | ||||
|       }, | ||||
|       name: { | ||||
|         title: localize("ui.panel.config.helpers.picker.headers.name"), | ||||
|         main: true, | ||||
|         sortable: true, | ||||
|         filterable: true, | ||||
|         grows: true, | ||||
|         direction: "asc", | ||||
|         template: (helper) => html` | ||||
|           <div style="font-size: 14px;">${helper.name}</div> | ||||
|           ${narrow | ||||
|             ? html`<div class="secondary">${helper.entity_id}</div> ` | ||||
|             : nothing} | ||||
|           ${helper.label_entries.length | ||||
|             ? html` | ||||
|                 <ha-data-table-labels | ||||
|                   .labels=${helper.label_entries} | ||||
|                 ></ha-data-table-labels> | ||||
|               ` | ||||
|             : nothing} | ||||
|         `, | ||||
|       }, | ||||
|       entity_id: { | ||||
|         title: localize("ui.panel.config.helpers.picker.headers.entity_id"), | ||||
|         hidden: this.narrow, | ||||
|         sortable: true, | ||||
|         filterable: true, | ||||
|         width: "25%", | ||||
|       }, | ||||
|       category: { | ||||
|         title: localize("ui.panel.config.helpers.picker.headers.category"), | ||||
|         hidden: true, | ||||
|         groupable: true, | ||||
|         filterable: true, | ||||
|         sortable: true, | ||||
|       }, | ||||
|       labels: { | ||||
|         title: "", | ||||
|         hidden: true, | ||||
|         filterable: true, | ||||
|         template: (helper) => | ||||
|           helper.label_entries.map((lbl) => lbl.name).join(" "), | ||||
|       }, | ||||
|       localized_type: { | ||||
|     (narrow: boolean, localize: LocalizeFunc): DataTableColumnContainer => { | ||||
|       const columns: DataTableColumnContainer<HelperItem> = { | ||||
|         icon: { | ||||
|           title: "", | ||||
|           label: localize("ui.panel.config.helpers.picker.headers.icon"), | ||||
|           type: "icon", | ||||
|           template: (helper) => | ||||
|             helper.entity | ||||
|               ? html`<ha-state-icon | ||||
|                   .hass=${this.hass} | ||||
|                   .stateObj=${helper.entity} | ||||
|                 ></ha-state-icon>` | ||||
|               : html`<ha-svg-icon | ||||
|                   .path=${helper.icon} | ||||
|                   style="color: var(--error-color)" | ||||
|                 ></ha-svg-icon>`, | ||||
|         }, | ||||
|         name: { | ||||
|           title: localize("ui.panel.config.helpers.picker.headers.name"), | ||||
|           main: true, | ||||
|           sortable: true, | ||||
|           filterable: true, | ||||
|           grows: true, | ||||
|           direction: "asc", | ||||
|           template: (helper) => html` | ||||
|             <div style="font-size: 14px;">${helper.name}</div> | ||||
|             ${narrow | ||||
|               ? html`<div class="secondary">${helper.entity_id}</div> ` | ||||
|               : nothing} | ||||
|             ${helper.label_entries.length | ||||
|               ? html` | ||||
|                   <ha-data-table-labels | ||||
|                     .labels=${helper.label_entries} | ||||
|                   ></ha-data-table-labels> | ||||
|                 ` | ||||
|               : nothing} | ||||
|           `, | ||||
|         }, | ||||
|       }; | ||||
|       if (!narrow) { | ||||
|         columns.entity_id = { | ||||
|           title: localize("ui.panel.config.helpers.picker.headers.entity_id"), | ||||
|           sortable: true, | ||||
|           filterable: true, | ||||
|           width: "25%", | ||||
|         }; | ||||
|       } | ||||
|       columns.localized_type = { | ||||
|         title: localize("ui.panel.config.helpers.picker.headers.type"), | ||||
|         sortable: true, | ||||
|         width: "25%", | ||||
|         filterable: true, | ||||
|         groupable: true, | ||||
|       }, | ||||
|       editable: { | ||||
|       }; | ||||
|       columns.editable = { | ||||
|         title: "", | ||||
|         label: this.hass.localize( | ||||
|           "ui.panel.config.helpers.picker.headers.editable" | ||||
| @@ -297,36 +237,9 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { | ||||
|               ` | ||||
|             : ""} | ||||
|         `, | ||||
|       }, | ||||
|       actions: { | ||||
|         title: "", | ||||
|         width: "64px", | ||||
|         type: "overflow-menu", | ||||
|         template: (helper) => html` | ||||
|           <ha-icon-overflow-menu | ||||
|             .hass=${this.hass} | ||||
|             narrow | ||||
|             .items=${[ | ||||
|               { | ||||
|                 path: mdiCog, | ||||
|                 label: this.hass.localize( | ||||
|                   "ui.panel.config.automation.picker.show_settings" | ||||
|                 ), | ||||
|                 action: () => this._openSettings(helper), | ||||
|               }, | ||||
|               { | ||||
|                 path: mdiTag, | ||||
|                 label: this.hass.localize( | ||||
|                   `ui.panel.config.automation.picker.${helper.category ? "edit_category" : "assign_category"}` | ||||
|                 ), | ||||
|                 action: () => this._editCategory(helper), | ||||
|               }, | ||||
|             ]} | ||||
|           > | ||||
|           </ha-icon-overflow-menu> | ||||
|         `, | ||||
|       }, | ||||
|     }) | ||||
|       }; | ||||
|       return columns; | ||||
|     } | ||||
|   ); | ||||
|  | ||||
|   private _getItems = memoizeOne( | ||||
| @@ -336,7 +249,6 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { | ||||
|       entityEntries: Record<string, EntityRegistryEntry>, | ||||
|       configEntries: Record<string, ConfigEntry>, | ||||
|       entityReg: EntityRegistryEntry[], | ||||
|       categoryReg?: CategoryRegistryEntry[], | ||||
|       labelReg?: LabelRegistryEntry[], | ||||
|       filteredStateItems?: string[] | null | ||||
|     ): HelperItem[] => { | ||||
| @@ -380,7 +292,6 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { | ||||
|         type: configEntry.domain, | ||||
|         configEntry, | ||||
|         entity: undefined, | ||||
|         selectable: false, | ||||
|       })); | ||||
|  | ||||
|       return [...states, ...entries] | ||||
| @@ -394,7 +305,6 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { | ||||
|             (reg) => reg.entity_id === item.entity_id | ||||
|           ); | ||||
|           const labels = labelReg && entityRegEntry?.labels; | ||||
|           const category = entityRegEntry?.categories.helpers; | ||||
|           return { | ||||
|             ...item, | ||||
|             localized_type: item.configEntry | ||||
| @@ -405,9 +315,6 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { | ||||
|             label_entries: (labels || []).map( | ||||
|               (lbl) => labelReg!.find((label) => label.label_id === lbl)! | ||||
|             ), | ||||
|             category: category | ||||
|               ? categoryReg?.find((cat) => cat.category_id === category)?.name | ||||
|               : undefined, | ||||
|           }; | ||||
|         }); | ||||
|     } | ||||
| @@ -423,69 +330,6 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { | ||||
|       return html` <hass-loading-screen></hass-loading-screen> `; | ||||
|     } | ||||
|  | ||||
|     const categoryItems = html`${this._categories?.map( | ||||
|         (category) => | ||||
|           html`<ha-menu-item | ||||
|             .value=${category.category_id} | ||||
|             @click=${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-menu-item>` | ||||
|       )} | ||||
|       <ha-menu-item .value=${null} @click=${this._handleBulkCategory}> | ||||
|         <div slot="headline"> | ||||
|           ${this.hass.localize( | ||||
|             "ui.panel.config.automation.picker.bulk_actions.no_category" | ||||
|           )} | ||||
|         </div> | ||||
|       </ha-menu-item> | ||||
|       <md-divider role="separator" tabindex="-1"></md-divider> | ||||
|       <ha-menu-item @click=${this._bulkCreateCategory}> | ||||
|         <div slot="headline"> | ||||
|           ${this.hass.localize("ui.panel.config.category.editor.add")} | ||||
|         </div> | ||||
|       </ha-menu-item>`; | ||||
|     const labelItems = html`${this._labels?.map((label) => { | ||||
|         const color = label.color ? computeCssColor(label.color) : undefined; | ||||
|         const selected = this._selected.every((entityId) => | ||||
|           this.hass.entities[entityId]?.labels.includes(label.label_id) | ||||
|         ); | ||||
|         const partial = | ||||
|           !selected && | ||||
|           this._selected.some((entityId) => | ||||
|             this.hass.entities[entityId]?.labels.includes(label.label_id) | ||||
|           ); | ||||
|         return html`<ha-menu-item | ||||
|           .value=${label.label_id} | ||||
|           .action=${selected ? "remove" : "add"} | ||||
|           @click=${this._handleBulkLabel} | ||||
|           keep-open | ||||
|         > | ||||
|           <ha-checkbox | ||||
|             slot="start" | ||||
|             .checked=${selected} | ||||
|             .indeterminate=${partial} | ||||
|             reducedTouchTarget | ||||
|           ></ha-checkbox> | ||||
|           <ha-label style=${color ? `--color: ${color}` : ""}> | ||||
|             ${label.icon | ||||
|               ? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>` | ||||
|               : nothing} | ||||
|             ${label.name} | ||||
|           </ha-label> | ||||
|         </ha-menu-item> `; | ||||
|       })}<md-divider role="separator" tabindex="-1"></md-divider> | ||||
|       <ha-menu-item @click=${this._bulkCreateLabel}> | ||||
|         <div slot="headline"> | ||||
|           ${this.hass.localize("ui.panel.config.labels.add_label")} | ||||
|         </div> | ||||
|       </ha-menu-item>`; | ||||
|     const labelsInOverflow = | ||||
|       (this._sizeController.value && this._sizeController.value < 700) || | ||||
|       (!this._sizeController.value && this.hass.dockedSidebar === "docked"); | ||||
|     return html` | ||||
|       <hass-tabs-subpage-data-table | ||||
|         .hass=${this.hass} | ||||
| @@ -493,9 +337,6 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { | ||||
|         back-path="/config" | ||||
|         .route=${this.route} | ||||
|         .tabs=${configSections.devices} | ||||
|         selectable | ||||
|         .selected=${this._selected.length} | ||||
|         @selection-changed=${this._handleSelectionChanged} | ||||
|         hasFilters | ||||
|         .filters=${Object.values(this._filters).filter( | ||||
|           (filter) => filter.value?.length | ||||
| @@ -507,11 +348,9 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { | ||||
|           this._entityEntries, | ||||
|           this._configEntries, | ||||
|           this._entityReg, | ||||
|           this._categories, | ||||
|           this._labels, | ||||
|           this._filteredStateItems | ||||
|         )} | ||||
|         initialGroupColumn="category" | ||||
|         .activeFilters=${this._activeFilters} | ||||
|         @clear-filter=${this._clearFilter} | ||||
|         @row-click=${this._openEditDialog} | ||||
| @@ -522,26 +361,6 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { | ||||
|         )} | ||||
|         class=${this.narrow ? "narrow" : ""} | ||||
|       > | ||||
|         <ha-filter-floor-areas | ||||
|           .hass=${this.hass} | ||||
|           .type=${"entity"} | ||||
|           .value=${this._filters["ha-filter-floor-areas"]?.value} | ||||
|           @data-table-filter-changed=${this._filterChanged} | ||||
|           slot="filter-pane" | ||||
|           .expanded=${this._expandedFilter === "ha-filter-floor-areas"} | ||||
|           .narrow=${this.narrow} | ||||
|           @expanded-changed=${this._filterExpanded} | ||||
|         ></ha-filter-floor-areas> | ||||
|         <ha-filter-devices | ||||
|           .hass=${this.hass} | ||||
|           .type=${"entity"} | ||||
|           .value=${this._filters["ha-filter-devices"]?.value} | ||||
|           @data-table-filter-changed=${this._filterChanged} | ||||
|           slot="filter-pane" | ||||
|           .expanded=${this._expandedFilter === "ha-filter-devices"} | ||||
|           .narrow=${this.narrow} | ||||
|           @expanded-changed=${this._filterExpanded} | ||||
|         ></ha-filter-devices> | ||||
|         <ha-filter-labels | ||||
|           .hass=${this.hass} | ||||
|           .value=${this._filters["ha-filter-labels"]?.value} | ||||
| @@ -551,114 +370,6 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { | ||||
|           .narrow=${this.narrow} | ||||
|           @expanded-changed=${this._filterExpanded} | ||||
|         ></ha-filter-labels> | ||||
|         <ha-filter-categories | ||||
|           .hass=${this.hass} | ||||
|           scope="helpers" | ||||
|           .value=${this._filters["ha-filter-categories"]?.value} | ||||
|           @data-table-filter-changed=${this._filterChanged} | ||||
|           slot="filter-pane" | ||||
|           .expanded=${this._expandedFilter === "ha-filter-categories"} | ||||
|           .narrow=${this.narrow} | ||||
|           @expanded-changed=${this._filterExpanded} | ||||
|         ></ha-filter-categories> | ||||
|  | ||||
|         ${!this.narrow | ||||
|           ? html`<ha-button-menu-new slot="selection-bar"> | ||||
|                 <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> | ||||
|                 ${categoryItems} | ||||
|               </ha-button-menu-new> | ||||
|               ${labelsInOverflow | ||||
|                 ? nothing | ||||
|                 : html`<ha-button-menu-new 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-button-menu-new>`}` | ||||
|           : nothing} | ||||
|         ${this.narrow || labelsInOverflow | ||||
|           ? html` | ||||
|           <ha-button-menu-new 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=${"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-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-menu-item> | ||||
|                     <ha-menu slot="menu">${categoryItems}</ha-menu> | ||||
|                   </ha-sub-menu>` | ||||
|                 : nothing | ||||
|             } | ||||
|             ${ | ||||
|               this.narrow || this.hass.dockedSidebar === "docked" | ||||
|                 ? html` <ha-sub-menu> | ||||
|                     <ha-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-menu-item> | ||||
|                     <ha-menu slot="menu">${labelItems}</ha-menu> | ||||
|                   </ha-sub-menu>` | ||||
|                 : nothing | ||||
|             } | ||||
|           </ha-button-menu-new>` | ||||
|           : nothing} | ||||
|  | ||||
|         <ha-integration-overflow-menu | ||||
|           .hass=${this.hass} | ||||
| @@ -726,27 +437,6 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { | ||||
|               items.intersection(labelItems) | ||||
|             : new Set([...items].filter((x) => labelItems!.has(x))); | ||||
|       } | ||||
|       if (key === "ha-filter-categories" && filter.value?.length) { | ||||
|         const categoryItems: Set<string> = new Set(); | ||||
|         this._stateItems | ||||
|           .filter( | ||||
|             (stateItem) => | ||||
|               filter.value![0] === | ||||
|               this._entityReg.find( | ||||
|                 (reg) => reg.entity_id === stateItem.entity_id | ||||
|               )?.categories.helpers | ||||
|           ) | ||||
|           .forEach((stateItem) => categoryItems.add(stateItem.entity_id)); | ||||
|         if (!items) { | ||||
|           items = categoryItems; | ||||
|           continue; | ||||
|         } | ||||
|         items = | ||||
|           "intersection" in items | ||||
|             ? // @ts-ignore | ||||
|               items.intersection(categoryItems) | ||||
|             : new Set([...items].filter((x) => categoryItems!.has(x))); | ||||
|       } | ||||
|     } | ||||
|     this._filteredStateItems = items ? [...items] : undefined; | ||||
|   } | ||||
| @@ -756,73 +446,6 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { | ||||
|     this._applyFilters(); | ||||
|   } | ||||
|  | ||||
|   private _editCategory(helper: any) { | ||||
|     const entityReg = this._entityReg.find( | ||||
|       (reg) => reg.entity_id === helper.entity_id | ||||
|     ); | ||||
|     if (!entityReg) { | ||||
|       showAlertDialog(this, { | ||||
|         title: this.hass.localize( | ||||
|           "ui.panel.config.automation.picker.no_category_support" | ||||
|         ), | ||||
|         text: this.hass.localize( | ||||
|           "ui.panel.config.automation.picker.no_category_entity_reg" | ||||
|         ), | ||||
|       }); | ||||
|       return; | ||||
|     } | ||||
|     showAssignCategoryDialog(this, { | ||||
|       scope: "helpers", | ||||
|       entityReg, | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private async _handleBulkCategory(ev) { | ||||
|     const category = ev.currentTarget.value; | ||||
|     this._bulkAddCategory(category); | ||||
|   } | ||||
|  | ||||
|   private async _bulkAddCategory(category: string) { | ||||
|     const promises: Promise<UpdateEntityRegistryEntryResult>[] = []; | ||||
|     this._selected.forEach((entityId) => { | ||||
|       promises.push( | ||||
|         updateEntityRegistryEntry(this.hass, entityId, { | ||||
|           categories: { helpers: category }, | ||||
|         }) | ||||
|       ); | ||||
|     }); | ||||
|     await Promise.all(promises); | ||||
|   } | ||||
|  | ||||
|   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) => { | ||||
|       promises.push( | ||||
|         updateEntityRegistryEntry(this.hass, entityId, { | ||||
|           labels: | ||||
|             action === "add" | ||||
|               ? this.hass.entities[entityId].labels.concat(label) | ||||
|               : this.hass.entities[entityId].labels.filter( | ||||
|                   (lbl) => lbl !== label | ||||
|                 ), | ||||
|         }) | ||||
|       ); | ||||
|     }); | ||||
|     await Promise.all(promises); | ||||
|   } | ||||
|  | ||||
|   private _handleSelectionChanged( | ||||
|     ev: HASSDomEvent<SelectionChangedEvent> | ||||
|   ): void { | ||||
|     this._selected = ev.detail.value; | ||||
|   } | ||||
|  | ||||
|   protected firstUpdated(changedProps: PropertyValues) { | ||||
|     super.firstUpdated(changedProps); | ||||
|     if (this.route.path === "/add") { | ||||
| @@ -940,69 +563,20 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private _openSettings(helper: HelperItem) { | ||||
|     if (helper.entity) { | ||||
|       showMoreInfoDialog(this, { | ||||
|         entityId: helper.entity_id, | ||||
|         view: "settings", | ||||
|       }); | ||||
|     } else { | ||||
|       showOptionsFlowDialog(this, helper.configEntry!); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private _createHelper() { | ||||
|     showHelperDetailDialog(this, {}); | ||||
|   } | ||||
|  | ||||
|   private async _bulkCreateCategory() { | ||||
|     showCategoryRegistryDetailDialog(this, { | ||||
|       scope: "helpers", | ||||
|       createEntry: async (values) => { | ||||
|         const category = await createCategoryRegistryEntry( | ||||
|           this.hass, | ||||
|           "helpers", | ||||
|           values | ||||
|         ); | ||||
|         this._bulkAddCategory(category.category_id); | ||||
|         return category; | ||||
|       }, | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private _bulkCreateLabel() { | ||||
|     showLabelDetailDialog(this, { | ||||
|       createEntry: async (values) => { | ||||
|         const label = await createLabelRegistryEntry(this.hass, values); | ||||
|         this._bulkLabel(label.label_id, "add"); | ||||
|         return label; | ||||
|       }, | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   static get styles(): CSSResultGroup { | ||||
|     return [ | ||||
|       haStyle, | ||||
|       css` | ||||
|         :host { | ||||
|           display: block; | ||||
|         } | ||||
|         hass-tabs-subpage-data-table { | ||||
|           --data-table-row-height: 60px; | ||||
|         } | ||||
|         hass-tabs-subpage-data-table.narrow { | ||||
|           --data-table-row-height: 72px; | ||||
|         } | ||||
|         ha-assist-chip { | ||||
|           --ha-assist-chip-container-shape: 10px; | ||||
|         } | ||||
|         ha-button-menu-new ha-assist-chip { | ||||
|           --md-assist-chip-trailing-space: 8px; | ||||
|         } | ||||
|         ha-label { | ||||
|           --ha-label-background-color: var(--color, var(--grey-color)); | ||||
|           --ha-label-background-opacity: 0.5; | ||||
|         } | ||||
|       `, | ||||
|     ]; | ||||
|   } | ||||
|   | ||||
| @@ -1,15 +1,10 @@ | ||||
| import { consume } from "@lit-labs/context"; | ||||
| import { ResizeController } from "@lit-labs/observers/resize-controller"; | ||||
| import "@lrnwebcomponents/simple-tooltip/simple-tooltip"; | ||||
| import { | ||||
|   mdiChevronRight, | ||||
|   mdiCog, | ||||
|   mdiContentDuplicate, | ||||
|   mdiDelete, | ||||
|   mdiDotsVertical, | ||||
|   mdiHelpCircle, | ||||
|   mdiInformationOutline, | ||||
|   mdiMenuDown, | ||||
|   mdiPalette, | ||||
|   mdiPencilOff, | ||||
|   mdiPlay, | ||||
| @@ -29,7 +24,6 @@ import { | ||||
| } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { computeCssColor } from "../../../common/color/compute-color"; | ||||
| import { formatShortDateTime } from "../../../common/datetime/format_date_time"; | ||||
| import { relativeTime } from "../../../common/datetime/relative_time"; | ||||
| import { HASSDomEvent, fireEvent } from "../../../common/dom/fire_event"; | ||||
| @@ -39,7 +33,6 @@ import { LocalizeFunc } from "../../../common/translations/localize"; | ||||
| import { | ||||
|   DataTableColumnContainer, | ||||
|   RowClickedEvent, | ||||
|   SelectionChangedEvent, | ||||
| } from "../../../components/data-table/ha-data-table"; | ||||
| import "../../../components/data-table/ha-data-table-labels"; | ||||
| import "../../../components/ha-button"; | ||||
| @@ -51,26 +44,18 @@ 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-menu-item"; | ||||
| import "../../../components/ha-state-icon"; | ||||
| import "../../../components/ha-sub-menu"; | ||||
| import "../../../components/ha-svg-icon"; | ||||
| import { | ||||
|   CategoryRegistryEntry, | ||||
|   createCategoryRegistryEntry, | ||||
|   subscribeCategoryRegistry, | ||||
| } from "../../../data/category_registry"; | ||||
| import { fullEntitiesContext } from "../../../data/context"; | ||||
| import { isUnavailableState } from "../../../data/entity"; | ||||
| import { | ||||
|   EntityRegistryEntry, | ||||
|   UpdateEntityRegistryEntryResult, | ||||
|   updateEntityRegistryEntry, | ||||
| } from "../../../data/entity_registry"; | ||||
| import { EntityRegistryEntry } from "../../../data/entity_registry"; | ||||
| import { forwardHaptic } from "../../../data/haptics"; | ||||
| import { | ||||
|   LabelRegistryEntry, | ||||
|   createLabelRegistryEntry, | ||||
|   subscribeLabelRegistry, | ||||
| } from "../../../data/label_registry"; | ||||
| import { | ||||
| @@ -84,7 +69,6 @@ import { | ||||
|   showAlertDialog, | ||||
|   showConfirmationDialog, | ||||
| } from "../../../dialogs/generic/show-dialog-box"; | ||||
| import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info-dialog"; | ||||
| import "../../../layouts/hass-tabs-subpage-data-table"; | ||||
| import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; | ||||
| import { haStyle } from "../../../resources/styles"; | ||||
| @@ -92,13 +76,10 @@ import { HomeAssistant, Route } from "../../../types"; | ||||
| import { documentationUrl } from "../../../util/documentation-url"; | ||||
| import { showToast } from "../../../util/toast"; | ||||
| import { showAssignCategoryDialog } from "../category/show-dialog-assign-category"; | ||||
| import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail"; | ||||
| import { configSections } from "../ha-panel-config"; | ||||
| import { showLabelDetailDialog } from "../labels/show-dialog-label-detail"; | ||||
|  | ||||
| type SceneItem = SceneEntity & { | ||||
|   name: string; | ||||
|   area: string | undefined; | ||||
|   category: string | undefined; | ||||
|   labels: LabelRegistryEntry[]; | ||||
| }; | ||||
| @@ -117,8 +98,6 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) { | ||||
|  | ||||
|   @state() private _searchParms = new URLSearchParams(window.location.search); | ||||
|  | ||||
|   @state() private _selected: string[] = []; | ||||
|  | ||||
|   @state() private _activeFilters?: string[]; | ||||
|  | ||||
|   @state() private _filteredScenes?: string[] | null; | ||||
| @@ -140,15 +119,10 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) { | ||||
|   @consume({ context: fullEntitiesContext, subscribe: true }) | ||||
|   _entityReg!: EntityRegistryEntry[]; | ||||
|  | ||||
|   private _sizeController = new ResizeController(this, { | ||||
|     callback: (entries) => entries[0]?.contentRect.width, | ||||
|   }); | ||||
|  | ||||
|   private _scenes = memoizeOne( | ||||
|     ( | ||||
|       scenes: SceneEntity[], | ||||
|       entityReg: EntityRegistryEntry[], | ||||
|       areas: HomeAssistant["areas"], | ||||
|       categoryReg?: CategoryRegistryEntry[], | ||||
|       labelReg?: LabelRegistryEntry[], | ||||
|       filteredScenes?: string[] | null | ||||
| @@ -169,9 +143,6 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) { | ||||
|         return { | ||||
|           ...scene, | ||||
|           name: computeStateName(scene), | ||||
|           area: entityRegEntry?.area_id | ||||
|             ? areas[entityRegEntry?.area_id]?.name | ||||
|             : undefined, | ||||
|           category: category | ||||
|             ? categoryReg?.find((cat) => cat.category_id === category)?.name | ||||
|             : undefined, | ||||
| @@ -214,13 +185,6 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) { | ||||
|               : nothing} | ||||
|           `, | ||||
|         }, | ||||
|         area: { | ||||
|           title: localize("ui.panel.config.scene.picker.headers.area"), | ||||
|           hidden: true, | ||||
|           groupable: true, | ||||
|           filterable: true, | ||||
|           sortable: true, | ||||
|         }, | ||||
|         category: { | ||||
|           title: localize("ui.panel.config.scene.picker.headers.category"), | ||||
|           hidden: true, | ||||
| @@ -234,13 +198,14 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) { | ||||
|           filterable: true, | ||||
|           template: (scene) => scene.labels.map((lbl) => lbl.name).join(" "), | ||||
|         }, | ||||
|         state: { | ||||
|       }; | ||||
|       if (!narrow) { | ||||
|         columns.state = { | ||||
|           title: localize( | ||||
|             "ui.panel.config.scene.picker.headers.last_activated" | ||||
|           ), | ||||
|           sortable: true, | ||||
|           width: "30%", | ||||
|           hidden: narrow, | ||||
|           template: (scene) => { | ||||
|             const lastActivated = scene.state; | ||||
|             if (!lastActivated || isUnavailableState(lastActivated)) { | ||||
| @@ -255,87 +220,80 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) { | ||||
|                 : relativeTime(date, this.hass.locale)} | ||||
|             `; | ||||
|           }, | ||||
|         }, | ||||
|         only_editable: { | ||||
|           title: "", | ||||
|           width: "56px", | ||||
|           template: (scene) => | ||||
|             !scene.attributes.id | ||||
|               ? html` | ||||
|                   <simple-tooltip animation-delay="0" position="left"> | ||||
|                     ${this.hass.localize( | ||||
|                       "ui.panel.config.scene.picker.only_editable" | ||||
|                     )} | ||||
|                   </simple-tooltip> | ||||
|                   <ha-svg-icon | ||||
|                     .path=${mdiPencilOff} | ||||
|                     style="color: var(--secondary-text-color)" | ||||
|                   ></ha-svg-icon> | ||||
|                 ` | ||||
|               : "", | ||||
|         }, | ||||
|         actions: { | ||||
|           title: "", | ||||
|           width: "64px", | ||||
|           type: "overflow-menu", | ||||
|           template: (scene) => html` | ||||
|             <ha-icon-overflow-menu | ||||
|               .hass=${this.hass} | ||||
|               narrow | ||||
|               .items=${[ | ||||
|                 { | ||||
|                   path: mdiInformationOutline, | ||||
|                   label: this.hass.localize( | ||||
|                     "ui.panel.config.scene.picker.show_info" | ||||
|                   ), | ||||
|                   action: () => this._showInfo(scene), | ||||
|                 }, | ||||
|                 { | ||||
|                   path: mdiCog, | ||||
|                   label: this.hass.localize( | ||||
|                     "ui.panel.config.automation.picker.show_settings" | ||||
|                   ), | ||||
|                   action: () => this._openSettings(scene), | ||||
|                 }, | ||||
|                 { | ||||
|                   path: mdiPlay, | ||||
|                   label: this.hass.localize( | ||||
|                     "ui.panel.config.scene.picker.activate" | ||||
|                   ), | ||||
|                   action: () => this._activateScene(scene), | ||||
|                 }, | ||||
|                 { | ||||
|                   path: mdiTag, | ||||
|                   label: this.hass.localize( | ||||
|                     `ui.panel.config.scene.picker.${scene.category ? "edit_category" : "assign_category"}` | ||||
|                   ), | ||||
|                   action: () => this._editCategory(scene), | ||||
|                 }, | ||||
|                 { | ||||
|                   divider: true, | ||||
|                 }, | ||||
|                 { | ||||
|                   path: mdiContentDuplicate, | ||||
|                   label: this.hass.localize( | ||||
|                     "ui.panel.config.scene.picker.duplicate" | ||||
|                   ), | ||||
|                   action: () => this._duplicate(scene), | ||||
|                   disabled: !scene.attributes.id, | ||||
|                 }, | ||||
|                 { | ||||
|                   label: this.hass.localize( | ||||
|                     "ui.panel.config.scene.picker.delete" | ||||
|                   ), | ||||
|                   path: mdiDelete, | ||||
|                   action: () => this._deleteConfirm(scene), | ||||
|                   warning: scene.attributes.id, | ||||
|                   disabled: !scene.attributes.id, | ||||
|                 }, | ||||
|               ]} | ||||
|             > | ||||
|             </ha-icon-overflow-menu> | ||||
|           `, | ||||
|         }, | ||||
|         }; | ||||
|       } | ||||
|       columns.only_editable = { | ||||
|         title: "", | ||||
|         width: "56px", | ||||
|         template: (scene) => | ||||
|           !scene.attributes.id | ||||
|             ? html` | ||||
|                 <simple-tooltip animation-delay="0" position="left"> | ||||
|                   ${this.hass.localize( | ||||
|                     "ui.panel.config.scene.picker.only_editable" | ||||
|                   )} | ||||
|                 </simple-tooltip> | ||||
|                 <ha-svg-icon | ||||
|                   .path=${mdiPencilOff} | ||||
|                   style="color: var(--secondary-text-color)" | ||||
|                 ></ha-svg-icon> | ||||
|               ` | ||||
|             : "", | ||||
|       }; | ||||
|       columns.actions = { | ||||
|         title: "", | ||||
|         width: "64px", | ||||
|         type: "overflow-menu", | ||||
|         template: (scene) => html` | ||||
|           <ha-icon-overflow-menu | ||||
|             .hass=${this.hass} | ||||
|             narrow | ||||
|             .items=${[ | ||||
|               { | ||||
|                 path: mdiInformationOutline, | ||||
|                 label: this.hass.localize( | ||||
|                   "ui.panel.config.scene.picker.show_info" | ||||
|                 ), | ||||
|                 action: () => this._showInfo(scene), | ||||
|               }, | ||||
|               { | ||||
|                 path: mdiPlay, | ||||
|                 label: this.hass.localize( | ||||
|                   "ui.panel.config.scene.picker.activate" | ||||
|                 ), | ||||
|                 action: () => this._activateScene(scene), | ||||
|               }, | ||||
|               { | ||||
|                 path: mdiTag, | ||||
|                 label: this.hass.localize( | ||||
|                   `ui.panel.config.scene.picker.${scene.category ? "edit_category" : "assign_category"}` | ||||
|                 ), | ||||
|                 action: () => this._editCategory(scene), | ||||
|               }, | ||||
|               { | ||||
|                 divider: true, | ||||
|               }, | ||||
|               { | ||||
|                 path: mdiContentDuplicate, | ||||
|                 label: this.hass.localize( | ||||
|                   "ui.panel.config.scene.picker.duplicate" | ||||
|                 ), | ||||
|                 action: () => this._duplicate(scene), | ||||
|                 disabled: !scene.attributes.id, | ||||
|               }, | ||||
|               { | ||||
|                 label: this.hass.localize( | ||||
|                   "ui.panel.config.scene.picker.delete" | ||||
|                 ), | ||||
|                 path: mdiDelete, | ||||
|                 action: () => this._deleteConfirm(scene), | ||||
|                 warning: scene.attributes.id, | ||||
|                 disabled: !scene.attributes.id, | ||||
|               }, | ||||
|             ]} | ||||
|           > | ||||
|           </ha-icon-overflow-menu> | ||||
|         `, | ||||
|       }; | ||||
|  | ||||
|       return columns; | ||||
| @@ -361,70 +319,6 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) { | ||||
|   } | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|     const categoryItems = html`${this._categories?.map( | ||||
|         (category) => | ||||
|           html`<ha-menu-item | ||||
|             .value=${category.category_id} | ||||
|             @click=${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-menu-item>` | ||||
|       )} | ||||
|       <ha-menu-item .value=${null} @click=${this._handleBulkCategory}> | ||||
|         <div slot="headline"> | ||||
|           ${this.hass.localize( | ||||
|             "ui.panel.config.automation.picker.bulk_actions.no_category" | ||||
|           )} | ||||
|         </div> | ||||
|       </ha-menu-item> | ||||
|       <md-divider role="separator" tabindex="-1"></md-divider> | ||||
|       <ha-menu-item @click=${this._bulkCreateCategory}> | ||||
|         <div slot="headline"> | ||||
|           ${this.hass.localize("ui.panel.config.category.editor.add")} | ||||
|         </div> | ||||
|       </ha-menu-item>`; | ||||
|     const labelItems = html` ${this._labels?.map((label) => { | ||||
|         const color = label.color ? computeCssColor(label.color) : undefined; | ||||
|         const selected = this._selected.every((entityId) => | ||||
|           this.hass.entities[entityId]?.labels.includes(label.label_id) | ||||
|         ); | ||||
|         const partial = | ||||
|           !selected && | ||||
|           this._selected.some((entityId) => | ||||
|             this.hass.entities[entityId]?.labels.includes(label.label_id) | ||||
|           ); | ||||
|         return html`<ha-menu-item | ||||
|           .value=${label.label_id} | ||||
|           .action=${selected ? "remove" : "add"} | ||||
|           @click=${this._handleBulkLabel} | ||||
|           keep-open | ||||
|         > | ||||
|           <ha-checkbox | ||||
|             slot="start" | ||||
|             .checked=${selected} | ||||
|             .indeterminate=${partial} | ||||
|             reducedTouchTarget | ||||
|           ></ha-checkbox> | ||||
|           <ha-label style=${color ? `--color: ${color}` : ""}> | ||||
|             ${label.icon | ||||
|               ? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>` | ||||
|               : nothing} | ||||
|             ${label.name} | ||||
|           </ha-label> | ||||
|         </ha-menu-item>`; | ||||
|       })} | ||||
|       <md-divider role="separator" tabindex="-1"></md-divider> | ||||
|       <ha-menu-item @click=${this._bulkCreateLabel}> | ||||
|         <div slot="headline"> | ||||
|           ${this.hass.localize("ui.panel.config.labels.add_label")} | ||||
|         </div></ha-menu-item | ||||
|       >`; | ||||
|     const labelsInOverflow = | ||||
|       (this._sizeController.value && this._sizeController.value < 700) || | ||||
|       (!this._sizeController.value && this.hass.dockedSidebar === "docked"); | ||||
|     return html` | ||||
|       <hass-tabs-subpage-data-table | ||||
|         .hass=${this.hass} | ||||
| @@ -432,9 +326,6 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) { | ||||
|         back-path="/config" | ||||
|         .route=${this.route} | ||||
|         .tabs=${configSections.automations} | ||||
|         selectable | ||||
|         .selected=${this._selected.length} | ||||
|         @selection-changed=${this._handleSelectionChanged} | ||||
|         hasFilters | ||||
|         .filters=${Object.values(this._filters).filter( | ||||
|           (filter) => filter.value?.length | ||||
| @@ -445,7 +336,6 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) { | ||||
|         .data=${this._scenes( | ||||
|           this.scenes, | ||||
|           this._entityReg, | ||||
|           this.hass.areas, | ||||
|           this._categories, | ||||
|           this._labels, | ||||
|           this._filteredScenes | ||||
| @@ -517,103 +407,6 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) { | ||||
|           @expanded-changed=${this._filterExpanded} | ||||
|         ></ha-filter-categories> | ||||
|  | ||||
|         ${!this.narrow | ||||
|           ? html`<ha-button-menu-new slot="selection-bar"> | ||||
|                 <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> | ||||
|                 ${categoryItems} | ||||
|               </ha-button-menu-new> | ||||
|               ${labelsInOverflow | ||||
|                 ? nothing | ||||
|                 : html`<ha-button-menu-new 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-button-menu-new>`}` | ||||
|           : nothing} | ||||
|         ${this.narrow || labelsInOverflow | ||||
|           ? html` | ||||
|           <ha-button-menu-new 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=${"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-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-menu-item> | ||||
|                     <ha-menu slot="menu">${categoryItems}</ha-menu> | ||||
|                   </ha-sub-menu>` | ||||
|                 : nothing | ||||
|             } | ||||
|             ${ | ||||
|               this.narrow || this.hass.dockedSidebar === "docked" | ||||
|                 ? html` <ha-sub-menu> | ||||
|                     <ha-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-menu-item> | ||||
|                     <ha-menu slot="menu">${labelItems}</ha-menu> | ||||
|                   </ha-sub-menu>` | ||||
|                 : nothing | ||||
|             } | ||||
|           </ha-button-menu-new>` | ||||
|           : nothing} | ||||
|         ${!this.scenes.length | ||||
|           ? html`<div class="empty" slot="empty"> | ||||
|               <ha-svg-icon .path=${mdiPalette}></ha-svg-icon> | ||||
| @@ -760,12 +553,6 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) { | ||||
|     this._applyFilters(); | ||||
|   } | ||||
|  | ||||
|   private _handleSelectionChanged( | ||||
|     ev: HASSDomEvent<SelectionChangedEvent> | ||||
|   ): void { | ||||
|     this._selected = ev.detail.value; | ||||
|   } | ||||
|  | ||||
|   private _handleRowClicked(ev: HASSDomEvent<RowClickedEvent>) { | ||||
|     const scene = this.scenes.find((a) => a.entity_id === ev.detail.id); | ||||
|  | ||||
| @@ -774,46 +561,6 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private async _handleBulkCategory(ev) { | ||||
|     const category = ev.currentTarget.value; | ||||
|     this._bulkAddCategory(category); | ||||
|   } | ||||
|  | ||||
|   private async _bulkAddCategory(category: string) { | ||||
|     const promises: Promise<UpdateEntityRegistryEntryResult>[] = []; | ||||
|     this._selected.forEach((entityId) => { | ||||
|       promises.push( | ||||
|         updateEntityRegistryEntry(this.hass, entityId, { | ||||
|           categories: { scene: category }, | ||||
|         }) | ||||
|       ); | ||||
|     }); | ||||
|     await Promise.all(promises); | ||||
|   } | ||||
|  | ||||
|   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) => { | ||||
|       promises.push( | ||||
|         updateEntityRegistryEntry(this.hass, entityId, { | ||||
|           labels: | ||||
|             action === "add" | ||||
|               ? this.hass.entities[entityId].labels.concat(label) | ||||
|               : this.hass.entities[entityId].labels.filter( | ||||
|                   (lbl) => lbl !== label | ||||
|                 ), | ||||
|         }) | ||||
|       ); | ||||
|     }); | ||||
|     await Promise.all(promises); | ||||
|   } | ||||
|  | ||||
|   private _editCategory(scene: any) { | ||||
|     const entityReg = this._entityReg.find( | ||||
|       (reg) => reg.entity_id === scene.entity_id | ||||
| @@ -839,13 +586,6 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) { | ||||
|     fireEvent(this, "hass-more-info", { entityId: scene.entity_id }); | ||||
|   } | ||||
|  | ||||
|   private _openSettings(scene: SceneEntity) { | ||||
|     showMoreInfoDialog(this, { | ||||
|       entityId: scene.entity_id, | ||||
|       view: "settings", | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private _activateScene = async (scene: SceneEntity) => { | ||||
|     await activateScene(this.hass, scene.entity_id); | ||||
|     showToast(this, { | ||||
| @@ -909,38 +649,10 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) { | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private async _bulkCreateCategory() { | ||||
|     showCategoryRegistryDetailDialog(this, { | ||||
|       scope: "scene", | ||||
|       createEntry: async (values) => { | ||||
|         const category = await createCategoryRegistryEntry( | ||||
|           this.hass, | ||||
|           "scene", | ||||
|           values | ||||
|         ); | ||||
|         this._bulkAddCategory(category.category_id); | ||||
|         return category; | ||||
|       }, | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private _bulkCreateLabel() { | ||||
|     showLabelDetailDialog(this, { | ||||
|       createEntry: async (values) => { | ||||
|         const label = await createLabelRegistryEntry(this.hass, values); | ||||
|         this._bulkLabel(label.label_id, "add"); | ||||
|         return label; | ||||
|       }, | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   static get styles(): CSSResultGroup { | ||||
|     return [ | ||||
|       haStyle, | ||||
|       css` | ||||
|         :host { | ||||
|           display: block; | ||||
|         } | ||||
|         hass-tabs-subpage-data-table { | ||||
|           --data-table-row-height: 60px; | ||||
|         } | ||||
| @@ -952,16 +664,6 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) { | ||||
|           --mdc-icon-size: 80px; | ||||
|           max-width: 500px; | ||||
|         } | ||||
|         ha-assist-chip { | ||||
|           --ha-assist-chip-container-shape: 10px; | ||||
|         } | ||||
|         ha-button-menu-new ha-assist-chip { | ||||
|           --md-assist-chip-trailing-space: 8px; | ||||
|         } | ||||
|         ha-label { | ||||
|           --ha-label-background-color: var(--color, var(--grey-color)); | ||||
|           --ha-label-background-opacity: 0.5; | ||||
|         } | ||||
|       `, | ||||
|     ]; | ||||
|   } | ||||
|   | ||||
| @@ -1,14 +1,9 @@ | ||||
| import { consume } from "@lit-labs/context"; | ||||
| import { ResizeController } from "@lit-labs/observers/resize-controller"; | ||||
| import { | ||||
|   mdiChevronRight, | ||||
|   mdiCog, | ||||
|   mdiContentDuplicate, | ||||
|   mdiDelete, | ||||
|   mdiDotsVertical, | ||||
|   mdiHelpCircle, | ||||
|   mdiInformationOutline, | ||||
|   mdiMenuDown, | ||||
|   mdiPlay, | ||||
|   mdiPlus, | ||||
|   mdiScriptText, | ||||
| @@ -29,7 +24,6 @@ import { | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { styleMap } from "lit/directives/style-map"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { computeCssColor } from "../../../common/color/compute-color"; | ||||
| import { isComponentLoaded } from "../../../common/config/is_component_loaded"; | ||||
| import { formatShortDateTime } from "../../../common/datetime/format_date_time"; | ||||
| import { relativeTime } from "../../../common/datetime/relative_time"; | ||||
| @@ -40,7 +34,6 @@ import { LocalizeFunc } from "../../../common/translations/localize"; | ||||
| import { | ||||
|   DataTableColumnContainer, | ||||
|   RowClickedEvent, | ||||
|   SelectionChangedEvent, | ||||
| } from "../../../components/data-table/ha-data-table"; | ||||
| import "../../../components/data-table/ha-data-table-labels"; | ||||
| import "../../../components/ha-fab"; | ||||
| @@ -52,24 +45,16 @@ 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-menu-item"; | ||||
| import "../../../components/ha-sub-menu"; | ||||
| import "../../../components/ha-svg-icon"; | ||||
| import { | ||||
|   CategoryRegistryEntry, | ||||
|   createCategoryRegistryEntry, | ||||
|   subscribeCategoryRegistry, | ||||
| } from "../../../data/category_registry"; | ||||
| import { fullEntitiesContext } from "../../../data/context"; | ||||
| import { UNAVAILABLE } from "../../../data/entity"; | ||||
| import { | ||||
|   EntityRegistryEntry, | ||||
|   UpdateEntityRegistryEntryResult, | ||||
|   updateEntityRegistryEntry, | ||||
| } from "../../../data/entity_registry"; | ||||
| import { EntityRegistryEntry } from "../../../data/entity_registry"; | ||||
| import { | ||||
|   LabelRegistryEntry, | ||||
|   createLabelRegistryEntry, | ||||
|   subscribeLabelRegistry, | ||||
| } from "../../../data/label_registry"; | ||||
| import { | ||||
| @@ -85,7 +70,6 @@ import { | ||||
|   showAlertDialog, | ||||
|   showConfirmationDialog, | ||||
| } from "../../../dialogs/generic/show-dialog-box"; | ||||
| import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info-dialog"; | ||||
| import "../../../layouts/hass-tabs-subpage-data-table"; | ||||
| import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; | ||||
| import { haStyle } from "../../../resources/styles"; | ||||
| @@ -94,13 +78,10 @@ import { documentationUrl } from "../../../util/documentation-url"; | ||||
| import { showToast } from "../../../util/toast"; | ||||
| import { showNewAutomationDialog } from "../automation/show-dialog-new-automation"; | ||||
| import { showAssignCategoryDialog } from "../category/show-dialog-assign-category"; | ||||
| import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail"; | ||||
| import { configSections } from "../ha-panel-config"; | ||||
| import { showLabelDetailDialog } from "../labels/show-dialog-label-detail"; | ||||
|  | ||||
| type ScriptItem = ScriptEntity & { | ||||
|   name: string; | ||||
|   area: string | undefined; | ||||
|   category: string | undefined; | ||||
|   labels: LabelRegistryEntry[]; | ||||
| }; | ||||
| @@ -121,8 +102,6 @@ class HaScriptPicker extends SubscribeMixin(LitElement) { | ||||
|  | ||||
|   @state() private _searchParms = new URLSearchParams(window.location.search); | ||||
|  | ||||
|   @state() private _selected: string[] = []; | ||||
|  | ||||
|   @state() private _activeFilters?: string[]; | ||||
|  | ||||
|   @state() private _filteredScripts?: string[] | null; | ||||
| @@ -144,15 +123,10 @@ class HaScriptPicker extends SubscribeMixin(LitElement) { | ||||
|   @consume({ context: fullEntitiesContext, subscribe: true }) | ||||
|   _entityReg!: EntityRegistryEntry[]; | ||||
|  | ||||
|   private _sizeController = new ResizeController(this, { | ||||
|     callback: (entries) => entries[0]?.contentRect.width, | ||||
|   }); | ||||
|  | ||||
|   private _scripts = memoizeOne( | ||||
|     ( | ||||
|       scripts: ScriptEntity[], | ||||
|       entityReg: EntityRegistryEntry[], | ||||
|       areas: HomeAssistant["areas"], | ||||
|       categoryReg?: CategoryRegistryEntry[], | ||||
|       labelReg?: LabelRegistryEntry[], | ||||
|       filteredScripts?: string[] | null | ||||
| @@ -175,9 +149,6 @@ class HaScriptPicker extends SubscribeMixin(LitElement) { | ||||
|         return { | ||||
|           ...script, | ||||
|           name: computeStateName(script), | ||||
|           area: entityRegEntry?.area_id | ||||
|             ? areas[entityRegEntry?.area_id]?.name | ||||
|             : undefined, | ||||
|           last_triggered: script.attributes.last_triggered || undefined, | ||||
|           category: category | ||||
|             ? categoryReg?.find((cat) => cat.category_id === category)?.name | ||||
| @@ -243,13 +214,6 @@ class HaScriptPicker extends SubscribeMixin(LitElement) { | ||||
|             `; | ||||
|           }, | ||||
|         }, | ||||
|         area: { | ||||
|           title: localize("ui.panel.config.script.picker.headers.area"), | ||||
|           hidden: true, | ||||
|           groupable: true, | ||||
|           filterable: true, | ||||
|           sortable: true, | ||||
|         }, | ||||
|         category: { | ||||
|           title: localize("ui.panel.config.script.picker.headers.category"), | ||||
|           hidden: true, | ||||
| @@ -263,8 +227,9 @@ class HaScriptPicker extends SubscribeMixin(LitElement) { | ||||
|           filterable: true, | ||||
|           template: (script) => script.labels.map((lbl) => lbl.name).join(" "), | ||||
|         }, | ||||
|         last_triggered: { | ||||
|           hidden: narrow, | ||||
|       }; | ||||
|       if (!narrow) { | ||||
|         columns.last_triggered = { | ||||
|           sortable: true, | ||||
|           width: "40%", | ||||
|           title: localize("ui.card.automation.last_triggered"), | ||||
| @@ -284,74 +249,66 @@ class HaScriptPicker extends SubscribeMixin(LitElement) { | ||||
|                 : this.hass.localize("ui.components.relative_time.never")} | ||||
|             `; | ||||
|           }, | ||||
|         }, | ||||
|         actions: { | ||||
|           title: "", | ||||
|           width: "64px", | ||||
|           type: "overflow-menu", | ||||
|           template: (script) => html` | ||||
|             <ha-icon-overflow-menu | ||||
|               .hass=${this.hass} | ||||
|               narrow | ||||
|               .items=${[ | ||||
|                 { | ||||
|                   path: mdiInformationOutline, | ||||
|                   label: this.hass.localize( | ||||
|                     "ui.panel.config.script.picker.show_info" | ||||
|                   ), | ||||
|                   action: () => this._showInfo(script), | ||||
|                 }, | ||||
|                 { | ||||
|                   path: mdiCog, | ||||
|                   label: this.hass.localize( | ||||
|                     "ui.panel.config.automation.picker.show_settings" | ||||
|                   ), | ||||
|                   action: () => this._openSettings(script), | ||||
|                 }, | ||||
|                 { | ||||
|                   path: mdiTag, | ||||
|                   label: this.hass.localize( | ||||
|                     `ui.panel.config.script.picker.${script.category ? "edit_category" : "assign_category"}` | ||||
|                   ), | ||||
|                   action: () => this._editCategory(script), | ||||
|                 }, | ||||
|                 { | ||||
|                   path: mdiPlay, | ||||
|                   label: this.hass.localize( | ||||
|                     "ui.panel.config.script.picker.run" | ||||
|                   ), | ||||
|                   action: () => this._runScript(script), | ||||
|                 }, | ||||
|                 { | ||||
|                   path: mdiTransitConnection, | ||||
|                   label: this.hass.localize( | ||||
|                     "ui.panel.config.script.picker.show_trace" | ||||
|                   ), | ||||
|                   action: () => this._showTrace(script), | ||||
|                 }, | ||||
|                 { | ||||
|                   divider: true, | ||||
|                 }, | ||||
|                 { | ||||
|                   path: mdiContentDuplicate, | ||||
|                   label: this.hass.localize( | ||||
|                     "ui.panel.config.script.picker.duplicate" | ||||
|                   ), | ||||
|                   action: () => this._duplicate(script), | ||||
|                 }, | ||||
|                 { | ||||
|                   label: this.hass.localize( | ||||
|                     "ui.panel.config.script.picker.delete" | ||||
|                   ), | ||||
|                   path: mdiDelete, | ||||
|                   action: () => this._deleteConfirm(script), | ||||
|                   warning: true, | ||||
|                 }, | ||||
|               ]} | ||||
|             > | ||||
|             </ha-icon-overflow-menu> | ||||
|           `, | ||||
|         }, | ||||
|         }; | ||||
|       } | ||||
|  | ||||
|       columns.actions = { | ||||
|         title: "", | ||||
|         width: "64px", | ||||
|         type: "overflow-menu", | ||||
|         template: (script) => html` | ||||
|           <ha-icon-overflow-menu | ||||
|             .hass=${this.hass} | ||||
|             narrow | ||||
|             .items=${[ | ||||
|               { | ||||
|                 path: mdiInformationOutline, | ||||
|                 label: this.hass.localize( | ||||
|                   "ui.panel.config.script.picker.show_info" | ||||
|                 ), | ||||
|                 action: () => this._showInfo(script), | ||||
|               }, | ||||
|               { | ||||
|                 path: mdiTag, | ||||
|                 label: this.hass.localize( | ||||
|                   `ui.panel.config.script.picker.${script.category ? "edit_category" : "assign_category"}` | ||||
|                 ), | ||||
|                 action: () => this._editCategory(script), | ||||
|               }, | ||||
|               { | ||||
|                 path: mdiPlay, | ||||
|                 label: this.hass.localize("ui.panel.config.script.picker.run"), | ||||
|                 action: () => this._runScript(script), | ||||
|               }, | ||||
|               { | ||||
|                 path: mdiTransitConnection, | ||||
|                 label: this.hass.localize( | ||||
|                   "ui.panel.config.script.picker.show_trace" | ||||
|                 ), | ||||
|                 action: () => this._showTrace(script), | ||||
|               }, | ||||
|               { | ||||
|                 divider: true, | ||||
|               }, | ||||
|               { | ||||
|                 path: mdiContentDuplicate, | ||||
|                 label: this.hass.localize( | ||||
|                   "ui.panel.config.script.picker.duplicate" | ||||
|                 ), | ||||
|                 action: () => this._duplicate(script), | ||||
|               }, | ||||
|               { | ||||
|                 label: this.hass.localize( | ||||
|                   "ui.panel.config.script.picker.delete" | ||||
|                 ), | ||||
|                 path: mdiDelete, | ||||
|                 action: () => this._deleteConfirm(script), | ||||
|                 warning: true, | ||||
|               }, | ||||
|             ]} | ||||
|           > | ||||
|           </ha-icon-overflow-menu> | ||||
|         `, | ||||
|       }; | ||||
|  | ||||
|       return columns; | ||||
| @@ -374,69 +331,6 @@ class HaScriptPicker extends SubscribeMixin(LitElement) { | ||||
|   } | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|     const categoryItems = html`${this._categories?.map( | ||||
|         (category) => | ||||
|           html`<ha-menu-item | ||||
|             .value=${category.category_id} | ||||
|             @click=${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-menu-item>` | ||||
|       )} | ||||
|       <ha-menu-item .value=${null} @click=${this._handleBulkCategory}> | ||||
|         <div slot="headline"> | ||||
|           ${this.hass.localize( | ||||
|             "ui.panel.config.automation.picker.bulk_actions.no_category" | ||||
|           )} | ||||
|         </div> </ha-menu-item | ||||
|       ><md-divider role="separator" tabindex="-1"></md-divider> | ||||
|       <ha-menu-item @click=${this._bulkCreateCategory}> | ||||
|         <div slot="headline"> | ||||
|           ${this.hass.localize("ui.panel.config.category.editor.add")} | ||||
|         </div> | ||||
|       </ha-menu-item>`; | ||||
|     const labelItems = html`${this._labels?.map((label) => { | ||||
|         const color = label.color ? computeCssColor(label.color) : undefined; | ||||
|         const selected = this._selected.every((entityId) => | ||||
|           this.hass.entities[entityId]?.labels.includes(label.label_id) | ||||
|         ); | ||||
|         const partial = | ||||
|           !selected && | ||||
|           this._selected.some((entityId) => | ||||
|             this.hass.entities[entityId]?.labels.includes(label.label_id) | ||||
|           ); | ||||
|         return html`<ha-menu-item | ||||
|           .value=${label.label_id} | ||||
|           .action=${selected ? "remove" : "add"} | ||||
|           @click=${this._handleBulkLabel} | ||||
|           keep-open | ||||
|           reducedTouchTarget | ||||
|         > | ||||
|           <ha-checkbox | ||||
|             slot="start" | ||||
|             .checked=${selected} | ||||
|             .indeterminate=${partial} | ||||
|           ></ha-checkbox> | ||||
|           <ha-label style=${color ? `--color: ${color}` : ""}> | ||||
|             ${label.icon | ||||
|               ? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>` | ||||
|               : nothing} | ||||
|             ${label.name} | ||||
|           </ha-label> | ||||
|         </ha-menu-item>`; | ||||
|       })} | ||||
|       <md-divider role="separator" tabindex="-1"></md-divider> | ||||
|       <ha-menu-item @click=${this._bulkCreateLabel}> | ||||
|         <div slot="headline"> | ||||
|           ${this.hass.localize("ui.panel.config.labels.add_label")} | ||||
|         </div></ha-menu-item | ||||
|       >`; | ||||
|     const labelsInOverflow = | ||||
|       (this._sizeController.value && this._sizeController.value < 700) || | ||||
|       (!this._sizeController.value && this.hass.dockedSidebar === "docked"); | ||||
|     return html` | ||||
|       <hass-tabs-subpage-data-table | ||||
|         .hass=${this.hass} | ||||
| @@ -446,9 +340,6 @@ class HaScriptPicker extends SubscribeMixin(LitElement) { | ||||
|         .tabs=${configSections.automations} | ||||
|         hasFilters | ||||
|         initialGroupColumn="category" | ||||
|         selectable | ||||
|         .selected=${this._selected.length} | ||||
|         @selection-changed=${this._handleSelectionChanged} | ||||
|         .filters=${Object.values(this._filters).filter( | ||||
|           (filter) => filter.value?.length | ||||
|         ).length} | ||||
| @@ -460,7 +351,6 @@ class HaScriptPicker extends SubscribeMixin(LitElement) { | ||||
|         .data=${this._scripts( | ||||
|           this.scripts, | ||||
|           this._entityReg, | ||||
|           this.hass.areas, | ||||
|           this._categories, | ||||
|           this._labels, | ||||
|           this._filteredScripts | ||||
| @@ -542,104 +432,6 @@ class HaScriptPicker extends SubscribeMixin(LitElement) { | ||||
|           .narrow=${this.narrow} | ||||
|           @expanded-changed=${this._filterExpanded} | ||||
|         ></ha-filter-blueprints> | ||||
|  | ||||
|         ${!this.narrow | ||||
|           ? html`<ha-button-menu-new slot="selection-bar"> | ||||
|                 <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> | ||||
|                 ${categoryItems} | ||||
|               </ha-button-menu-new> | ||||
|               ${labelsInOverflow | ||||
|                 ? nothing | ||||
|                 : html`<ha-button-menu-new 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-button-menu-new>`}` | ||||
|           : nothing} | ||||
|         ${this.narrow || labelsInOverflow | ||||
|           ? html` | ||||
|           <ha-button-menu-new 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=${"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-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-menu-item> | ||||
|                     <ha-menu slot="menu">${categoryItems}</ha-menu> | ||||
|                   </ha-sub-menu>` | ||||
|                 : nothing | ||||
|             } | ||||
|             ${ | ||||
|               this.narrow || this.hass.dockedSidebar === "docked" | ||||
|                 ? html` <ha-sub-menu> | ||||
|                     <ha-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-menu-item> | ||||
|                     <ha-menu slot="menu">${labelItems}</ha-menu> | ||||
|                   </ha-sub-menu>` | ||||
|                 : nothing | ||||
|             } | ||||
|           </ha-button-menu-new>` | ||||
|           : nothing} | ||||
|         ${!this.scripts.length | ||||
|           ? html` <div class="empty" slot="empty"> | ||||
|               <ha-svg-icon .path=${mdiScriptText}></ha-svg-icon> | ||||
| @@ -837,52 +629,6 @@ class HaScriptPicker extends SubscribeMixin(LitElement) { | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private _handleSelectionChanged( | ||||
|     ev: HASSDomEvent<SelectionChangedEvent> | ||||
|   ): void { | ||||
|     this._selected = ev.detail.value; | ||||
|   } | ||||
|  | ||||
|   private async _handleBulkCategory(ev) { | ||||
|     const category = ev.currentTarget.value; | ||||
|     this._bulkAddCategory(category); | ||||
|   } | ||||
|  | ||||
|   private async _bulkAddCategory(category: string) { | ||||
|     const promises: Promise<UpdateEntityRegistryEntryResult>[] = []; | ||||
|     this._selected.forEach((entityId) => { | ||||
|       promises.push( | ||||
|         updateEntityRegistryEntry(this.hass, entityId, { | ||||
|           categories: { script: category }, | ||||
|         }) | ||||
|       ); | ||||
|     }); | ||||
|     await Promise.all(promises); | ||||
|   } | ||||
|  | ||||
|   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) => { | ||||
|       promises.push( | ||||
|         updateEntityRegistryEntry(this.hass, entityId, { | ||||
|           labels: | ||||
|             action === "add" | ||||
|               ? this.hass.entities[entityId].labels.concat(label) | ||||
|               : this.hass.entities[entityId].labels.filter( | ||||
|                   (lbl) => lbl !== label | ||||
|                 ), | ||||
|         }) | ||||
|       ); | ||||
|     }); | ||||
|     await Promise.all(promises); | ||||
|   } | ||||
|  | ||||
|   private _handleRowClicked(ev: HASSDomEvent<RowClickedEvent>) { | ||||
|     const entry = this.entityRegistry.find((e) => e.entity_id === ev.detail.id); | ||||
|     if (entry) { | ||||
| @@ -919,13 +665,6 @@ class HaScriptPicker extends SubscribeMixin(LitElement) { | ||||
|     fireEvent(this, "hass-more-info", { entityId: script.entity_id }); | ||||
|   } | ||||
|  | ||||
|   private _openSettings(script: any) { | ||||
|     showMoreInfoDialog(this, { | ||||
|       entityId: script.entity_id, | ||||
|       view: "settings", | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private _showTrace(script: any) { | ||||
|     const entry = this.entityRegistry.find( | ||||
|       (e) => e.entity_id === script.entity_id | ||||
| @@ -1025,38 +764,10 @@ class HaScriptPicker extends SubscribeMixin(LitElement) { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private async _bulkCreateCategory() { | ||||
|     showCategoryRegistryDetailDialog(this, { | ||||
|       scope: "script", | ||||
|       createEntry: async (values) => { | ||||
|         const category = await createCategoryRegistryEntry( | ||||
|           this.hass, | ||||
|           "script", | ||||
|           values | ||||
|         ); | ||||
|         this._bulkAddCategory(category.category_id); | ||||
|         return category; | ||||
|       }, | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private _bulkCreateLabel() { | ||||
|     showLabelDetailDialog(this, { | ||||
|       createEntry: async (values) => { | ||||
|         const label = await createLabelRegistryEntry(this.hass, values); | ||||
|         this._bulkLabel(label.label_id, "add"); | ||||
|         return label; | ||||
|       }, | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   static get styles(): CSSResultGroup { | ||||
|     return [ | ||||
|       haStyle, | ||||
|       css` | ||||
|         :host { | ||||
|           display: block; | ||||
|         } | ||||
|         hass-tabs-subpage-data-table { | ||||
|           --data-table-row-height: 60px; | ||||
|         } | ||||
| @@ -1071,16 +782,6 @@ class HaScriptPicker extends SubscribeMixin(LitElement) { | ||||
|           --mdc-icon-size: 80px; | ||||
|           max-width: 500px; | ||||
|         } | ||||
|         ha-assist-chip { | ||||
|           --ha-assist-chip-container-shape: 10px; | ||||
|         } | ||||
|         ha-button-menu-new ha-assist-chip { | ||||
|           --md-assist-chip-trailing-space: 8px; | ||||
|         } | ||||
|         ha-label { | ||||
|           --ha-label-background-color: var(--color, var(--grey-color)); | ||||
|           --ha-label-background-opacity: 0.5; | ||||
|         } | ||||
|       `, | ||||
|     ]; | ||||
|   } | ||||
|   | ||||
| @@ -35,7 +35,6 @@ import { ButtonsHeaderFooterConfig } from "../header-footer/types"; | ||||
| const HIDE_DOMAIN = new Set([ | ||||
|   "automation", | ||||
|   "configurator", | ||||
|   "conversation", | ||||
|   "device_tracker", | ||||
|   "geo_location", | ||||
|   "persistent_notification", | ||||
|   | ||||
| @@ -58,12 +58,18 @@ export interface AndCondition extends BaseCondition { | ||||
|  | ||||
| function getValueFromEntityId( | ||||
|   hass: HomeAssistant, | ||||
|   value: string | ||||
| ): string | undefined { | ||||
|   if (isValidEntityId(value) && hass.states[value]) { | ||||
|     return hass.states[value]?.state; | ||||
|   value: string | string[] | ||||
| ): string | string[] { | ||||
|   if ( | ||||
|     typeof value === "string" && | ||||
|     isValidEntityId(value) && | ||||
|     hass.states[value] | ||||
|   ) { | ||||
|     value = hass.states[value]?.state; | ||||
|   } else if (Array.isArray(value)) { | ||||
|     value = value.map((v) => getValueFromEntityId(hass, v) as string); | ||||
|   } | ||||
|   return undefined; | ||||
|   return value; | ||||
| } | ||||
|  | ||||
| function checkStateCondition( | ||||
| @@ -77,17 +83,8 @@ function checkStateCondition( | ||||
|   let value = condition.state ?? condition.state_not; | ||||
|  | ||||
|   // Handle entity_id, UI should be updated for conditionnal card (filters does not have UI for now) | ||||
|   if (Array.isArray(value)) { | ||||
|     const entityValues = value | ||||
|       .map((v) => getValueFromEntityId(hass, v)) | ||||
|       .filter((v): v is string => v !== undefined); | ||||
|     value = [...value, ...entityValues]; | ||||
|   } else if (typeof value === "string") { | ||||
|     const entityValue = getValueFromEntityId(hass, value); | ||||
|     value = [value]; | ||||
|     if (entityValue) { | ||||
|       value.push(entityValue); | ||||
|     } | ||||
|   if (Array.isArray(value) || typeof value === "string") { | ||||
|     value = getValueFromEntityId(hass, value); | ||||
|   } | ||||
|  | ||||
|   return condition.state != null | ||||
| @@ -106,10 +103,10 @@ function checkStateNumericCondition( | ||||
|  | ||||
|   // Handle entity_id, UI should be updated for conditionnal card (filters does not have UI for now) | ||||
|   if (typeof above === "string") { | ||||
|     above = getValueFromEntityId(hass, above) ?? above; | ||||
|     above = getValueFromEntityId(hass, above) as string; | ||||
|   } | ||||
|   if (typeof below === "string") { | ||||
|     below = getValueFromEntityId(hass, below) ?? below; | ||||
|     below = getValueFromEntityId(hass, below) as string; | ||||
|   } | ||||
|  | ||||
|   const numericState = Number(state); | ||||
|   | ||||
| @@ -172,14 +172,12 @@ class DialogDashboardStrategyEditor extends LitElement { | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   private _takeControl(ev) { | ||||
|     ev.stopPropagation(); | ||||
|   private _takeControl() { | ||||
|     this._params!.takeControl(); | ||||
|     this.closeDialog(); | ||||
|   } | ||||
|  | ||||
|   private _showRawConfigEditor(ev) { | ||||
|     ev.stopPropagation(); | ||||
|   private _showRawConfigEditor() { | ||||
|     this._params!.showRawConfigEditor(); | ||||
|     this.closeDialog(); | ||||
|   } | ||||
|   | ||||
| @@ -116,9 +116,6 @@ export const getMyRedirects = (hasSupervisor: boolean): Redirects => ({ | ||||
|   entities: { | ||||
|     redirect: "/config/entities", | ||||
|   }, | ||||
|   labels: { | ||||
|     redirect: "/config/labels", | ||||
|   }, | ||||
|   energy: { | ||||
|     component: "energy", | ||||
|     redirect: "/energy", | ||||
|   | ||||
| @@ -129,7 +129,7 @@ export class HaStateControlAlarmControlPanelModes extends LitElement { | ||||
|         max-height: max(320px, var(--modes-count, 1) * 80px); | ||||
|         min-height: max(200px, var(--modes-count, 1) * 80px); | ||||
|         --control-select-thickness: 130px; | ||||
|         --control-select-border-radius: 36px; | ||||
|         --control-select-border-radius: 48px; | ||||
|         --control-select-color: var(--primary-color); | ||||
|         --control-select-background: var(--disabled-color); | ||||
|         --control-select-background-opacity: 0.2; | ||||
|   | ||||
| @@ -75,7 +75,7 @@ export class HaStateControlCoverPosition extends LitElement { | ||||
|         max-height: 320px; | ||||
|         min-height: 200px; | ||||
|         --control-slider-thickness: 130px; | ||||
|         --control-slider-border-radius: 36px; | ||||
|         --control-slider-border-radius: 48px; | ||||
|         --control-slider-color: var(--primary-color); | ||||
|         --control-slider-background: var(--disabled-color); | ||||
|         --control-slider-background-opacity: 0.2; | ||||
|   | ||||
| @@ -112,7 +112,7 @@ export class HaStateControlInfoCoverTiltPosition extends LitElement { | ||||
|         max-height: 320px; | ||||
|         min-height: 200px; | ||||
|         --control-slider-thickness: 130px; | ||||
|         --control-slider-border-radius: 36px; | ||||
|         --control-slider-border-radius: 48px; | ||||
|         --control-slider-color: var(--primary-color); | ||||
|         --control-slider-background: var(--disabled-color); | ||||
|         --control-slider-background-opacity: 0.2; | ||||
|   | ||||
| @@ -142,7 +142,7 @@ export class HaStateControlCoverToggle extends LitElement { | ||||
|         max-height: 320px; | ||||
|         min-height: 200px; | ||||
|         --control-switch-thickness: 130px; | ||||
|         --control-switch-border-radius: 36px; | ||||
|         --control-switch-border-radius: 48px; | ||||
|         --control-switch-padding: 6px; | ||||
|         --mdc-icon-size: 24px; | ||||
|       } | ||||
| @@ -159,7 +159,7 @@ export class HaStateControlCoverToggle extends LitElement { | ||||
|       ha-control-button { | ||||
|         flex: 1; | ||||
|         width: 100%; | ||||
|         --control-button-border-radius: 36px; | ||||
|         --control-button-border-radius: 48px; | ||||
|         --mdc-icon-size: 24px; | ||||
|       } | ||||
|       ha-control-button.active { | ||||
|   | ||||
| @@ -142,7 +142,7 @@ export class HaStateControlFanSpeed extends LitElement { | ||||
|         max-height: 320px; | ||||
|         min-height: 200px; | ||||
|         --control-slider-thickness: 130px; | ||||
|         --control-slider-border-radius: 36px; | ||||
|         --control-slider-border-radius: 48px; | ||||
|         --control-slider-color: var(--primary-color); | ||||
|         --control-slider-background: var(--disabled-color); | ||||
|         --control-slider-background-opacity: 0.2; | ||||
| @@ -153,7 +153,7 @@ export class HaStateControlFanSpeed extends LitElement { | ||||
|         max-height: 320px; | ||||
|         min-height: 200px; | ||||
|         --control-select-thickness: 130px; | ||||
|         --control-select-border-radius: 36px; | ||||
|         --control-select-border-radius: 48px; | ||||
|         --control-select-color: var(--primary-color); | ||||
|         --control-select-background: var(--disabled-color); | ||||
|         --control-select-background-opacity: 0.2; | ||||
|   | ||||
| @@ -133,7 +133,7 @@ export class HaStateControlToggle extends LitElement { | ||||
|         max-height: 320px; | ||||
|         min-height: 200px; | ||||
|         --control-switch-thickness: 130px; | ||||
|         --control-switch-border-radius: 36px; | ||||
|         --control-switch-border-radius: 48px; | ||||
|         --control-switch-padding: 6px; | ||||
|         --mdc-icon-size: 24px; | ||||
|       } | ||||
| @@ -150,7 +150,7 @@ export class HaStateControlToggle extends LitElement { | ||||
|       ha-control-button { | ||||
|         flex: 1; | ||||
|         width: 100%; | ||||
|         --control-button-border-radius: 36px; | ||||
|         --control-button-border-radius: 48px; | ||||
|         --mdc-icon-size: 24px; | ||||
|       } | ||||
|       ha-control-button.active { | ||||
|   | ||||
| @@ -89,7 +89,7 @@ export class HaStateControlLightBrightness extends LitElement { | ||||
|         max-height: 320px; | ||||
|         min-height: 200px; | ||||
|         --control-slider-thickness: 130px; | ||||
|         --control-slider-border-radius: 36px; | ||||
|         --control-slider-border-radius: 48px; | ||||
|         --control-slider-color: var(--primary-color); | ||||
|         --control-slider-background: var(--disabled-color); | ||||
|         --control-slider-background-opacity: 0.2; | ||||
|   | ||||
| @@ -167,7 +167,7 @@ export class HaStateControlLockToggle extends LitElement { | ||||
|         max-height: 320px; | ||||
|         min-height: 200px; | ||||
|         --control-switch-thickness: 130px; | ||||
|         --control-switch-border-radius: 36px; | ||||
|         --control-switch-border-radius: 48px; | ||||
|         --control-switch-padding: 6px; | ||||
|         --mdc-icon-size: 24px; | ||||
|       } | ||||
| @@ -187,7 +187,7 @@ export class HaStateControlLockToggle extends LitElement { | ||||
|       ha-control-button { | ||||
|         flex: 1; | ||||
|         width: 100%; | ||||
|         --control-button-border-radius: 36px; | ||||
|         --control-button-border-radius: 48px; | ||||
|         --mdc-icon-size: 24px; | ||||
|       } | ||||
|       ha-control-button.active { | ||||
|   | ||||
| @@ -71,7 +71,7 @@ export class HaStateControlValvePosition extends LitElement { | ||||
|         max-height: 320px; | ||||
|         min-height: 200px; | ||||
|         --control-slider-thickness: 130px; | ||||
|         --control-slider-border-radius: 36px; | ||||
|         --control-slider-border-radius: 48px; | ||||
|         --control-slider-color: var(--primary-color); | ||||
|         --control-slider-background: var(--disabled-color); | ||||
|         --control-slider-background-opacity: 0.2; | ||||
|   | ||||
| @@ -142,7 +142,7 @@ export class HaStateControlValveToggle extends LitElement { | ||||
|         max-height: 320px; | ||||
|         min-height: 200px; | ||||
|         --control-switch-thickness: 130px; | ||||
|         --control-switch-border-radius: 36px; | ||||
|         --control-switch-border-radius: 48px; | ||||
|         --control-switch-padding: 6px; | ||||
|         --mdc-icon-size: 24px; | ||||
|       } | ||||
| @@ -159,7 +159,7 @@ export class HaStateControlValveToggle extends LitElement { | ||||
|       ha-control-button { | ||||
|         flex: 1; | ||||
|         width: 100%; | ||||
|         --control-button-border-radius: 36px; | ||||
|         --control-button-border-radius: 48px; | ||||
|         --mdc-icon-size: 24px; | ||||
|       } | ||||
|       ha-control-button.active { | ||||
|   | ||||
| @@ -1927,10 +1927,7 @@ | ||||
|             "aliases_section": "Aliases", | ||||
|             "no_aliases": "No configured aliases", | ||||
|             "configured_aliases": "{count} configured {count, plural,\n  one {alias}\n  other {aliases}\n}", | ||||
|             "aliases_description": "Aliases are alternative names used in voice assistants to refer to this floor.", | ||||
|             "areas_section": "Areas", | ||||
|             "areas_description": "Specify the areas that are on this floor.", | ||||
|             "add_area": "Add area" | ||||
|             "aliases_description": "Aliases are alternative names used in voice assistants to refer to this floor." | ||||
|           } | ||||
|         }, | ||||
|         "category": { | ||||
| @@ -1965,7 +1962,6 @@ | ||||
|             "color": "Color" | ||||
|           }, | ||||
|           "add_label": "Add label", | ||||
|           "manage_labels": "Manage labels", | ||||
|           "no_labels": "You don't have any labels", | ||||
|           "introduction": "Labels can help you organize your areas, devices and entities. They can be used to filter in the UI, or use them as a target in automations.", | ||||
|           "introduction2": "Go to the area, device or entity you want to add a label to, and click on the edit button to assign labels to them.", | ||||
| @@ -2266,8 +2262,7 @@ | ||||
|               "name": "Name", | ||||
|               "entity_id": "Entity ID", | ||||
|               "type": "Type", | ||||
|               "editable": "Editable", | ||||
|               "category": "Category" | ||||
|               "editable": "Editable" | ||||
|             }, | ||||
|             "create_helper": "Create helper", | ||||
|             "no_helpers": "Looks like you don't have any helpers yet!" | ||||
| @@ -2690,8 +2685,7 @@ | ||||
|               "trigger": "Trigger", | ||||
|               "actions": "Actions", | ||||
|               "state": "State", | ||||
|               "category": "Category", | ||||
|               "area": "Area" | ||||
|               "category": "Category" | ||||
|             }, | ||||
|             "bulk_action": "Action", | ||||
|             "bulk_actions": { | ||||
| @@ -3565,8 +3559,7 @@ | ||||
|             "headers": { | ||||
|               "name": "Name", | ||||
|               "state": "State", | ||||
|               "category": "Category", | ||||
|               "area": "Area" | ||||
|               "category": "Category" | ||||
|             }, | ||||
|             "edit_category": "[%key:ui::panel::config::automation::picker::edit_category%]", | ||||
|             "assign_category": "[%key:ui::panel::config::automation::picker::assign_category%]", | ||||
| @@ -3675,8 +3668,7 @@ | ||||
|               "state": "State", | ||||
|               "name": "Name", | ||||
|               "last_activated": "Last activated", | ||||
|               "category": "Category", | ||||
|               "area": "Area" | ||||
|               "category": "Category" | ||||
|             }, | ||||
|             "edit_category": "[%key:ui::panel::config::automation::picker::edit_category%]", | ||||
|             "assign_category": "[%key:ui::panel::config::automation::picker::assign_category%]", | ||||
| @@ -4060,9 +4052,6 @@ | ||||
|               "button": "Hide selected", | ||||
|               "confirm_title": "Do you want to hide {number} {number, plural,\n  one {entity}\n  other {entities}\n}?", | ||||
|               "confirm_text": "Hidden entities will not be shown on your dashboard. Their history is still tracked and you can still interact with them with services." | ||||
|             }, | ||||
|             "unhide_selected": { | ||||
|               "button": "Unhide selected" | ||||
|             } | ||||
|           } | ||||
|         }, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user