mirror of
				https://github.com/home-assistant/frontend.git
				synced 2025-10-30 22:19:55 +00:00 
			
		
		
		
	Compare commits
	
		
			23 Commits
		
	
	
		
			20240329.0
			...
			fix-menu-o
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 29a103e884 | ||
|   | 912d2cbd79 | ||
|   | 48ee3a34eb | ||
|   | 21263a1ffb | ||
|   | db59e138e9 | ||
|   | bc8012dcc9 | ||
|   | d8b43597a0 | ||
|   | 871949e760 | ||
|   | 4fb42d3545 | ||
|   | 2e58d6656c | ||
|   | a3024b38e9 | ||
|   | 85f2016371 | ||
|   | 1ce3347c2e | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 4f8415e8a7 | ||
|   | b202a36feb | ||
|   | 7e3e224746 | ||
|   | 503a7979d0 | ||
|   | f3ba6e7996 | ||
|   | f13dcb4139 | ||
|   | e8dc61ec36 | ||
|   | 88c59c5c13 | ||
|   | 85f80ff863 | ||
|   | d56abe6b72 | 
| @@ -4,4 +4,11 @@ import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; | |||||||
| export const mockAreaRegistry = ( | export const mockAreaRegistry = ( | ||||||
|   hass: MockHomeAssistant, |   hass: MockHomeAssistant, | ||||||
|   data: AreaRegistryEntry[] = [] |   data: AreaRegistryEntry[] = [] | ||||||
| ) => hass.mockWS("config/area_registry/list", () => data); | ) => { | ||||||
|  |   hass.mockWS("config/area_registry/list", () => data); | ||||||
|  |   const areas = {}; | ||||||
|  |   data.forEach((area) => { | ||||||
|  |     areas[area.area_id] = area; | ||||||
|  |   }); | ||||||
|  |   hass.updateHass({ areas }); | ||||||
|  | }; | ||||||
|   | |||||||
| @@ -4,4 +4,11 @@ import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; | |||||||
| export const mockDeviceRegistry = ( | export const mockDeviceRegistry = ( | ||||||
|   hass: MockHomeAssistant, |   hass: MockHomeAssistant, | ||||||
|   data: DeviceRegistryEntry[] = [] |   data: DeviceRegistryEntry[] = [] | ||||||
| ) => hass.mockWS("config/device_registry/list", () => data); | ) => { | ||||||
|  |   hass.mockWS("config/device_registry/list", () => data); | ||||||
|  |   const devices = {}; | ||||||
|  |   data.forEach((device) => { | ||||||
|  |     devices[device.id] = device; | ||||||
|  |   }); | ||||||
|  |   hass.updateHass({ devices }); | ||||||
|  | }; | ||||||
|   | |||||||
							
								
								
									
										7
									
								
								demo/src/stubs/floor_registry.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								demo/src/stubs/floor_registry.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | |||||||
|  | import { FloorRegistryEntry } from "../../../src/data/floor_registry"; | ||||||
|  | import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; | ||||||
|  |  | ||||||
|  | export const mockFloorRegistry = ( | ||||||
|  |   hass: MockHomeAssistant, | ||||||
|  |   data: FloorRegistryEntry[] = [] | ||||||
|  | ) => hass.mockWS("config/floor_registry/list", () => data); | ||||||
							
								
								
									
										7
									
								
								demo/src/stubs/label_registry.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								demo/src/stubs/label_registry.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | |||||||
|  | import { LabelRegistryEntry } from "../../../src/data/label_registry"; | ||||||
|  | import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; | ||||||
|  |  | ||||||
|  | export const mockLabelRegistry = ( | ||||||
|  |   hass: MockHomeAssistant, | ||||||
|  |   data: LabelRegistryEntry[] = [] | ||||||
|  | ) => hass.mockWS("config/label_registry/list", () => data); | ||||||
| @@ -17,6 +17,10 @@ import { provideHass } from "../../../../src/fake_data/provide_hass"; | |||||||
| import { ProvideHassElement } from "../../../../src/mixins/provide-hass-lit-mixin"; | import { ProvideHassElement } from "../../../../src/mixins/provide-hass-lit-mixin"; | ||||||
| import type { HomeAssistant } from "../../../../src/types"; | import type { HomeAssistant } from "../../../../src/types"; | ||||||
| import "../../components/demo-black-white-row"; | import "../../components/demo-black-white-row"; | ||||||
|  | import { FloorRegistryEntry } from "../../../../src/data/floor_registry"; | ||||||
|  | import { LabelRegistryEntry } from "../../../../src/data/label_registry"; | ||||||
|  | import { mockFloorRegistry } from "../../../../demo/src/stubs/floor_registry"; | ||||||
|  | import { mockLabelRegistry } from "../../../../demo/src/stubs/label_registry"; | ||||||
|  |  | ||||||
| const ENTITIES = [ | const ENTITIES = [ | ||||||
|   getEntity("alarm_control_panel", "alarm", "disarmed", { |   getEntity("alarm_control_panel", "alarm", "disarmed", { | ||||||
| @@ -100,7 +104,7 @@ const DEVICES = [ | |||||||
| const AREAS: AreaRegistryEntry[] = [ | const AREAS: AreaRegistryEntry[] = [ | ||||||
|   { |   { | ||||||
|     area_id: "backyard", |     area_id: "backyard", | ||||||
|     floor_id: null, |     floor_id: "ground", | ||||||
|     name: "Backyard", |     name: "Backyard", | ||||||
|     icon: null, |     icon: null, | ||||||
|     picture: null, |     picture: null, | ||||||
| @@ -109,7 +113,7 @@ const AREAS: AreaRegistryEntry[] = [ | |||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     area_id: "bedroom", |     area_id: "bedroom", | ||||||
|     floor_id: null, |     floor_id: "first", | ||||||
|     name: "Bedroom", |     name: "Bedroom", | ||||||
|     icon: "mdi:bed", |     icon: "mdi:bed", | ||||||
|     picture: null, |     picture: null, | ||||||
| @@ -118,7 +122,7 @@ const AREAS: AreaRegistryEntry[] = [ | |||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     area_id: "livingroom", |     area_id: "livingroom", | ||||||
|     floor_id: null, |     floor_id: "ground", | ||||||
|     name: "Livingroom", |     name: "Livingroom", | ||||||
|     icon: "mdi:sofa", |     icon: "mdi:sofa", | ||||||
|     picture: null, |     picture: null, | ||||||
| @@ -127,6 +131,45 @@ const AREAS: AreaRegistryEntry[] = [ | |||||||
|   }, |   }, | ||||||
| ]; | ]; | ||||||
|  |  | ||||||
|  | const FLOORS: FloorRegistryEntry[] = [ | ||||||
|  |   { | ||||||
|  |     floor_id: "ground", | ||||||
|  |     name: "Ground floor", | ||||||
|  |     level: 0, | ||||||
|  |     icon: null, | ||||||
|  |     aliases: [], | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     floor_id: "first", | ||||||
|  |     name: "First floor", | ||||||
|  |     level: 1, | ||||||
|  |     icon: "mdi:numeric-1", | ||||||
|  |     aliases: [], | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     floor_id: "second", | ||||||
|  |     name: "Second floor", | ||||||
|  |     level: 2, | ||||||
|  |     icon: "mdi:numeric-2", | ||||||
|  |     aliases: [], | ||||||
|  |   }, | ||||||
|  | ]; | ||||||
|  |  | ||||||
|  | const LABELS: LabelRegistryEntry[] = [ | ||||||
|  |   { | ||||||
|  |     label_id: "energy", | ||||||
|  |     name: "Energy", | ||||||
|  |     icon: null, | ||||||
|  |     color: "yellow", | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     label_id: "entertainment", | ||||||
|  |     name: "Entertainment", | ||||||
|  |     icon: "mdi:popcorn", | ||||||
|  |     color: "blue", | ||||||
|  |   }, | ||||||
|  | ]; | ||||||
|  |  | ||||||
| const SCHEMAS: { | const SCHEMAS: { | ||||||
|   name: string; |   name: string; | ||||||
|   input: Record<string, (BlueprintInput & { required?: boolean }) | null>; |   input: Record<string, (BlueprintInput & { required?: boolean }) | null>; | ||||||
| @@ -134,7 +177,12 @@ const SCHEMAS: { | |||||||
|   { |   { | ||||||
|     name: "One of each", |     name: "One of each", | ||||||
|     input: { |     input: { | ||||||
|  |       label: { name: "Label", selector: { label: {} } }, | ||||||
|  |       floor: { name: "Floor", selector: { floor: {} } }, | ||||||
|  |       area: { name: "Area", selector: { area: {} } }, | ||||||
|  |       device: { name: "Device", selector: { device: {} } }, | ||||||
|       entity: { name: "Entity", selector: { entity: {} } }, |       entity: { name: "Entity", selector: { entity: {} } }, | ||||||
|  |       target: { name: "Target", selector: { target: {} } }, | ||||||
|       state: { |       state: { | ||||||
|         name: "State", |         name: "State", | ||||||
|         selector: { state: { entity_id: "alarm_control_panel.alarm" } }, |         selector: { state: { entity_id: "alarm_control_panel.alarm" } }, | ||||||
| @@ -143,15 +191,12 @@ const SCHEMAS: { | |||||||
|         name: "Attribute", |         name: "Attribute", | ||||||
|         selector: { attribute: { entity_id: "" } }, |         selector: { attribute: { entity_id: "" } }, | ||||||
|       }, |       }, | ||||||
|       device: { name: "Device", selector: { device: {} } }, |  | ||||||
|       config_entry: { |       config_entry: { | ||||||
|         name: "Integration", |         name: "Integration", | ||||||
|         selector: { config_entry: {} }, |         selector: { config_entry: {} }, | ||||||
|       }, |       }, | ||||||
|       duration: { name: "Duration", selector: { duration: {} } }, |       duration: { name: "Duration", selector: { duration: {} } }, | ||||||
|       addon: { name: "Addon", selector: { addon: {} } }, |       addon: { name: "Addon", selector: { addon: {} } }, | ||||||
|       area: { name: "Area", selector: { area: {} } }, |  | ||||||
|       target: { name: "Target", selector: { target: {} } }, |  | ||||||
|       number_box: { |       number_box: { | ||||||
|         name: "Number Box", |         name: "Number Box", | ||||||
|         selector: { |         selector: { | ||||||
| @@ -300,6 +345,8 @@ const SCHEMAS: { | |||||||
|       entity: { name: "Entity", selector: { entity: { multiple: true } } }, |       entity: { name: "Entity", selector: { entity: { multiple: true } } }, | ||||||
|       device: { name: "Device", selector: { device: { multiple: true } } }, |       device: { name: "Device", selector: { device: { multiple: true } } }, | ||||||
|       area: { name: "Area", selector: { area: { multiple: true } } }, |       area: { name: "Area", selector: { area: { multiple: true } } }, | ||||||
|  |       floor: { name: "Floor", selector: { floor: { multiple: true } } }, | ||||||
|  |       label: { name: "Label", selector: { label: { multiple: true } } }, | ||||||
|       select: { |       select: { | ||||||
|         name: "Select Multiple", |         name: "Select Multiple", | ||||||
|         selector: { |         selector: { | ||||||
| @@ -356,6 +403,8 @@ class DemoHaSelector extends LitElement implements ProvideHassElement { | |||||||
|     mockDeviceRegistry(hass, DEVICES); |     mockDeviceRegistry(hass, DEVICES); | ||||||
|     mockConfigEntries(hass); |     mockConfigEntries(hass); | ||||||
|     mockAreaRegistry(hass, AREAS); |     mockAreaRegistry(hass, AREAS); | ||||||
|  |     mockFloorRegistry(hass, FLOORS); | ||||||
|  |     mockLabelRegistry(hass, LABELS); | ||||||
|     mockHassioSupervisor(hass); |     mockHassioSupervisor(hass); | ||||||
|     hass.mockWS("auth/sign_path", (params) => params); |     hass.mockWS("auth/sign_path", (params) => params); | ||||||
|     hass.mockWS("media_player/browse_media", this._browseMedia); |     hass.mockWS("media_player/browse_media", this._browseMedia); | ||||||
|   | |||||||
| @@ -33,7 +33,7 @@ | |||||||
|     "@codemirror/legacy-modes": "6.3.3", |     "@codemirror/legacy-modes": "6.3.3", | ||||||
|     "@codemirror/search": "6.5.6", |     "@codemirror/search": "6.5.6", | ||||||
|     "@codemirror/state": "6.4.1", |     "@codemirror/state": "6.4.1", | ||||||
|     "@codemirror/view": "6.26.0", |     "@codemirror/view": "6.26.1", | ||||||
|     "@egjs/hammerjs": "2.0.17", |     "@egjs/hammerjs": "2.0.17", | ||||||
|     "@formatjs/intl-datetimeformat": "6.12.3", |     "@formatjs/intl-datetimeformat": "6.12.3", | ||||||
|     "@formatjs/intl-displaynames": "6.6.6", |     "@formatjs/intl-displaynames": "6.6.6", | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" | |||||||
|  |  | ||||||
| [project] | [project] | ||||||
| name         = "home-assistant-frontend" | name         = "home-assistant-frontend" | ||||||
| version      = "20240329.0" | version      = "20240402.0" | ||||||
| license      = {text = "Apache-2.0"} | license      = {text = "Apache-2.0"} | ||||||
| description  = "The Home Assistant frontend" | description  = "The Home Assistant frontend" | ||||||
| readme       = "README.md" | readme       = "README.md" | ||||||
|   | |||||||
| @@ -22,14 +22,6 @@ export class HaAssistChip extends MdAssistChip { | |||||||
|         ); |         ); | ||||||
|         --md-assist-chip-outline-color: var(--outline-color); |         --md-assist-chip-outline-color: var(--outline-color); | ||||||
|         --md-assist-chip-label-text-weight: 400; |         --md-assist-chip-label-text-weight: 400; | ||||||
|         --ha-assist-chip-filled-container-color: rgba( |  | ||||||
|           var(--rgb-primary-text-color), |  | ||||||
|           0.15 |  | ||||||
|         ); |  | ||||||
|         --ha-assist-chip-active-container-color: rgba( |  | ||||||
|           var(--rgb-primary-color), |  | ||||||
|           0.15 |  | ||||||
|         ); |  | ||||||
|       } |       } | ||||||
|       /** Material 3 doesn't have a filled chip, so we have to make our own **/ |       /** Material 3 doesn't have a filled chip, so we have to make our own **/ | ||||||
|       .filled { |       .filled { | ||||||
| @@ -52,10 +44,17 @@ export class HaAssistChip extends MdAssistChip { | |||||||
|         margin-inline-end: unset; |         margin-inline-end: unset; | ||||||
|         margin-inline-start: var(--_icon-label-space); |         margin-inline-start: var(--_icon-label-space); | ||||||
|       } |       } | ||||||
|  |       ::before { | ||||||
|  |         background: var(--ha-assist-chip-container-color); | ||||||
|  |         opacity: var(--ha-assist-chip-container-opacity); | ||||||
|  |       } | ||||||
|       :where(.active)::before { |       :where(.active)::before { | ||||||
|         background: var(--ha-assist-chip-active-container-color); |         background: var(--ha-assist-chip-active-container-color); | ||||||
|         opacity: var(--ha-assist-chip-active-container-opacity); |         opacity: var(--ha-assist-chip-active-container-opacity); | ||||||
|       } |       } | ||||||
|  |       .label { | ||||||
|  |         font-family: Roboto, sans-serif; | ||||||
|  |       } | ||||||
|     `, |     `, | ||||||
|   ]; |   ]; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -5,20 +5,22 @@ import { LabelRegistryEntry } from "../../data/label_registry"; | |||||||
| import { computeCssColor } from "../../common/color/compute-color"; | import { computeCssColor } from "../../common/color/compute-color"; | ||||||
| import { fireEvent } from "../../common/dom/fire_event"; | import { fireEvent } from "../../common/dom/fire_event"; | ||||||
| import "../ha-label"; | import "../ha-label"; | ||||||
|  | import { stringCompare } from "../../common/string/compare"; | ||||||
|  |  | ||||||
| @customElement("ha-data-table-labels") | @customElement("ha-data-table-labels") | ||||||
| class HaDataTableLabels extends LitElement { | class HaDataTableLabels extends LitElement { | ||||||
|   @property({ attribute: false }) public labels!: LabelRegistryEntry[]; |   @property({ attribute: false }) public labels!: LabelRegistryEntry[]; | ||||||
|  |  | ||||||
|   protected render(): TemplateResult { |   protected render(): TemplateResult { | ||||||
|  |     const labels = this.labels.sort((a, b) => stringCompare(a.name, b.name)); | ||||||
|     return html` |     return html` | ||||||
|       <ha-chip-set> |       <ha-chip-set> | ||||||
|         ${repeat( |         ${repeat( | ||||||
|           this.labels.slice(0, 2), |           labels.slice(0, 2), | ||||||
|           (label) => label.label_id, |           (label) => label.label_id, | ||||||
|           (label) => this._renderLabel(label, true) |           (label) => this._renderLabel(label, true) | ||||||
|         )} |         )} | ||||||
|         ${this.labels.length > 2 |         ${labels.length > 2 | ||||||
|           ? html`<ha-button-menu |           ? html`<ha-button-menu | ||||||
|               absolute |               absolute | ||||||
|               role="button" |               role="button" | ||||||
| @@ -27,10 +29,10 @@ class HaDataTableLabels extends LitElement { | |||||||
|               @closed=${this._handleIconOverflowMenuClosed} |               @closed=${this._handleIconOverflowMenuClosed} | ||||||
|             > |             > | ||||||
|               <ha-label slot="trigger" class="plus" dense> |               <ha-label slot="trigger" class="plus" dense> | ||||||
|                 +${this.labels.length - 2} |                 +${labels.length - 2} | ||||||
|               </ha-label> |               </ha-label> | ||||||
|               ${repeat( |               ${repeat( | ||||||
|                 this.labels.slice(2), |                 labels.slice(2), | ||||||
|                 (label) => label.label_id, |                 (label) => label.label_id, | ||||||
|                 (label) => html` |                 (label) => html` | ||||||
|                   <ha-list-item @click=${this._labelClicked} .item=${label}> |                   <ha-list-item @click=${this._labelClicked} .item=${label}> | ||||||
|   | |||||||
| @@ -181,6 +181,13 @@ export class HaDataTable extends LitElement { | |||||||
|     this._checkedRowsChanged(); |     this._checkedRowsChanged(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   public selectAll(): void { | ||||||
|  |     this._checkedRows = this._filteredData | ||||||
|  |       .filter((data) => data.selectable !== false) | ||||||
|  |       .map((data) => data[this.id]); | ||||||
|  |     this._checkedRowsChanged(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   public connectedCallback() { |   public connectedCallback() { | ||||||
|     super.connectedCallback(); |     super.connectedCallback(); | ||||||
|     if (this._items.length) { |     if (this._items.length) { | ||||||
| @@ -386,7 +393,7 @@ export class HaDataTable extends LitElement { | |||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _keyFunction = (row: DataTableRowData) => row[this.id] || row; |   private _keyFunction = (row: DataTableRowData) => row?.[this.id] || row; | ||||||
|  |  | ||||||
|   private _renderRow = (row: DataTableRowData, index: number) => { |   private _renderRow = (row: DataTableRowData, index: number) => { | ||||||
|     // not sure how this happens... |     // not sure how this happens... | ||||||
| @@ -593,10 +600,7 @@ export class HaDataTable extends LitElement { | |||||||
|   private _handleHeaderRowCheckboxClick(ev: Event) { |   private _handleHeaderRowCheckboxClick(ev: Event) { | ||||||
|     const checkbox = ev.target as HaCheckbox; |     const checkbox = ev.target as HaCheckbox; | ||||||
|     if (checkbox.checked) { |     if (checkbox.checked) { | ||||||
|       this._checkedRows = this._filteredData |       this.selectAll(); | ||||||
|         .filter((data) => data.selectable !== false) |  | ||||||
|         .map((data) => data[this.id]); |  | ||||||
|       this._checkedRowsChanged(); |  | ||||||
|     } else { |     } else { | ||||||
|       this._checkedRows = []; |       this._checkedRows = []; | ||||||
|       this._checkedRowsChanged(); |       this._checkedRowsChanged(); | ||||||
| @@ -623,9 +627,13 @@ export class HaDataTable extends LitElement { | |||||||
|       ev |       ev | ||||||
|         .composedPath() |         .composedPath() | ||||||
|         .find((el) => |         .find((el) => | ||||||
|           ["ha-checkbox", "mwc-button", "ha-button", "ha-assist-chip"].includes( |           [ | ||||||
|             (el as HTMLElement).localName |             "ha-checkbox", | ||||||
|           ) |             "mwc-button", | ||||||
|  |             "ha-button", | ||||||
|  |             "ha-icon-button", | ||||||
|  |             "ha-assist-chip", | ||||||
|  |           ].includes((el as HTMLElement).localName) | ||||||
|         ) |         ) | ||||||
|     ) { |     ) { | ||||||
|       return; |       return; | ||||||
|   | |||||||
| @@ -21,10 +21,8 @@ import { | |||||||
|   getDeviceEntityDisplayLookup, |   getDeviceEntityDisplayLookup, | ||||||
| } from "../data/device_registry"; | } from "../data/device_registry"; | ||||||
| import { EntityRegistryDisplayEntry } from "../data/entity_registry"; | import { EntityRegistryDisplayEntry } from "../data/entity_registry"; | ||||||
| import { | import { showAlertDialog } from "../dialogs/generic/show-dialog-box"; | ||||||
|   showAlertDialog, | import { showAreaRegistryDetailDialog } from "../panels/config/areas/show-dialog-area-registry-detail"; | ||||||
|   showPromptDialog, |  | ||||||
| } from "../dialogs/generic/show-dialog-box"; |  | ||||||
| import { HomeAssistant, ValueChangedEvent } from "../types"; | import { HomeAssistant, ValueChangedEvent } from "../types"; | ||||||
| import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker"; | import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker"; | ||||||
| import "./ha-combo-box"; | import "./ha-combo-box"; | ||||||
| @@ -38,7 +36,7 @@ type ScorableAreaRegistryEntry = ScorableTextItem & AreaRegistryEntry; | |||||||
| const rowRenderer: ComboBoxLitRenderer<AreaRegistryEntry> = (item) => | const rowRenderer: ComboBoxLitRenderer<AreaRegistryEntry> = (item) => | ||||||
|   html`<ha-list-item |   html`<ha-list-item | ||||||
|     graphic="icon" |     graphic="icon" | ||||||
|     class=${classMap({ "add-new": item.area_id === "add_new" })} |     class=${classMap({ "add-new": item.area_id === ADD_NEW_ID })} | ||||||
|   > |   > | ||||||
|     ${item.icon |     ${item.icon | ||||||
|       ? html`<ha-icon slot="graphic" .icon=${item.icon}></ha-icon>` |       ? html`<ha-icon slot="graphic" .icon=${item.icon}></ha-icon>` | ||||||
| @@ -46,6 +44,10 @@ const rowRenderer: ComboBoxLitRenderer<AreaRegistryEntry> = (item) => | |||||||
|     ${item.name} |     ${item.name} | ||||||
|   </ha-list-item>`; |   </ha-list-item>`; | ||||||
|  |  | ||||||
|  | const ADD_NEW_ID = "___ADD_NEW___"; | ||||||
|  | const NO_ITEMS_ID = "___NO_ITEMS___"; | ||||||
|  | const ADD_NEW_SUGGESTION_ID = "___ADD_NEW_SUGGESTION___"; | ||||||
|  |  | ||||||
| @customElement("ha-area-picker") | @customElement("ha-area-picker") | ||||||
| export class HaAreaPicker extends LitElement { | export class HaAreaPicker extends LitElement { | ||||||
|   @property({ attribute: false }) public hass!: HomeAssistant; |   @property({ attribute: false }) public hass!: HomeAssistant; | ||||||
| @@ -134,20 +136,6 @@ export class HaAreaPicker extends LitElement { | |||||||
|       noAdd: this["noAdd"], |       noAdd: this["noAdd"], | ||||||
|       excludeAreas: this["excludeAreas"] |       excludeAreas: this["excludeAreas"] | ||||||
|     ): AreaRegistryEntry[] => { |     ): AreaRegistryEntry[] => { | ||||||
|       if (!areas.length) { |  | ||||||
|         return [ |  | ||||||
|           { |  | ||||||
|             area_id: "no_areas", |  | ||||||
|             floor_id: null, |  | ||||||
|             name: this.hass.localize("ui.components.area-picker.no_areas"), |  | ||||||
|             picture: null, |  | ||||||
|             icon: null, |  | ||||||
|             aliases: [], |  | ||||||
|             labels: [], |  | ||||||
|           }, |  | ||||||
|         ]; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       let deviceEntityLookup: DeviceEntityDisplayLookup = {}; |       let deviceEntityLookup: DeviceEntityDisplayLookup = {}; | ||||||
|       let inputDevices: DeviceRegistryEntry[] | undefined; |       let inputDevices: DeviceRegistryEntry[] | undefined; | ||||||
|       let inputEntities: EntityRegistryDisplayEntry[] | undefined; |       let inputEntities: EntityRegistryDisplayEntry[] | undefined; | ||||||
| @@ -284,9 +272,9 @@ export class HaAreaPicker extends LitElement { | |||||||
|       if (!outputAreas.length) { |       if (!outputAreas.length) { | ||||||
|         outputAreas = [ |         outputAreas = [ | ||||||
|           { |           { | ||||||
|             area_id: "no_areas", |             area_id: NO_ITEMS_ID, | ||||||
|             floor_id: null, |             floor_id: null, | ||||||
|             name: this.hass.localize("ui.components.area-picker.no_match"), |             name: this.hass.localize("ui.components.area-picker.no_areas"), | ||||||
|             picture: null, |             picture: null, | ||||||
|             icon: null, |             icon: null, | ||||||
|             aliases: [], |             aliases: [], | ||||||
| @@ -300,7 +288,7 @@ export class HaAreaPicker extends LitElement { | |||||||
|         : [ |         : [ | ||||||
|             ...outputAreas, |             ...outputAreas, | ||||||
|             { |             { | ||||||
|               area_id: "add_new", |               area_id: ADD_NEW_ID, | ||||||
|               floor_id: null, |               floor_id: null, | ||||||
|               name: this.hass.localize("ui.components.area-picker.add_new"), |               name: this.hass.localize("ui.components.area-picker.add_new"), | ||||||
|               picture: null, |               picture: null, | ||||||
| @@ -374,20 +362,40 @@ export class HaAreaPicker extends LitElement { | |||||||
|  |  | ||||||
|     const filteredItems = fuzzyFilterSort<ScorableAreaRegistryEntry>( |     const filteredItems = fuzzyFilterSort<ScorableAreaRegistryEntry>( | ||||||
|       filterString, |       filterString, | ||||||
|       target.items || [] |       target.items?.filter( | ||||||
|  |         (item) => ![NO_ITEMS_ID, ADD_NEW_ID].includes(item.label_id) | ||||||
|  |       ) || [] | ||||||
|     ); |     ); | ||||||
|     if (!this.noAdd && filteredItems?.length === 0) { |     if (filteredItems.length === 0) { | ||||||
|       this._suggestion = filterString; |       if (!this.noAdd) { | ||||||
|       this.comboBox.filteredItems = [ |         this.comboBox.filteredItems = [ | ||||||
|         { |           { | ||||||
|           area_id: "add_new_suggestion", |             area_id: NO_ITEMS_ID, | ||||||
|           name: this.hass.localize( |             floor_id: null, | ||||||
|             "ui.components.area-picker.add_new_sugestion", |             name: this.hass.localize("ui.components.area-picker.no_match"), | ||||||
|             { name: this._suggestion } |             icon: null, | ||||||
|           ), |             picture: null, | ||||||
|           picture: null, |             labels: [], | ||||||
|         }, |             aliases: [], | ||||||
|       ]; |           }, | ||||||
|  |         ] as AreaRegistryEntry[]; | ||||||
|  |       } else { | ||||||
|  |         this._suggestion = filterString; | ||||||
|  |         this.comboBox.filteredItems = [ | ||||||
|  |           { | ||||||
|  |             area_id: ADD_NEW_SUGGESTION_ID, | ||||||
|  |             floor_id: null, | ||||||
|  |             name: this.hass.localize( | ||||||
|  |               "ui.components.area-picker.add_new_sugestion", | ||||||
|  |               { name: this._suggestion } | ||||||
|  |             ), | ||||||
|  |             icon: "mdi:plus", | ||||||
|  |             picture: null, | ||||||
|  |             labels: [], | ||||||
|  |             aliases: [], | ||||||
|  |           }, | ||||||
|  |         ] as AreaRegistryEntry[]; | ||||||
|  |       } | ||||||
|     } else { |     } else { | ||||||
|       this.comboBox.filteredItems = filteredItems; |       this.comboBox.filteredItems = filteredItems; | ||||||
|     } |     } | ||||||
| @@ -405,11 +413,13 @@ export class HaAreaPicker extends LitElement { | |||||||
|     ev.stopPropagation(); |     ev.stopPropagation(); | ||||||
|     let newValue = ev.detail.value; |     let newValue = ev.detail.value; | ||||||
|  |  | ||||||
|     if (newValue === "no_areas") { |     if (newValue === NO_ITEMS_ID) { | ||||||
|       newValue = ""; |       newValue = ""; | ||||||
|  |       this.comboBox.setInputValue(""); | ||||||
|  |       return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (!["add_new_suggestion", "add_new"].includes(newValue)) { |     if (![ADD_NEW_SUGGESTION_ID, ADD_NEW_ID].includes(newValue)) { | ||||||
|       if (newValue !== this._value) { |       if (newValue !== this._value) { | ||||||
|         this._setValue(newValue); |         this._setValue(newValue); | ||||||
|       } |       } | ||||||
| @@ -417,25 +427,12 @@ export class HaAreaPicker extends LitElement { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     (ev.target as any).value = this._value; |     (ev.target as any).value = this._value; | ||||||
|     showPromptDialog(this, { |  | ||||||
|       title: this.hass.localize("ui.components.area-picker.add_dialog.title"), |     showAreaRegistryDetailDialog(this, { | ||||||
|       text: this.hass.localize("ui.components.area-picker.add_dialog.text"), |       suggestedName: newValue === ADD_NEW_SUGGESTION_ID ? this._suggestion : "", | ||||||
|       confirmText: this.hass.localize( |       createEntry: async (values) => { | ||||||
|         "ui.components.area-picker.add_dialog.add" |  | ||||||
|       ), |  | ||||||
|       inputLabel: this.hass.localize( |  | ||||||
|         "ui.components.area-picker.add_dialog.name" |  | ||||||
|       ), |  | ||||||
|       defaultValue: |  | ||||||
|         newValue === "add_new_suggestion" ? this._suggestion : undefined, |  | ||||||
|       confirm: async (name) => { |  | ||||||
|         if (!name) { |  | ||||||
|           return; |  | ||||||
|         } |  | ||||||
|         try { |         try { | ||||||
|           const area = await createAreaRegistryEntry(this.hass, { |           const area = await createAreaRegistryEntry(this.hass, values); | ||||||
|             name, |  | ||||||
|           }); |  | ||||||
|           const areas = [...Object.values(this.hass.areas), area]; |           const areas = [...Object.values(this.hass.areas), area]; | ||||||
|           this.comboBox.filteredItems = this._getAreas( |           this.comboBox.filteredItems = this._getAreas( | ||||||
|             areas, |             areas, | ||||||
| @@ -455,18 +452,16 @@ export class HaAreaPicker extends LitElement { | |||||||
|         } catch (err: any) { |         } catch (err: any) { | ||||||
|           showAlertDialog(this, { |           showAlertDialog(this, { | ||||||
|             title: this.hass.localize( |             title: this.hass.localize( | ||||||
|               "ui.components.area-picker.add_dialog.failed_create_area" |               "ui.components.area-picker.failed_create_area" | ||||||
|             ), |             ), | ||||||
|             text: err.message, |             text: err.message, | ||||||
|           }); |           }); | ||||||
|         } |         } | ||||||
|       }, |       }, | ||||||
|       cancel: () => { |  | ||||||
|         this._setValue(undefined); |  | ||||||
|         this._suggestion = undefined; |  | ||||||
|         this.comboBox.setInputValue(""); |  | ||||||
|       }, |  | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|  |     this._suggestion = undefined; | ||||||
|  |     this.comboBox.setInputValue(""); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _setValue(value?: string) { |   private _setValue(value?: string) { | ||||||
|   | |||||||
							
								
								
									
										89
									
								
								src/components/ha-button-menu-new.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								src/components/ha-button-menu-new.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,89 @@ | |||||||
|  | import { Button } from "@material/mwc-button"; | ||||||
|  | import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||||
|  | import { customElement, property, query } from "lit/decorators"; | ||||||
|  | import { FOCUS_TARGET } from "../dialogs/make-dialog-manager"; | ||||||
|  | import type { HaIconButton } from "./ha-icon-button"; | ||||||
|  | import "./ha-menu"; | ||||||
|  | import type { HaMenu } from "./ha-menu"; | ||||||
|  |  | ||||||
|  | @customElement("ha-button-menu-new") | ||||||
|  | export class HaButtonMenuNew extends LitElement { | ||||||
|  |   protected readonly [FOCUS_TARGET]; | ||||||
|  |  | ||||||
|  |   @property({ type: Boolean }) public disabled = false; | ||||||
|  |  | ||||||
|  |   @property() public positioning?: "fixed" | "absolute" | "popover"; | ||||||
|  |  | ||||||
|  |   @property({ type: Boolean, attribute: "has-overflow" }) public hasOverflow = | ||||||
|  |     false; | ||||||
|  |  | ||||||
|  |   @query("ha-menu", true) private _menu!: HaMenu; | ||||||
|  |  | ||||||
|  |   public get items() { | ||||||
|  |     return this._menu.items; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public override focus() { | ||||||
|  |     if (this._menu.open) { | ||||||
|  |       this._menu.focus(); | ||||||
|  |     } else { | ||||||
|  |       this._triggerButton?.focus(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   protected render(): TemplateResult { | ||||||
|  |     return html` | ||||||
|  |       <div @click=${this._handleClick}> | ||||||
|  |         <slot name="trigger" @slotchange=${this._setTriggerAria}></slot> | ||||||
|  |       </div> | ||||||
|  |       <ha-menu | ||||||
|  |         .positioning=${this.positioning} | ||||||
|  |         .hasOverflow=${this.hasOverflow} | ||||||
|  |       > | ||||||
|  |         <slot></slot> | ||||||
|  |       </ha-menu> | ||||||
|  |     `; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _handleClick(): void { | ||||||
|  |     if (this.disabled) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     this._menu.anchorElement = this; | ||||||
|  |     if (this._menu.open) { | ||||||
|  |       this._menu.close(); | ||||||
|  |     } else { | ||||||
|  |       this._menu.show(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private get _triggerButton() { | ||||||
|  |     return this.querySelector( | ||||||
|  |       'ha-icon-button[slot="trigger"], mwc-button[slot="trigger"], ha-assist-chip[slot="trigger"]' | ||||||
|  |     ) as HaIconButton | Button | null; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _setTriggerAria() { | ||||||
|  |     if (this._triggerButton) { | ||||||
|  |       this._triggerButton.ariaHasPopup = "menu"; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   static get styles(): CSSResultGroup { | ||||||
|  |     return css` | ||||||
|  |       :host { | ||||||
|  |         display: inline-block; | ||||||
|  |         position: relative; | ||||||
|  |       } | ||||||
|  |       ::slotted([disabled]) { | ||||||
|  |         color: var(--disabled-text-color); | ||||||
|  |       } | ||||||
|  |     `; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | declare global { | ||||||
|  |   interface HTMLElementTagNameMap { | ||||||
|  |     "ha-button-menu-new": HaButtonMenuNew; | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -6,6 +6,7 @@ import { computeCssColor, THEME_COLORS } from "../common/color/compute-color"; | |||||||
| import { fireEvent } from "../common/dom/fire_event"; | import { fireEvent } from "../common/dom/fire_event"; | ||||||
| import { stopPropagation } from "../common/dom/stop_propagation"; | import { stopPropagation } from "../common/dom/stop_propagation"; | ||||||
| import "./ha-select"; | import "./ha-select"; | ||||||
|  | import "./ha-list-item"; | ||||||
| import { HomeAssistant } from "../types"; | import { HomeAssistant } from "../types"; | ||||||
| import { LocalizeKeys } from "../common/translations/localize"; | import { LocalizeKeys } from "../common/translations/localize"; | ||||||
|  |  | ||||||
| @@ -53,18 +54,18 @@ export class HaColorPicker extends LitElement { | |||||||
|             ` |             ` | ||||||
|           : nothing} |           : nothing} | ||||||
|         ${this.defaultColor |         ${this.defaultColor | ||||||
|           ? html` <mwc-list-item value="default"> |           ? html` <ha-list-item value="default"> | ||||||
|               ${this.hass.localize(`ui.components.color-picker.default_color`)} |               ${this.hass.localize(`ui.components.color-picker.default_color`)} | ||||||
|             </mwc-list-item>` |             </ha-list-item>` | ||||||
|           : nothing} |           : nothing} | ||||||
|         ${Array.from(THEME_COLORS).map( |         ${Array.from(THEME_COLORS).map( | ||||||
|           (color) => html` |           (color) => html` | ||||||
|             <mwc-list-item .value=${color} graphic="icon"> |             <ha-list-item .value=${color} graphic="icon"> | ||||||
|               ${this.hass.localize( |               ${this.hass.localize( | ||||||
|                 `ui.components.color-picker.colors.${color}` as LocalizeKeys |                 `ui.components.color-picker.colors.${color}` as LocalizeKeys | ||||||
|               ) || color} |               ) || color} | ||||||
|               <span slot="graphic">${this.renderColorCircle(color)}</span> |               <span slot="graphic">${this.renderColorCircle(color)}</span> | ||||||
|             </mwc-list-item> |             </ha-list-item> | ||||||
|           ` |           ` | ||||||
|         )} |         )} | ||||||
|       </ha-select> |       </ha-select> | ||||||
|   | |||||||
| @@ -50,7 +50,7 @@ export class HaFilterBlueprints extends LitElement { | |||||||
|                     ? nothing |                     ? nothing | ||||||
|                     : html`<ha-check-list-item |                     : html`<ha-check-list-item | ||||||
|                         .value=${id} |                         .value=${id} | ||||||
|                         .selected=${this.value?.includes(id)} |                         .selected=${(this.value || []).includes(id)} | ||||||
|                       > |                       > | ||||||
|                         ${blueprint.metadata.name || id} |                         ${blueprint.metadata.name || id} | ||||||
|                       </ha-check-list-item>` |                       </ha-check-list-item>` | ||||||
|   | |||||||
| @@ -57,7 +57,8 @@ export class HaFilterDevices extends LitElement { | |||||||
|         ${this._shouldRender |         ${this._shouldRender | ||||||
|           ? html`<mwc-list class="ha-scrollbar"> |           ? html`<mwc-list class="ha-scrollbar"> | ||||||
|               <lit-virtualizer |               <lit-virtualizer | ||||||
|                 .items=${this._devices(this.hass.devices)} |                 .items=${this._devices(this.hass.devices, this.value)} | ||||||
|  |                 .keyFunction=${this._keyFunction} | ||||||
|                 .renderItem=${this._renderItem} |                 .renderItem=${this._renderItem} | ||||||
|                 @click=${this._handleItemClick} |                 @click=${this._handleItemClick} | ||||||
|               > |               > | ||||||
| @@ -68,6 +69,8 @@ export class HaFilterDevices extends LitElement { | |||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   private _keyFunction = (device) => device?.id; | ||||||
|  |  | ||||||
|   private _renderItem = (device) => |   private _renderItem = (device) => | ||||||
|     html`<ha-check-list-item |     html`<ha-check-list-item | ||||||
|       .value=${device.id} |       .value=${device.id} | ||||||
| @@ -109,7 +112,7 @@ export class HaFilterDevices extends LitElement { | |||||||
|     this.expanded = ev.detail.expanded; |     this.expanded = ev.detail.expanded; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _devices = memoizeOne((devices: HomeAssistant["devices"]) => { |   private _devices = memoizeOne((devices: HomeAssistant["devices"], _value) => { | ||||||
|     const values = Object.values(devices); |     const values = Object.values(devices); | ||||||
|     return values.sort((a, b) => |     return values.sort((a, b) => | ||||||
|       stringCompare( |       stringCompare( | ||||||
|   | |||||||
| @@ -59,7 +59,12 @@ export class HaFilterEntities extends LitElement { | |||||||
|           ? html` |           ? html` | ||||||
|               <mwc-list class="ha-scrollbar"> |               <mwc-list class="ha-scrollbar"> | ||||||
|                 <lit-virtualizer |                 <lit-virtualizer | ||||||
|                   .items=${this._entities(this.hass.states, this.type)} |                   .items=${this._entities( | ||||||
|  |                     this.hass.states, | ||||||
|  |                     this.type, | ||||||
|  |                     this.value | ||||||
|  |                   )} | ||||||
|  |                   .keyFunction=${this._keyFunction} | ||||||
|                   .renderItem=${this._renderItem} |                   .renderItem=${this._renderItem} | ||||||
|                   @click=${this._handleItemClick} |                   @click=${this._handleItemClick} | ||||||
|                 > |                 > | ||||||
| @@ -81,6 +86,8 @@ export class HaFilterEntities extends LitElement { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   private _keyFunction = (entity) => entity?.entity_id; | ||||||
|  |  | ||||||
|   private _renderItem = (entity) => |   private _renderItem = (entity) => | ||||||
|     html`<ha-check-list-item |     html`<ha-check-list-item | ||||||
|       .value=${entity.entity_id} |       .value=${entity.entity_id} | ||||||
| @@ -119,7 +126,7 @@ export class HaFilterEntities extends LitElement { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _entities = memoizeOne( |   private _entities = memoizeOne( | ||||||
|     (states: HomeAssistant["states"], type: this["type"]) => { |     (states: HomeAssistant["states"], type: this["type"], _value) => { | ||||||
|       const values = Object.values(states); |       const values = Object.values(states); | ||||||
|       return values |       return values | ||||||
|         .filter( |         .filter( | ||||||
|   | |||||||
| @@ -1,15 +1,16 @@ | |||||||
| import { SelectedDetail } from "@material/mwc-list"; | import { SelectedDetail } from "@material/mwc-list"; | ||||||
| import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; | import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; | ||||||
| import { customElement, property, state } from "lit/decorators"; | import { customElement, property, state } from "lit/decorators"; | ||||||
|  | import { repeat } from "lit/directives/repeat"; | ||||||
| import memoizeOne from "memoize-one"; | import memoizeOne from "memoize-one"; | ||||||
| import { fireEvent } from "../common/dom/fire_event"; | import { fireEvent } from "../common/dom/fire_event"; | ||||||
| import { stringCompare } from "../common/string/compare"; | import { stringCompare } from "../common/string/compare"; | ||||||
| import { haStyleScrollbar } from "../resources/styles"; |  | ||||||
| import type { HomeAssistant } from "../types"; |  | ||||||
| import { | import { | ||||||
|   fetchIntegrationManifests, |   fetchIntegrationManifests, | ||||||
|   IntegrationManifest, |   IntegrationManifest, | ||||||
| } from "../data/integration"; | } from "../data/integration"; | ||||||
|  | import { haStyleScrollbar } from "../resources/styles"; | ||||||
|  | import type { HomeAssistant } from "../types"; | ||||||
| import "./ha-domain-icon"; | import "./ha-domain-icon"; | ||||||
|  |  | ||||||
| @customElement("ha-filter-integrations") | @customElement("ha-filter-integrations") | ||||||
| @@ -47,11 +48,15 @@ export class HaFilterIntegrations extends LitElement { | |||||||
|                 multi |                 multi | ||||||
|                 class="ha-scrollbar" |                 class="ha-scrollbar" | ||||||
|               > |               > | ||||||
|                 ${this._integrations(this._manifests).map( |                 ${repeat( | ||||||
|  |                   this._integrations(this._manifests, this.value), | ||||||
|  |                   (i) => i.domain, | ||||||
|                   (integration) => |                   (integration) => | ||||||
|                     html`<ha-check-list-item |                     html`<ha-check-list-item | ||||||
|                       .value=${integration.domain} |                       .value=${integration.domain} | ||||||
|                       .selected=${this.value?.includes(integration.domain)} |                       .selected=${(this.value || []).includes( | ||||||
|  |                         integration.domain | ||||||
|  |                       )} | ||||||
|                       graphic="icon" |                       graphic="icon" | ||||||
|                     > |                     > | ||||||
|                       <ha-domain-icon |                       <ha-domain-icon | ||||||
| @@ -92,26 +97,27 @@ export class HaFilterIntegrations extends LitElement { | |||||||
|     this._manifests = await fetchIntegrationManifests(this.hass); |     this._manifests = await fetchIntegrationManifests(this.hass); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _integrations = memoizeOne((manifest: IntegrationManifest[]) => |   private _integrations = memoizeOne( | ||||||
|     manifest |     (manifest: IntegrationManifest[], _value) => | ||||||
|       .filter( |       manifest | ||||||
|         (mnfst) => |         .filter( | ||||||
|           !mnfst.integration_type || |           (mnfst) => | ||||||
|           !["entity", "system", "hardware"].includes(mnfst.integration_type) |             !mnfst.integration_type || | ||||||
|       ) |             !["entity", "system", "hardware"].includes(mnfst.integration_type) | ||||||
|       .sort((a, b) => |         ) | ||||||
|         stringCompare( |         .sort((a, b) => | ||||||
|           a.name || a.domain, |           stringCompare( | ||||||
|           b.name || b.domain, |             a.name || a.domain, | ||||||
|           this.hass.locale.language |             b.name || b.domain, | ||||||
|  |             this.hass.locale.language | ||||||
|  |           ) | ||||||
|         ) |         ) | ||||||
|       ) |  | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
|   private async _integrationsSelected( |   private async _integrationsSelected( | ||||||
|     ev: CustomEvent<SelectedDetail<Set<number>>> |     ev: CustomEvent<SelectedDetail<Set<number>>> | ||||||
|   ) { |   ) { | ||||||
|     const integrations = this._integrations(this._manifests!); |     const integrations = this._integrations(this._manifests!, this.value); | ||||||
|  |  | ||||||
|     if (!ev.detail.index.size) { |     if (!ev.detail.index.size) { | ||||||
|       fireEvent(this, "data-table-filter-changed", { |       fireEvent(this, "data-table-filter-changed", { | ||||||
|   | |||||||
| @@ -1,9 +1,10 @@ | |||||||
| import { SelectedDetail } from "@material/mwc-list"; | import { SelectedDetail } from "@material/mwc-list"; | ||||||
| import "@material/mwc-menu/mwc-menu-surface"; | import "@material/mwc-menu/mwc-menu-surface"; | ||||||
|  | import { mdiPlus } from "@mdi/js"; | ||||||
| import { UnsubscribeFunc } from "home-assistant-js-websocket"; | import { UnsubscribeFunc } from "home-assistant-js-websocket"; | ||||||
| import { CSSResultGroup, LitElement, css, html, nothing } from "lit"; | import { CSSResultGroup, LitElement, css, html, nothing } from "lit"; | ||||||
| import { customElement, property, state } from "lit/decorators"; | import { customElement, property, state } from "lit/decorators"; | ||||||
| import { mdiPlus } from "@mdi/js"; | import { repeat } from "lit/directives/repeat"; | ||||||
| import { computeCssColor } from "../common/color/compute-color"; | import { computeCssColor } from "../common/color/compute-color"; | ||||||
| import { fireEvent } from "../common/dom/fire_event"; | import { fireEvent } from "../common/dom/fire_event"; | ||||||
| import { | import { | ||||||
| @@ -12,13 +13,13 @@ import { | |||||||
|   subscribeLabelRegistry, |   subscribeLabelRegistry, | ||||||
| } from "../data/label_registry"; | } from "../data/label_registry"; | ||||||
| import { SubscribeMixin } from "../mixins/subscribe-mixin"; | import { SubscribeMixin } from "../mixins/subscribe-mixin"; | ||||||
|  | import { showLabelDetailDialog } from "../panels/config/labels/show-dialog-label-detail"; | ||||||
| import { haStyleScrollbar } from "../resources/styles"; | import { haStyleScrollbar } from "../resources/styles"; | ||||||
| import type { HomeAssistant } from "../types"; | import type { HomeAssistant } from "../types"; | ||||||
| import "./ha-check-list-item"; | import "./ha-check-list-item"; | ||||||
| import "./ha-expansion-panel"; | import "./ha-expansion-panel"; | ||||||
| import "./ha-icon"; | import "./ha-icon"; | ||||||
| import "./ha-label"; | import "./ha-label"; | ||||||
| import { showLabelDetailDialog } from "../panels/config/labels/show-dialog-label-detail"; |  | ||||||
|  |  | ||||||
| @customElement("ha-filter-labels") | @customElement("ha-filter-labels") | ||||||
| export class HaFilterLabels extends SubscribeMixin(LitElement) { | export class HaFilterLabels extends SubscribeMixin(LitElement) { | ||||||
| @@ -63,26 +64,30 @@ export class HaFilterLabels extends SubscribeMixin(LitElement) { | |||||||
|                 class="ha-scrollbar" |                 class="ha-scrollbar" | ||||||
|                 multi |                 multi | ||||||
|               > |               > | ||||||
|                 ${this._labels.map((label) => { |                 ${repeat( | ||||||
|                   const color = label.color |                   this._labels, | ||||||
|                     ? computeCssColor(label.color) |                   (label) => label.label_id, | ||||||
|                     : undefined; |                   (label) => { | ||||||
|                   return html`<ha-check-list-item |                     const color = label.color | ||||||
|                     .value=${label.label_id} |                       ? computeCssColor(label.color) | ||||||
|                     .selected=${this.value?.includes(label.label_id)} |                       : undefined; | ||||||
|                     hasMeta |                     return html`<ha-check-list-item | ||||||
|                   > |                       .value=${label.label_id} | ||||||
|                     <ha-label style=${color ? `--color: ${color}` : ""}> |                       .selected=${(this.value || []).includes(label.label_id)} | ||||||
|                       ${label.icon |                       hasMeta | ||||||
|                         ? html`<ha-icon |                     > | ||||||
|                             slot="icon" |                       <ha-label style=${color ? `--color: ${color}` : ""}> | ||||||
|                             .icon=${label.icon} |                         ${label.icon | ||||||
|                           ></ha-icon>` |                           ? html`<ha-icon | ||||||
|                         : nothing} |                               slot="icon" | ||||||
|                       ${label.name} |                               .icon=${label.icon} | ||||||
|                     </ha-label> |                             ></ha-icon>` | ||||||
|                   </ha-check-list-item>`; |                           : nothing} | ||||||
|                 })} |                         ${label.name} | ||||||
|  |                       </ha-label> | ||||||
|  |                     </ha-check-list-item>`; | ||||||
|  |                   } | ||||||
|  |                 )} | ||||||
|               </mwc-list> |               </mwc-list> | ||||||
|             ` |             ` | ||||||
|           : nothing} |           : nothing} | ||||||
|   | |||||||
| @@ -23,11 +23,9 @@ import { | |||||||
|   getFloorAreaLookup, |   getFloorAreaLookup, | ||||||
|   subscribeFloorRegistry, |   subscribeFloorRegistry, | ||||||
| } from "../data/floor_registry"; | } from "../data/floor_registry"; | ||||||
| import { | import { showAlertDialog } from "../dialogs/generic/show-dialog-box"; | ||||||
|   showAlertDialog, |  | ||||||
|   showPromptDialog, |  | ||||||
| } from "../dialogs/generic/show-dialog-box"; |  | ||||||
| import { SubscribeMixin } from "../mixins/subscribe-mixin"; | import { SubscribeMixin } from "../mixins/subscribe-mixin"; | ||||||
|  | import { showFloorRegistryDetailDialog } from "../panels/config/areas/show-dialog-floor-registry-detail"; | ||||||
| import { HomeAssistant, ValueChangedEvent } from "../types"; | import { HomeAssistant, ValueChangedEvent } from "../types"; | ||||||
| import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker"; | import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker"; | ||||||
| import "./ha-combo-box"; | import "./ha-combo-box"; | ||||||
| @@ -274,7 +272,7 @@ export class HaFloorPicker extends SubscribeMixin(LitElement) { | |||||||
|       if (areaIds) { |       if (areaIds) { | ||||||
|         const floorAreaLookup = getFloorAreaLookup(areas); |         const floorAreaLookup = getFloorAreaLookup(areas); | ||||||
|         outputFloors = outputFloors.filter((floor) => |         outputFloors = outputFloors.filter((floor) => | ||||||
|           floorAreaLookup[floor.floor_id].some((area) => |           floorAreaLookup[floor.floor_id]?.some((area) => | ||||||
|             areaIds!.includes(area.area_id) |             areaIds!.includes(area.area_id) | ||||||
|           ) |           ) | ||||||
|         ); |         ); | ||||||
| @@ -386,7 +384,7 @@ export class HaFloorPicker extends SubscribeMixin(LitElement) { | |||||||
|         this.comboBox.filteredItems = [ |         this.comboBox.filteredItems = [ | ||||||
|           { |           { | ||||||
|             floor_id: NO_FLOORS_ID, |             floor_id: NO_FLOORS_ID, | ||||||
|             name: this.hass.localize("ui.components.floor-picker.no_floors"), |             name: this.hass.localize("ui.components.floor-picker.no_match"), | ||||||
|             icon: null, |             icon: null, | ||||||
|             level: null, |             level: null, | ||||||
|             aliases: [], |             aliases: [], | ||||||
| @@ -438,25 +436,12 @@ export class HaFloorPicker extends SubscribeMixin(LitElement) { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     (ev.target as any).value = this._value; |     (ev.target as any).value = this._value; | ||||||
|     showPromptDialog(this, { |  | ||||||
|       title: this.hass.localize("ui.components.floor-picker.add_dialog.title"), |     showFloorRegistryDetailDialog(this, { | ||||||
|       text: this.hass.localize("ui.components.floor-picker.add_dialog.text"), |       suggestedName: newValue === ADD_NEW_SUGGESTION_ID ? this._suggestion : "", | ||||||
|       confirmText: this.hass.localize( |       createEntry: async (values) => { | ||||||
|         "ui.components.floor-picker.add_dialog.add" |  | ||||||
|       ), |  | ||||||
|       inputLabel: this.hass.localize( |  | ||||||
|         "ui.components.floor-picker.add_dialog.name" |  | ||||||
|       ), |  | ||||||
|       defaultValue: |  | ||||||
|         newValue === ADD_NEW_SUGGESTION_ID ? this._suggestion : undefined, |  | ||||||
|       confirm: async (name) => { |  | ||||||
|         if (!name) { |  | ||||||
|           return; |  | ||||||
|         } |  | ||||||
|         try { |         try { | ||||||
|           const floor = await createFloorRegistryEntry(this.hass, { |           const floor = await createFloorRegistryEntry(this.hass, values); | ||||||
|             name, |  | ||||||
|           }); |  | ||||||
|           const floors = [...this._floors!, floor]; |           const floors = [...this._floors!, floor]; | ||||||
|           this.comboBox.filteredItems = this._getFloors( |           this.comboBox.filteredItems = this._getFloors( | ||||||
|             floors, |             floors, | ||||||
| @@ -477,18 +462,16 @@ export class HaFloorPicker extends SubscribeMixin(LitElement) { | |||||||
|         } catch (err: any) { |         } catch (err: any) { | ||||||
|           showAlertDialog(this, { |           showAlertDialog(this, { | ||||||
|             title: this.hass.localize( |             title: this.hass.localize( | ||||||
|               "ui.components.floor-picker.add_dialog.failed_create_floor" |               "ui.components.floor-picker.failed_create_floor" | ||||||
|             ), |             ), | ||||||
|             text: err.message, |             text: err.message, | ||||||
|           }); |           }); | ||||||
|         } |         } | ||||||
|       }, |       }, | ||||||
|       cancel: () => { |  | ||||||
|         this._setValue(undefined); |  | ||||||
|         this._suggestion = undefined; |  | ||||||
|         this.comboBox.setInputValue(""); |  | ||||||
|       }, |  | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|  |     this._suggestion = undefined; | ||||||
|  |     this.comboBox.setInputValue(""); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _setValue(value?: string) { |   private _setValue(value?: string) { | ||||||
|   | |||||||
							
								
								
									
										169
									
								
								src/components/ha-floors-picker.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										169
									
								
								src/components/ha-floors-picker.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,169 @@ | |||||||
|  | import { HassEntity } from "home-assistant-js-websocket"; | ||||||
|  | import { css, html, LitElement, nothing } from "lit"; | ||||||
|  | import { customElement, property } from "lit/decorators"; | ||||||
|  | import { fireEvent } from "../common/dom/fire_event"; | ||||||
|  | import { SubscribeMixin } from "../mixins/subscribe-mixin"; | ||||||
|  | import type { HomeAssistant } from "../types"; | ||||||
|  | import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker"; | ||||||
|  | import "./ha-floor-picker"; | ||||||
|  |  | ||||||
|  | @customElement("ha-floors-picker") | ||||||
|  | export class HaFloorsPicker extends SubscribeMixin(LitElement) { | ||||||
|  |   @property({ attribute: false }) public hass!: HomeAssistant; | ||||||
|  |  | ||||||
|  |   @property() public label?: string; | ||||||
|  |  | ||||||
|  |   @property({ type: Array }) public value?: string[]; | ||||||
|  |  | ||||||
|  |   @property() public helper?: string; | ||||||
|  |  | ||||||
|  |   @property() public placeholder?: string; | ||||||
|  |  | ||||||
|  |   @property({ type: Boolean, attribute: "no-add" }) | ||||||
|  |   public noAdd = false; | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Show only floors with entities from specific domains. | ||||||
|  |    * @type {Array} | ||||||
|  |    * @attr include-domains | ||||||
|  |    */ | ||||||
|  |   @property({ type: Array, attribute: "include-domains" }) | ||||||
|  |   public includeDomains?: string[]; | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Show no floors with entities of these domains. | ||||||
|  |    * @type {Array} | ||||||
|  |    * @attr exclude-domains | ||||||
|  |    */ | ||||||
|  |   @property({ type: Array, attribute: "exclude-domains" }) | ||||||
|  |   public excludeDomains?: string[]; | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Show only floors with entities of these device classes. | ||||||
|  |    * @type {Array} | ||||||
|  |    * @attr include-device-classes | ||||||
|  |    */ | ||||||
|  |   @property({ type: Array, attribute: "include-device-classes" }) | ||||||
|  |   public includeDeviceClasses?: string[]; | ||||||
|  |  | ||||||
|  |   @property({ attribute: false }) | ||||||
|  |   public deviceFilter?: HaDevicePickerDeviceFilterFunc; | ||||||
|  |  | ||||||
|  |   @property({ attribute: false }) | ||||||
|  |   public entityFilter?: (entity: HassEntity) => boolean; | ||||||
|  |  | ||||||
|  |   @property({ attribute: "picked-floor-label" }) | ||||||
|  |   public pickedFloorLabel?: string; | ||||||
|  |  | ||||||
|  |   @property({ attribute: "pick-floor-label" }) | ||||||
|  |   public pickFloorLabel?: string; | ||||||
|  |  | ||||||
|  |   @property({ type: Boolean }) public disabled = false; | ||||||
|  |  | ||||||
|  |   @property({ type: Boolean }) public required = false; | ||||||
|  |  | ||||||
|  |   protected render() { | ||||||
|  |     if (!this.hass) { | ||||||
|  |       return nothing; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const currentFloors = this._currentFloors; | ||||||
|  |     return html` | ||||||
|  |       ${currentFloors.map( | ||||||
|  |         (floor) => html` | ||||||
|  |           <div> | ||||||
|  |             <ha-floor-picker | ||||||
|  |               .curValue=${floor} | ||||||
|  |               .noAdd=${this.noAdd} | ||||||
|  |               .hass=${this.hass} | ||||||
|  |               .value=${floor} | ||||||
|  |               .label=${this.pickedFloorLabel} | ||||||
|  |               .includeDomains=${this.includeDomains} | ||||||
|  |               .excludeDomains=${this.excludeDomains} | ||||||
|  |               .includeDeviceClasses=${this.includeDeviceClasses} | ||||||
|  |               .deviceFilter=${this.deviceFilter} | ||||||
|  |               .entityFilter=${this.entityFilter} | ||||||
|  |               .disabled=${this.disabled} | ||||||
|  |               @value-changed=${this._floorChanged} | ||||||
|  |             ></ha-floor-picker> | ||||||
|  |           </div> | ||||||
|  |         ` | ||||||
|  |       )} | ||||||
|  |       <div> | ||||||
|  |         <ha-floor-picker | ||||||
|  |           .noAdd=${this.noAdd} | ||||||
|  |           .hass=${this.hass} | ||||||
|  |           .label=${this.pickFloorLabel} | ||||||
|  |           .helper=${this.helper} | ||||||
|  |           .includeDomains=${this.includeDomains} | ||||||
|  |           .excludeDomains=${this.excludeDomains} | ||||||
|  |           .includeDeviceClasses=${this.includeDeviceClasses} | ||||||
|  |           .deviceFilter=${this.deviceFilter} | ||||||
|  |           .entityFilter=${this.entityFilter} | ||||||
|  |           .disabled=${this.disabled} | ||||||
|  |           .placeholder=${this.placeholder} | ||||||
|  |           .required=${this.required && !currentFloors.length} | ||||||
|  |           @value-changed=${this._addFloor} | ||||||
|  |           .excludeFloors=${currentFloors} | ||||||
|  |         ></ha-floor-picker> | ||||||
|  |       </div> | ||||||
|  |     `; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private get _currentFloors(): string[] { | ||||||
|  |     return this.value || []; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private async _updateFloors(floors) { | ||||||
|  |     this.value = floors; | ||||||
|  |  | ||||||
|  |     fireEvent(this, "value-changed", { | ||||||
|  |       value: floors, | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _floorChanged(ev: CustomEvent) { | ||||||
|  |     ev.stopPropagation(); | ||||||
|  |     const curValue = (ev.currentTarget as any).curValue; | ||||||
|  |     const newValue = ev.detail.value; | ||||||
|  |     if (newValue === curValue) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     const currentFloors = this._currentFloors; | ||||||
|  |     if (!newValue || currentFloors.includes(newValue)) { | ||||||
|  |       this._updateFloors(currentFloors.filter((ent) => ent !== curValue)); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     this._updateFloors( | ||||||
|  |       currentFloors.map((ent) => (ent === curValue ? newValue : ent)) | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _addFloor(ev: CustomEvent) { | ||||||
|  |     ev.stopPropagation(); | ||||||
|  |  | ||||||
|  |     const toAdd = ev.detail.value; | ||||||
|  |     if (!toAdd) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     (ev.currentTarget as any).value = ""; | ||||||
|  |     const currentFloors = this._currentFloors; | ||||||
|  |     if (currentFloors.includes(toAdd)) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     this._updateFloors([...currentFloors, toAdd]); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   static override styles = css` | ||||||
|  |     div { | ||||||
|  |       margin-top: 8px; | ||||||
|  |     } | ||||||
|  |   `; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | declare global { | ||||||
|  |   interface HTMLElementTagNameMap { | ||||||
|  |     "ha-floors-picker": HaFloorsPicker; | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -43,6 +43,7 @@ class HaLabel extends LitElement { | |||||||
|           border-radius: 18px; |           border-radius: 18px; | ||||||
|           color: var(--ha-label-text-color); |           color: var(--ha-label-text-color); | ||||||
|           --mdc-icon-size: 12px; |           --mdc-icon-size: 12px; | ||||||
|  |           text-wrap: nowrap; | ||||||
|         } |         } | ||||||
|         .content > * { |         .content > * { | ||||||
|           position: relative; |           position: relative; | ||||||
|   | |||||||
| @@ -17,6 +17,7 @@ import "./chips/ha-input-chip"; | |||||||
| import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker"; | import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker"; | ||||||
| import "./ha-label-picker"; | import "./ha-label-picker"; | ||||||
| import type { HaLabelPicker } from "./ha-label-picker"; | import type { HaLabelPicker } from "./ha-label-picker"; | ||||||
|  | import { stringCompare } from "../common/string/compare"; | ||||||
|  |  | ||||||
| @customElement("ha-labels-picker") | @customElement("ha-labels-picker") | ||||||
| export class HaLabelsPicker extends SubscribeMixin(LitElement) { | export class HaLabelsPicker extends SubscribeMixin(LitElement) { | ||||||
| @@ -75,7 +76,7 @@ export class HaLabelsPicker extends SubscribeMixin(LitElement) { | |||||||
|  |  | ||||||
|   @property({ type: Boolean }) public required = false; |   @property({ type: Boolean }) public required = false; | ||||||
|  |  | ||||||
|   @state() private _labels?: LabelRegistryEntry[]; |   @state() private _labels?: { [id: string]: LabelRegistryEntry }; | ||||||
|  |  | ||||||
|   @query("ha-label-picker", true) public labelPicker!: HaLabelPicker; |   @query("ha-label-picker", true) public labelPicker!: HaLabelPicker; | ||||||
|  |  | ||||||
| @@ -92,22 +93,28 @@ export class HaLabelsPicker extends SubscribeMixin(LitElement) { | |||||||
|   protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] { |   protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] { | ||||||
|     return [ |     return [ | ||||||
|       subscribeLabelRegistry(this.hass.connection, (labels) => { |       subscribeLabelRegistry(this.hass.connection, (labels) => { | ||||||
|         this._labels = labels; |         const lookUp = {}; | ||||||
|  |         labels.forEach((label) => { | ||||||
|  |           lookUp[label.label_id] = label; | ||||||
|  |         }); | ||||||
|  |         this._labels = lookUp; | ||||||
|       }), |       }), | ||||||
|     ]; |     ]; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   protected render(): TemplateResult { |   protected render(): TemplateResult { | ||||||
|  |     const labels = this.value | ||||||
|  |       ?.map((id) => this._labels?.[id]) | ||||||
|  |       .sort((a, b) => | ||||||
|  |         stringCompare(a?.name || "", b?.name || "", this.hass.locale.language) | ||||||
|  |       ); | ||||||
|     return html` |     return html` | ||||||
|       ${this.value?.length |       ${labels?.length | ||||||
|         ? html`<ha-chip-set> |         ? html`<ha-chip-set> | ||||||
|             ${repeat( |             ${repeat( | ||||||
|               this.value, |               labels, | ||||||
|               (item) => item, |               (label) => label?.label_id, | ||||||
|               (item, idx) => { |               (label, idx) => { | ||||||
|                 const label = this._labels?.find( |  | ||||||
|                   (lbl) => lbl.label_id === item |  | ||||||
|                 ); |  | ||||||
|                 const color = label?.color |                 const color = label?.color | ||||||
|                   ? computeCssColor(label.color) |                   ? computeCssColor(label.color) | ||||||
|                   : undefined; |                   : undefined; | ||||||
| @@ -168,9 +175,6 @@ export class HaLabelsPicker extends SubscribeMixin(LitElement) { | |||||||
|           label.label_id, |           label.label_id, | ||||||
|           values |           values | ||||||
|         ); |         ); | ||||||
|         this._labels = this._labels!.map((lbl) => |  | ||||||
|           lbl.label_id === updated.label_id ? updated : lbl |  | ||||||
|         ); |  | ||||||
|         return updated; |         return updated; | ||||||
|       }, |       }, | ||||||
|     }); |     }); | ||||||
|   | |||||||
							
								
								
									
										44
									
								
								src/components/ha-menu-item.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								src/components/ha-menu-item.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | |||||||
|  | import { customElement } from "lit/decorators"; | ||||||
|  | import "element-internals-polyfill"; | ||||||
|  | import { CSSResult, css } from "lit"; | ||||||
|  | import { MdMenuItem } from "@material/web/menu/menu-item"; | ||||||
|  |  | ||||||
|  | @customElement("ha-menu-item") | ||||||
|  | export class HaMenuItem extends MdMenuItem { | ||||||
|  |   static override styles: CSSResult[] = [ | ||||||
|  |     ...MdMenuItem.styles, | ||||||
|  |     css` | ||||||
|  |       :host { | ||||||
|  |         --ha-icon-display: block; | ||||||
|  |         --md-sys-color-primary: var(--primary-text-color); | ||||||
|  |         --md-sys-color-on-primary: var(--primary-text-color); | ||||||
|  |         --md-sys-color-secondary: var(--secondary-text-color); | ||||||
|  |         --md-sys-color-surface: var(--card-background-color); | ||||||
|  |         --md-sys-color-on-surface: var(--primary-text-color); | ||||||
|  |         --md-sys-color-on-surface-variant: var(--secondary-text-color); | ||||||
|  |         --md-sys-color-secondary-container: rgba( | ||||||
|  |           var(--rgb-primary-color), | ||||||
|  |           0.15 | ||||||
|  |         ); | ||||||
|  |         --md-sys-color-on-secondary-container: var(--text-primary-color); | ||||||
|  |         --mdc-icon-size: 16px; | ||||||
|  |  | ||||||
|  |         --md-sys-color-on-primary-container: var(--primary-text-color); | ||||||
|  |         --md-sys-color-on-secondary-container: var(--primary-text-color); | ||||||
|  |       } | ||||||
|  |       :host(.warning) { | ||||||
|  |         --md-menu-item-label-text-color: var(--error-color); | ||||||
|  |         --md-menu-item-leading-icon-color: var(--error-color); | ||||||
|  |       } | ||||||
|  |       ::slotted([slot="headline"]) { | ||||||
|  |         text-wrap: nowrap; | ||||||
|  |       } | ||||||
|  |     `, | ||||||
|  |   ]; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | declare global { | ||||||
|  |   interface HTMLElementTagNameMap { | ||||||
|  |     "ha-menu-item": HaMenuItem; | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										22
									
								
								src/components/ha-menu.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/components/ha-menu.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | |||||||
|  | import { customElement } from "lit/decorators"; | ||||||
|  | import "element-internals-polyfill"; | ||||||
|  | import { CSSResult, css } from "lit"; | ||||||
|  | import { MdMenu } from "@material/web/menu/menu"; | ||||||
|  |  | ||||||
|  | @customElement("ha-menu") | ||||||
|  | export class HaMenu extends MdMenu { | ||||||
|  |   static override styles: CSSResult[] = [ | ||||||
|  |     ...MdMenu.styles, | ||||||
|  |     css` | ||||||
|  |       :host { | ||||||
|  |         --md-sys-color-surface-container: var(--card-background-color); | ||||||
|  |       } | ||||||
|  |     `, | ||||||
|  |   ]; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | declare global { | ||||||
|  |   interface HTMLElementTagNameMap { | ||||||
|  |     "ha-menu": HaMenu; | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -27,6 +27,9 @@ export class HaOutlinedTextField extends MdOutlinedTextField { | |||||||
|         --md-outlined-field-focus-outline-width: 1px; |         --md-outlined-field-focus-outline-width: 1px; | ||||||
|         --mdc-icon-size: var(--md-input-chip-icon-size, 18px); |         --mdc-icon-size: var(--md-input-chip-icon-size, 18px); | ||||||
|       } |       } | ||||||
|  |       .input { | ||||||
|  |         font-family: Roboto, sans-serif; | ||||||
|  |       } | ||||||
|     `, |     `, | ||||||
|   ]; |   ]; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -87,8 +87,12 @@ export class HaAreaSelector extends LitElement { | |||||||
|           .label=${this.label} |           .label=${this.label} | ||||||
|           .helper=${this.helper} |           .helper=${this.helper} | ||||||
|           no-add |           no-add | ||||||
|           .deviceFilter=${this._filterDevices} |           .deviceFilter=${this.selector.area?.device | ||||||
|           .entityFilter=${this._filterEntities} |             ? this._filterDevices | ||||||
|  |             : undefined} | ||||||
|  |           .entityFilter=${this.selector.area?.entity | ||||||
|  |             ? this._filterEntities | ||||||
|  |             : undefined} | ||||||
|           .disabled=${this.disabled} |           .disabled=${this.disabled} | ||||||
|           .required=${this.required} |           .required=${this.required} | ||||||
|         ></ha-area-picker> |         ></ha-area-picker> | ||||||
| @@ -102,8 +106,12 @@ export class HaAreaSelector extends LitElement { | |||||||
|         .helper=${this.helper} |         .helper=${this.helper} | ||||||
|         .pickAreaLabel=${this.label} |         .pickAreaLabel=${this.label} | ||||||
|         no-add |         no-add | ||||||
|         .deviceFilter=${this._filterDevices} |         .deviceFilter=${this.selector.area?.device | ||||||
|         .entityFilter=${this._filterEntities} |           ? this._filterDevices | ||||||
|  |           : undefined} | ||||||
|  |         .entityFilter=${this.selector.area?.entity | ||||||
|  |           ? this._filterEntities | ||||||
|  |           : undefined} | ||||||
|         .disabled=${this.disabled} |         .disabled=${this.disabled} | ||||||
|         .required=${this.required} |         .required=${this.required} | ||||||
|       ></ha-areas-picker> |       ></ha-areas-picker> | ||||||
|   | |||||||
							
								
								
									
										153
									
								
								src/components/ha-selector/ha-selector-floor.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										153
									
								
								src/components/ha-selector/ha-selector-floor.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,153 @@ | |||||||
|  | import { HassEntity } from "home-assistant-js-websocket"; | ||||||
|  | import { html, LitElement, PropertyValues, nothing } from "lit"; | ||||||
|  | import { customElement, property, state } from "lit/decorators"; | ||||||
|  | import memoizeOne from "memoize-one"; | ||||||
|  | import { ensureArray } from "../../common/array/ensure-array"; | ||||||
|  | import type { DeviceRegistryEntry } from "../../data/device_registry"; | ||||||
|  | import { getDeviceIntegrationLookup } from "../../data/device_registry"; | ||||||
|  | import { fireEvent } from "../../common/dom/fire_event"; | ||||||
|  | import { | ||||||
|  |   EntitySources, | ||||||
|  |   fetchEntitySourcesWithCache, | ||||||
|  | } from "../../data/entity_sources"; | ||||||
|  | import type { FloorSelector } from "../../data/selector"; | ||||||
|  | import { | ||||||
|  |   filterSelectorDevices, | ||||||
|  |   filterSelectorEntities, | ||||||
|  | } from "../../data/selector"; | ||||||
|  | import { HomeAssistant } from "../../types"; | ||||||
|  | import "../ha-floor-picker"; | ||||||
|  | import "../ha-floors-picker"; | ||||||
|  |  | ||||||
|  | @customElement("ha-selector-floor") | ||||||
|  | export class HaFloorSelector extends LitElement { | ||||||
|  |   @property({ attribute: false }) public hass!: HomeAssistant; | ||||||
|  |  | ||||||
|  |   @property({ attribute: false }) public selector!: FloorSelector; | ||||||
|  |  | ||||||
|  |   @property() public value?: any; | ||||||
|  |  | ||||||
|  |   @property() public label?: string; | ||||||
|  |  | ||||||
|  |   @property() public helper?: string; | ||||||
|  |  | ||||||
|  |   @property({ type: Boolean }) public disabled = false; | ||||||
|  |  | ||||||
|  |   @property({ type: Boolean }) public required = true; | ||||||
|  |  | ||||||
|  |   @state() private _entitySources?: EntitySources; | ||||||
|  |  | ||||||
|  |   private _deviceIntegrationLookup = memoizeOne(getDeviceIntegrationLookup); | ||||||
|  |  | ||||||
|  |   private _hasIntegration(selector: FloorSelector) { | ||||||
|  |     return ( | ||||||
|  |       (selector.floor?.entity && | ||||||
|  |         ensureArray(selector.floor.entity).some( | ||||||
|  |           (filter) => filter.integration | ||||||
|  |         )) || | ||||||
|  |       (selector.floor?.device && | ||||||
|  |         ensureArray(selector.floor.device).some((device) => device.integration)) | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   protected willUpdate(changedProperties: PropertyValues): void { | ||||||
|  |     if (changedProperties.has("selector") && this.value !== undefined) { | ||||||
|  |       if (this.selector.floor?.multiple && !Array.isArray(this.value)) { | ||||||
|  |         this.value = [this.value]; | ||||||
|  |         fireEvent(this, "value-changed", { value: this.value }); | ||||||
|  |       } else if (!this.selector.floor?.multiple && Array.isArray(this.value)) { | ||||||
|  |         this.value = this.value[0]; | ||||||
|  |         fireEvent(this, "value-changed", { value: this.value }); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   protected updated(changedProperties: PropertyValues): void { | ||||||
|  |     if ( | ||||||
|  |       changedProperties.has("selector") && | ||||||
|  |       this._hasIntegration(this.selector) && | ||||||
|  |       !this._entitySources | ||||||
|  |     ) { | ||||||
|  |       fetchEntitySourcesWithCache(this.hass).then((sources) => { | ||||||
|  |         this._entitySources = sources; | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   protected render() { | ||||||
|  |     if (this._hasIntegration(this.selector) && !this._entitySources) { | ||||||
|  |       return nothing; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (!this.selector.floor?.multiple) { | ||||||
|  |       return html` | ||||||
|  |         <ha-floor-picker | ||||||
|  |           .hass=${this.hass} | ||||||
|  |           .value=${this.value} | ||||||
|  |           .label=${this.label} | ||||||
|  |           .helper=${this.helper} | ||||||
|  |           no-add | ||||||
|  |           .deviceFilter=${this.selector.floor?.device | ||||||
|  |             ? this._filterDevices | ||||||
|  |             : undefined} | ||||||
|  |           .entityFilter=${this.selector.floor?.entity | ||||||
|  |             ? this._filterEntities | ||||||
|  |             : undefined} | ||||||
|  |           .disabled=${this.disabled} | ||||||
|  |           .required=${this.required} | ||||||
|  |         ></ha-floor-picker> | ||||||
|  |       `; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return html` | ||||||
|  |       <ha-floors-picker | ||||||
|  |         .hass=${this.hass} | ||||||
|  |         .value=${this.value} | ||||||
|  |         .helper=${this.helper} | ||||||
|  |         .pickFloorLabel=${this.label} | ||||||
|  |         no-add | ||||||
|  |         .deviceFilter=${this.selector.floor?.device | ||||||
|  |           ? this._filterDevices | ||||||
|  |           : undefined} | ||||||
|  |         .entityFilter=${this.selector.floor?.entity | ||||||
|  |           ? this._filterEntities | ||||||
|  |           : undefined} | ||||||
|  |         .disabled=${this.disabled} | ||||||
|  |         .required=${this.required} | ||||||
|  |       ></ha-floors-picker> | ||||||
|  |     `; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _filterEntities = (entity: HassEntity): boolean => { | ||||||
|  |     if (!this.selector.floor?.entity) { | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return ensureArray(this.selector.floor.entity).some((filter) => | ||||||
|  |       filterSelectorEntities(filter, entity, this._entitySources) | ||||||
|  |     ); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   private _filterDevices = (device: DeviceRegistryEntry): boolean => { | ||||||
|  |     if (!this.selector.floor?.device) { | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const deviceIntegrations = this._entitySources | ||||||
|  |       ? this._deviceIntegrationLookup( | ||||||
|  |           this._entitySources, | ||||||
|  |           Object.values(this.hass.entities) | ||||||
|  |         ) | ||||||
|  |       : undefined; | ||||||
|  |  | ||||||
|  |     return ensureArray(this.selector.floor.device).some((filter) => | ||||||
|  |       filterSelectorDevices(filter, device, deviceIntegrations) | ||||||
|  |     ); | ||||||
|  |   }; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | declare global { | ||||||
|  |   interface HTMLElementTagNameMap { | ||||||
|  |     "ha-selector-floor": HaFloorSelector; | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -30,6 +30,7 @@ const LOAD_ELEMENTS = { | |||||||
|   entity: () => import("./ha-selector-entity"), |   entity: () => import("./ha-selector-entity"), | ||||||
|   statistic: () => import("./ha-selector-statistic"), |   statistic: () => import("./ha-selector-statistic"), | ||||||
|   file: () => import("./ha-selector-file"), |   file: () => import("./ha-selector-file"), | ||||||
|  |   floor: () => import("./ha-selector-floor"), | ||||||
|   label: () => import("./ha-selector-label"), |   label: () => import("./ha-selector-label"), | ||||||
|   language: () => import("./ha-selector-language"), |   language: () => import("./ha-selector-language"), | ||||||
|   navigation: () => import("./ha-selector-navigation"), |   navigation: () => import("./ha-selector-navigation"), | ||||||
|   | |||||||
							
								
								
									
										38
									
								
								src/components/ha-sub-menu.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								src/components/ha-sub-menu.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | |||||||
|  | import { customElement } from "lit/decorators"; | ||||||
|  | import "element-internals-polyfill"; | ||||||
|  | import { CSSResult, css } from "lit"; | ||||||
|  | import { MdSubMenu } from "@material/web/menu/sub-menu"; | ||||||
|  |  | ||||||
|  | @customElement("ha-sub-menu") | ||||||
|  | // @ts-expect-error | ||||||
|  | export class HaSubMenu extends MdSubMenu { | ||||||
|  |   static override styles: CSSResult[] = [ | ||||||
|  |     MdSubMenu.styles, | ||||||
|  |     css` | ||||||
|  |       :host { | ||||||
|  |         --ha-icon-display: block; | ||||||
|  |         --md-sys-color-primary: var(--primary-text-color); | ||||||
|  |         --md-sys-color-on-primary: var(--primary-text-color); | ||||||
|  |         --md-sys-color-secondary: var(--secondary-text-color); | ||||||
|  |         --md-sys-color-surface: var(--card-background-color); | ||||||
|  |         --md-sys-color-on-surface: var(--primary-text-color); | ||||||
|  |         --md-sys-color-on-surface-variant: var(--secondary-text-color); | ||||||
|  |         --md-sys-color-secondary-container: rgba( | ||||||
|  |           var(--rgb-primary-color), | ||||||
|  |           0.15 | ||||||
|  |         ); | ||||||
|  |         --md-sys-color-on-secondary-container: var(--text-primary-color); | ||||||
|  |         --mdc-icon-size: 16px; | ||||||
|  |  | ||||||
|  |         --md-sys-color-on-primary-container: var(--primary-text-color); | ||||||
|  |         --md-sys-color-on-secondary-container: var(--primary-text-color); | ||||||
|  |       } | ||||||
|  |     `, | ||||||
|  |   ]; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | declare global { | ||||||
|  |   interface HTMLElementTagNameMap { | ||||||
|  |     "ha-sub-menu": HaSubMenu; | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -31,6 +31,7 @@ export type Selector = | |||||||
|   | DateSelector |   | DateSelector | ||||||
|   | DateTimeSelector |   | DateTimeSelector | ||||||
|   | DeviceSelector |   | DeviceSelector | ||||||
|  |   | FloorSelector | ||||||
|   | LegacyDeviceSelector |   | LegacyDeviceSelector | ||||||
|   | DurationSelector |   | DurationSelector | ||||||
|   | EntitySelector |   | EntitySelector | ||||||
| @@ -170,6 +171,14 @@ export interface DeviceSelector { | |||||||
|   } | null; |   } | null; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | export interface FloorSelector { | ||||||
|  |   floor: { | ||||||
|  |     entity?: EntitySelectorFilter | readonly EntitySelectorFilter[]; | ||||||
|  |     device?: DeviceSelectorFilter | readonly DeviceSelectorFilter[]; | ||||||
|  |     multiple?: boolean; | ||||||
|  |   } | null; | ||||||
|  | } | ||||||
|  |  | ||||||
| export interface LegacyDeviceSelector { | export interface LegacyDeviceSelector { | ||||||
|   device: DeviceSelector["device"] & { |   device: DeviceSelector["device"] & { | ||||||
|     /** |     /** | ||||||
|   | |||||||
| @@ -77,6 +77,8 @@ declare global { | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | const DEFAULT_VIEW: View = "info"; | ||||||
|  |  | ||||||
| @customElement("ha-more-info-dialog") | @customElement("ha-more-info-dialog") | ||||||
| export class MoreInfoDialog extends LitElement { | export class MoreInfoDialog extends LitElement { | ||||||
|   @property({ attribute: false }) public hass!: HomeAssistant; |   @property({ attribute: false }) public hass!: HomeAssistant; | ||||||
| @@ -85,7 +87,9 @@ export class MoreInfoDialog extends LitElement { | |||||||
|  |  | ||||||
|   @state() private _entityId?: string | null; |   @state() private _entityId?: string | null; | ||||||
|  |  | ||||||
|   @state() private _currView: View = "info"; |   @state() private _currView: View = DEFAULT_VIEW; | ||||||
|  |  | ||||||
|  |   @state() private _initialView: View = DEFAULT_VIEW; | ||||||
|  |  | ||||||
|   @state() private _childView?: ChildView; |   @state() private _childView?: ChildView; | ||||||
|  |  | ||||||
| @@ -102,7 +106,8 @@ export class MoreInfoDialog extends LitElement { | |||||||
|       this.closeDialog(); |       this.closeDialog(); | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|     this._currView = params.view || "info"; |     this._currView = params.view || DEFAULT_VIEW; | ||||||
|  |     this._initialView = params.view || DEFAULT_VIEW; | ||||||
|     this._childView = undefined; |     this._childView = undefined; | ||||||
|     this.large = false; |     this.large = false; | ||||||
|     this._loadEntityRegistryEntry(); |     this._loadEntityRegistryEntry(); | ||||||
| @@ -127,6 +132,7 @@ export class MoreInfoDialog extends LitElement { | |||||||
|     this._entry = undefined; |     this._entry = undefined; | ||||||
|     this._childView = undefined; |     this._childView = undefined; | ||||||
|     this._infoEditMode = false; |     this._infoEditMode = false; | ||||||
|  |     this._initialView = DEFAULT_VIEW; | ||||||
|     fireEvent(this, "dialog-closed", { dialog: this.localName }); |     fireEvent(this, "dialog-closed", { dialog: this.localName }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -183,10 +189,15 @@ export class MoreInfoDialog extends LitElement { | |||||||
|     if (this._childView) { |     if (this._childView) { | ||||||
|       this._childView = undefined; |       this._childView = undefined; | ||||||
|     } else { |     } else { | ||||||
|       this.setView("info"); |       this.setView(this._initialView); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   private _resetInitialView() { | ||||||
|  |     this._initialView = DEFAULT_VIEW; | ||||||
|  |     this.setView(DEFAULT_VIEW); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   private _goToHistory() { |   private _goToHistory() { | ||||||
|     this.setView("history"); |     this.setView("history"); | ||||||
|   } |   } | ||||||
| @@ -262,7 +273,10 @@ export class MoreInfoDialog extends LitElement { | |||||||
|  |  | ||||||
|     const title = this._childView?.viewTitle ?? name; |     const title = this._childView?.viewTitle ?? name; | ||||||
|  |  | ||||||
|     const isInfoView = this._currView === "info" && !this._childView; |     const isDefaultView = this._currView === DEFAULT_VIEW && !this._childView; | ||||||
|  |     const isSpecificInitialView = | ||||||
|  |       this._initialView !== DEFAULT_VIEW && !this._childView; | ||||||
|  |     const showCloseIcon = isDefaultView || isSpecificInitialView; | ||||||
|  |  | ||||||
|     return html` |     return html` | ||||||
|       <ha-dialog |       <ha-dialog | ||||||
| @@ -274,7 +288,7 @@ export class MoreInfoDialog extends LitElement { | |||||||
|         flexContent |         flexContent | ||||||
|       > |       > | ||||||
|         <ha-dialog-header slot="heading"> |         <ha-dialog-header slot="heading"> | ||||||
|           ${isInfoView |           ${showCloseIcon | ||||||
|             ? html` |             ? html` | ||||||
|                 <ha-icon-button |                 <ha-icon-button | ||||||
|                   slot="navigationIcon" |                   slot="navigationIcon" | ||||||
| @@ -297,7 +311,7 @@ export class MoreInfoDialog extends LitElement { | |||||||
|           <span slot="title" .title=${title} @click=${this._enlarge}> |           <span slot="title" .title=${title} @click=${this._enlarge}> | ||||||
|             ${title} |             ${title} | ||||||
|           </span> |           </span> | ||||||
|           ${isInfoView |           ${isDefaultView | ||||||
|             ? html` |             ? html` | ||||||
|                 ${this.shouldShowHistory(domain) |                 ${this.shouldShowHistory(domain) | ||||||
|                   ? html` |                   ? html` | ||||||
| @@ -407,7 +421,34 @@ export class MoreInfoDialog extends LitElement { | |||||||
|                     ` |                     ` | ||||||
|                   : nothing} |                   : nothing} | ||||||
|               ` |               ` | ||||||
|             : nothing} |             : isSpecificInitialView | ||||||
|  |               ? html` | ||||||
|  |                   <ha-button-menu | ||||||
|  |                     corner="BOTTOM_END" | ||||||
|  |                     menuCorner="END" | ||||||
|  |                     slot="actionItems" | ||||||
|  |                     @closed=${stopPropagation} | ||||||
|  |                     fixed | ||||||
|  |                   > | ||||||
|  |                     <ha-icon-button | ||||||
|  |                       slot="trigger" | ||||||
|  |                       .label=${this.hass.localize("ui.common.menu")} | ||||||
|  |                       .path=${mdiDotsVertical} | ||||||
|  |                     ></ha-icon-button> | ||||||
|  |  | ||||||
|  |                     <ha-list-item | ||||||
|  |                       graphic="icon" | ||||||
|  |                       @request-selected=${this._resetInitialView} | ||||||
|  |                     > | ||||||
|  |                       ${this.hass.localize("ui.dialogs.more_info_control.info")} | ||||||
|  |                       <ha-svg-icon | ||||||
|  |                         slot="graphic" | ||||||
|  |                         .path=${mdiInformationOutline} | ||||||
|  |                       ></ha-svg-icon> | ||||||
|  |                     </ha-list-item> | ||||||
|  |                   </ha-button-menu> | ||||||
|  |                 ` | ||||||
|  |               : nothing} | ||||||
|         </ha-dialog-header> |         </ha-dialog-header> | ||||||
|         <div |         <div | ||||||
|           class="content" |           class="content" | ||||||
|   | |||||||
| @@ -142,9 +142,12 @@ class HassSubpage extends LitElement { | |||||||
|           right: calc(16px + env(safe-area-inset-right)); |           right: calc(16px + env(safe-area-inset-right)); | ||||||
|           inset-inline-end: calc(16px + env(safe-area-inset-right)); |           inset-inline-end: calc(16px + env(safe-area-inset-right)); | ||||||
|           inset-inline-start: initial; |           inset-inline-start: initial; | ||||||
|  |  | ||||||
|           bottom: calc(16px + env(safe-area-inset-bottom)); |           bottom: calc(16px + env(safe-area-inset-bottom)); | ||||||
|           z-index: 1; |           z-index: 1; | ||||||
|  |           display: flex; | ||||||
|  |           flex-wrap: wrap; | ||||||
|  |           justify-content: flex-end; | ||||||
|  |           gap: 8px; | ||||||
|         } |         } | ||||||
|         :host([narrow]) #fab.tabs { |         :host([narrow]) #fab.tabs { | ||||||
|           bottom: calc(84px + env(safe-area-inset-bottom)); |           bottom: calc(84px + env(safe-area-inset-bottom)); | ||||||
|   | |||||||
| @@ -1,15 +1,13 @@ | |||||||
| import { ResizeController } from "@lit-labs/observers/resize-controller"; | import { ResizeController } from "@lit-labs/observers/resize-controller"; | ||||||
| import "@lrnwebcomponents/simple-tooltip/simple-tooltip"; | import "@lrnwebcomponents/simple-tooltip/simple-tooltip"; | ||||||
| import "@material/mwc-button/mwc-button"; | import "@material/mwc-button/mwc-button"; | ||||||
| import "@material/web/menu/menu"; | import "@material/web/divider/divider"; | ||||||
| import type { MdMenu } from "@material/web/menu/menu"; |  | ||||||
| import "@material/web/menu/menu-item"; |  | ||||||
| import { | import { | ||||||
|   mdiArrowDown, |   mdiArrowDown, | ||||||
|   mdiArrowUp, |   mdiArrowUp, | ||||||
|   mdiClose, |   mdiClose, | ||||||
|   mdiFilterRemove, |  | ||||||
|   mdiFilterVariant, |   mdiFilterVariant, | ||||||
|  |   mdiFilterVariantRemove, | ||||||
|   mdiFormatListChecks, |   mdiFormatListChecks, | ||||||
|   mdiMenuDown, |   mdiMenuDown, | ||||||
| } from "@mdi/js"; | } from "@mdi/js"; | ||||||
| @@ -34,7 +32,10 @@ import type { | |||||||
|   HaDataTable, |   HaDataTable, | ||||||
|   SortingDirection, |   SortingDirection, | ||||||
| } from "../components/data-table/ha-data-table"; | } from "../components/data-table/ha-data-table"; | ||||||
|  | import "../components/ha-button-menu-new"; | ||||||
| import "../components/ha-dialog"; | import "../components/ha-dialog"; | ||||||
|  | import { HaMenu } from "../components/ha-menu"; | ||||||
|  | import "../components/ha-menu-item"; | ||||||
| import "../components/search-input-outlined"; | import "../components/search-input-outlined"; | ||||||
| import type { HomeAssistant, Route } from "../types"; | import type { HomeAssistant, Route } from "../types"; | ||||||
| import "./hass-tabs-subpage"; | import "./hass-tabs-subpage"; | ||||||
| @@ -177,9 +178,9 @@ export class HaTabsSubpageDataTable extends LitElement { | |||||||
|  |  | ||||||
|   @query("ha-data-table", true) private _dataTable!: HaDataTable; |   @query("ha-data-table", true) private _dataTable!: HaDataTable; | ||||||
|  |  | ||||||
|   @query("#group-by-menu") private _groupByMenu!: MdMenu; |   @query("#group-by-menu") private _groupByMenu!: HaMenu; | ||||||
|  |  | ||||||
|   @query("#sort-by-menu") private _sortByMenu!: MdMenu; |   @query("#sort-by-menu") private _sortByMenu!: HaMenu; | ||||||
|  |  | ||||||
|   private _showPaneController = new ResizeController(this, { |   private _showPaneController = new ResizeController(this, { | ||||||
|     callback: (entries) => entries[0]?.contentRect.width > 750, |     callback: (entries) => entries[0]?.contentRect.width > 750, | ||||||
| @@ -227,6 +228,9 @@ export class HaTabsSubpageDataTable extends LitElement { | |||||||
|             class="has-dropdown select-mode-chip" |             class="has-dropdown select-mode-chip" | ||||||
|             .active=${this._selectMode} |             .active=${this._selectMode} | ||||||
|             @click=${this._enableSelectMode} |             @click=${this._enableSelectMode} | ||||||
|  |             .title=${localize( | ||||||
|  |               "ui.components.subpage-data-table.enter_selection_mode" | ||||||
|  |             )} | ||||||
|           > |           > | ||||||
|             <ha-svg-icon slot="icon" .path=${mdiFormatListChecks}></ha-svg-icon> |             <ha-svg-icon slot="icon" .path=${mdiFormatListChecks}></ha-svg-icon> | ||||||
|           </ha-assist-chip>` |           </ha-assist-chip>` | ||||||
| @@ -252,8 +256,11 @@ export class HaTabsSubpageDataTable extends LitElement { | |||||||
|             id="sort-by-anchor" |             id="sort-by-anchor" | ||||||
|             @click=${this._toggleSortBy} |             @click=${this._toggleSortBy} | ||||||
|           > |           > | ||||||
|             <ha-svg-icon slot="trailing-icon" .path=${mdiMenuDown}></ha-svg-icon |             <ha-svg-icon | ||||||
|           ></ha-assist-chip> |               slot="trailing-icon" | ||||||
|  |               .path=${mdiMenuDown} | ||||||
|  |             ></ha-svg-icon> | ||||||
|  |           </ha-assist-chip> | ||||||
|         ` |         ` | ||||||
|       : nothing; |       : nothing; | ||||||
|  |  | ||||||
| @@ -290,11 +297,45 @@ export class HaTabsSubpageDataTable extends LitElement { | |||||||
|       > |       > | ||||||
|         ${this._selectMode |         ${this._selectMode | ||||||
|           ? html`<div class="selection-bar" slot="toolbar"> |           ? html`<div class="selection-bar" slot="toolbar"> | ||||||
|               <div class="center-vertical"> |               <div class="selection-controls"> | ||||||
|                 <ha-icon-button |                 <ha-icon-button | ||||||
|                   .path=${mdiClose} |                   .path=${mdiClose} | ||||||
|                   @click=${this._disableSelectMode} |                   @click=${this._disableSelectMode} | ||||||
|  |                   .label=${localize( | ||||||
|  |                     "ui.components.subpage-data-table.exit_selection_mode" | ||||||
|  |                   )} | ||||||
|                 ></ha-icon-button> |                 ></ha-icon-button> | ||||||
|  |                 <ha-button-menu-new positioning="absolute"> | ||||||
|  |                   <ha-assist-chip | ||||||
|  |                     .label=${localize( | ||||||
|  |                       "ui.components.subpage-data-table.select" | ||||||
|  |                     )} | ||||||
|  |                     slot="trigger" | ||||||
|  |                   > | ||||||
|  |                     <ha-svg-icon | ||||||
|  |                       slot="icon" | ||||||
|  |                       .path=${mdiFormatListChecks} | ||||||
|  |                     ></ha-svg-icon> | ||||||
|  |                     <ha-svg-icon | ||||||
|  |                       slot="trailing-icon" | ||||||
|  |                       .path=${mdiMenuDown} | ||||||
|  |                     ></ha-svg-icon | ||||||
|  |                   ></ha-assist-chip> | ||||||
|  |                   <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} | ||||||
|  |                     >${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} | ||||||
|  |                     >${localize( | ||||||
|  |                       "ui.components.subpage-data-table.close_select_mode" | ||||||
|  |                     )} | ||||||
|  |                   </ha-menu-item> | ||||||
|  |                 </ha-button-menu-new> | ||||||
|                 <p> |                 <p> | ||||||
|                   ${localize("ui.components.subpage-data-table.selected", { |                   ${localize("ui.components.subpage-data-table.selected", { | ||||||
|                     selected: this.selected || "0", |                     selected: this.selected || "0", | ||||||
| @@ -318,6 +359,9 @@ export class HaTabsSubpageDataTable extends LitElement { | |||||||
|                     slot="navigationIcon" |                     slot="navigationIcon" | ||||||
|                     .path=${mdiClose} |                     .path=${mdiClose} | ||||||
|                     @click=${this._toggleFilters} |                     @click=${this._toggleFilters} | ||||||
|  |                     .label=${localize( | ||||||
|  |                       "ui.components.subpage-data-table.close_filter" | ||||||
|  |                     )} | ||||||
|                   ></ha-icon-button> |                   ></ha-icon-button> | ||||||
|                   <span slot="title" |                   <span slot="title" | ||||||
|                     >${localize( |                     >${localize( | ||||||
| @@ -326,7 +370,11 @@ export class HaTabsSubpageDataTable extends LitElement { | |||||||
|                   > |                   > | ||||||
|                   <ha-icon-button |                   <ha-icon-button | ||||||
|                     slot="actionItems" |                     slot="actionItems" | ||||||
|                     .path=${mdiFilterRemove} |                     @click=${this._clearFilters} | ||||||
|  |                     .path=${mdiFilterVariantRemove} | ||||||
|  |                     .label=${localize( | ||||||
|  |                       "ui.components.subpage-data-table.clear_filter" | ||||||
|  |                     )} | ||||||
|                   ></ha-icon-button> |                   ></ha-icon-button> | ||||||
|                 </ha-dialog-header> |                 </ha-dialog-header> | ||||||
|                 <div class="filter-dialog-content"> |                 <div class="filter-dialog-content"> | ||||||
| @@ -347,8 +395,11 @@ export class HaTabsSubpageDataTable extends LitElement { | |||||||
|                     ></ha-svg-icon> |                     ></ha-svg-icon> | ||||||
|                   </ha-assist-chip> |                   </ha-assist-chip> | ||||||
|                   <ha-icon-button |                   <ha-icon-button | ||||||
|                     .path=${mdiFilterRemove} |                     .path=${mdiFilterVariantRemove} | ||||||
|                     @click=${this._clearFilters} |                     @click=${this._clearFilters} | ||||||
|  |                     .label=${localize( | ||||||
|  |                       "ui.components.subpage-data-table.clear_filter" | ||||||
|  |                     )} | ||||||
|                   ></ha-icon-button> |                   ></ha-icon-button> | ||||||
|                 </div> |                 </div> | ||||||
|                 <div class="pane-content"> |                 <div class="pane-content"> | ||||||
| @@ -409,39 +460,39 @@ export class HaTabsSubpageDataTable extends LitElement { | |||||||
|               </ha-data-table>`} |               </ha-data-table>`} | ||||||
|         <div slot="fab"><slot name="fab"></slot></div> |         <div slot="fab"><slot name="fab"></slot></div> | ||||||
|       </hass-tabs-subpage> |       </hass-tabs-subpage> | ||||||
|       <md-menu anchor="group-by-anchor" id="group-by-menu" positioning="fixed"> |       <ha-menu anchor="group-by-anchor" id="group-by-menu" positioning="fixed"> | ||||||
|         ${Object.entries(this.columns).map(([id, column]) => |         ${Object.entries(this.columns).map(([id, column]) => | ||||||
|           column.groupable |           column.groupable | ||||||
|             ? html` |             ? html` | ||||||
|                 <md-menu-item |                 <ha-menu-item | ||||||
|                   .value=${id} |                   .value=${id} | ||||||
|                   @click=${this._handleGroupBy} |                   @click=${this._handleGroupBy} | ||||||
|                   .selected=${id === this._groupColumn} |                   .selected=${id === this._groupColumn} | ||||||
|                   class=${classMap({ selected: id === this._groupColumn })} |                   class=${classMap({ selected: id === this._groupColumn })} | ||||||
|                 > |                 > | ||||||
|                   ${column.title || column.label} |                   ${column.title || column.label} | ||||||
|                 </md-menu-item> |                 </ha-menu-item> | ||||||
|               ` |               ` | ||||||
|             : nothing |             : nothing | ||||||
|         )} |         )} | ||||||
|         <li divider role="separator"></li> |         <md-divider role="separator" tabindex="-1"></md-divider> | ||||||
|         <md-menu-item |         <ha-menu-item | ||||||
|           .value=${undefined} |           .value=${undefined} | ||||||
|           @click=${this._handleGroupBy} |           @click=${this._handleGroupBy} | ||||||
|           .selected=${this._groupColumn === undefined} |           .selected=${this._groupColumn === undefined} | ||||||
|           class=${classMap({ selected: this._groupColumn === undefined })} |           class=${classMap({ selected: this._groupColumn === undefined })} | ||||||
|           >${localize( |  | ||||||
|             "ui.components.subpage-data-table.dont_group_by" |  | ||||||
|           )}</md-menu-item |  | ||||||
|         > |         > | ||||||
|       </md-menu> |           ${localize("ui.components.subpage-data-table.dont_group_by")} | ||||||
|       <md-menu anchor="sort-by-anchor" id="sort-by-menu" positioning="fixed"> |         </ha-menu-item> | ||||||
|  |       </ha-menu> | ||||||
|  |       <ha-menu anchor="sort-by-anchor" id="sort-by-menu" positioning="fixed"> | ||||||
|         ${Object.entries(this.columns).map(([id, column]) => |         ${Object.entries(this.columns).map(([id, column]) => | ||||||
|           column.sortable |           column.sortable | ||||||
|             ? html` |             ? html` | ||||||
|                 <md-menu-item |                 <ha-menu-item | ||||||
|                   .value=${id} |                   .value=${id} | ||||||
|                   @click=${this._handleSortBy} |                   @click=${this._handleSortBy} | ||||||
|  |                   keep-open | ||||||
|                   .selected=${id === this._sortColumn} |                   .selected=${id === this._sortColumn} | ||||||
|                   class=${classMap({ selected: id === this._sortColumn })} |                   class=${classMap({ selected: id === this._sortColumn })} | ||||||
|                 > |                 > | ||||||
| @@ -456,11 +507,11 @@ export class HaTabsSubpageDataTable extends LitElement { | |||||||
|                       ` |                       ` | ||||||
|                     : nothing} |                     : nothing} | ||||||
|                   ${column.title || column.label} |                   ${column.title || column.label} | ||||||
|                 </md-menu-item> |                 </ha-menu-item> | ||||||
|               ` |               ` | ||||||
|             : nothing |             : nothing | ||||||
|         )} |         )} | ||||||
|       </md-menu> |       </ha-menu> | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -478,8 +529,6 @@ export class HaTabsSubpageDataTable extends LitElement { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _handleSortBy(ev) { |   private _handleSortBy(ev) { | ||||||
|     ev.stopPropagation(); |  | ||||||
|     ev.preventDefault(); |  | ||||||
|     const columnId = ev.currentTarget.value; |     const columnId = ev.currentTarget.value; | ||||||
|     if (!this._sortDirection || this._sortColumn !== columnId) { |     if (!this._sortDirection || this._sortColumn !== columnId) { | ||||||
|       this._sortDirection = "asc"; |       this._sortDirection = "asc"; | ||||||
| @@ -504,6 +553,14 @@ export class HaTabsSubpageDataTable extends LitElement { | |||||||
|     this._dataTable.clearSelection(); |     this._dataTable.clearSelection(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   private _selectAll() { | ||||||
|  |     this._dataTable.selectAll(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _selectNone() { | ||||||
|  |     this._dataTable.clearSelection(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   private _handleSearchChange(ev: CustomEvent) { |   private _handleSearchChange(ev: CustomEvent) { | ||||||
|     if (this.filter === ev.detail.value) { |     if (this.filter === ev.detail.value) { | ||||||
|       return; |       return; | ||||||
| @@ -637,6 +694,8 @@ export class HaTabsSubpageDataTable extends LitElement { | |||||||
|         position: absolute; |         position: absolute; | ||||||
|         top: -4px; |         top: -4px; | ||||||
|         right: -4px; |         right: -4px; | ||||||
|  |         inset-inline-end: -4px; | ||||||
|  |         inset-inline-start: initial; | ||||||
|         min-width: 16px; |         min-width: 16px; | ||||||
|         box-sizing: border-box; |         box-sizing: border-box; | ||||||
|         border-radius: 50%; |         border-radius: 50%; | ||||||
| @@ -669,21 +728,31 @@ export class HaTabsSubpageDataTable extends LitElement { | |||||||
|         padding: 8px 12px; |         padding: 8px 12px; | ||||||
|         box-sizing: border-box; |         box-sizing: border-box; | ||||||
|         font-size: 14px; |         font-size: 14px; | ||||||
|  |         --ha-assist-chip-container-color: var(--primary-background-color); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .selection-controls { | ||||||
|  |         display: flex; | ||||||
|  |         align-items: center; | ||||||
|  |         gap: 8px; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .selection-controls p { | ||||||
|  |         margin-left: 8px; | ||||||
|  |         margin-inline-start: 8px; | ||||||
|  |         margin-inline-end: initial; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .center-vertical { |       .center-vertical { | ||||||
|         display: flex; |         display: flex; | ||||||
|         align-items: center; |         align-items: center; | ||||||
|  |         gap: 8px; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .relative { |       .relative { | ||||||
|         position: relative; |         position: relative; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       .selection-bar p { |  | ||||||
|         margin-left: 16px; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       ha-assist-chip { |       ha-assist-chip { | ||||||
|         --ha-assist-chip-container-shape: 10px; |         --ha-assist-chip-container-shape: 10px; | ||||||
|       } |       } | ||||||
| @@ -712,23 +781,10 @@ export class HaTabsSubpageDataTable extends LitElement { | |||||||
|         display: flex; |         display: flex; | ||||||
|         flex-direction: column; |         flex-direction: column; | ||||||
|       } |       } | ||||||
|       /* TODO: Migrate to ha-menu and ha-menu-item */ |  | ||||||
|       md-menu { |  | ||||||
|         --md-menu-container-color: var(--card-background-color); |  | ||||||
|       } |  | ||||||
|       md-menu-item { |  | ||||||
|         --md-menu-item-label-text-color: var(--primary-text-color); |  | ||||||
|         --mdc-icon-size: 16px; |  | ||||||
|         --md-menu-item-selected-container-color: rgba( |  | ||||||
|           var(--rgb-primary-color), |  | ||||||
|           0.15 |  | ||||||
|         ); |  | ||||||
|       } |  | ||||||
|       md-menu-item.selected { |  | ||||||
|         --md-menu-item-label-text-color: var(--primary-color); |  | ||||||
|       } |  | ||||||
|       #sort-by-anchor, |       #sort-by-anchor, | ||||||
|       #group-by-anchor { |       #group-by-anchor, | ||||||
|  |       ha-button-menu-new ha-assist-chip { | ||||||
|         --md-assist-chip-trailing-space: 8px; |         --md-assist-chip-trailing-space: 8px; | ||||||
|       } |       } | ||||||
|     `; |     `; | ||||||
|   | |||||||
| @@ -344,6 +344,10 @@ class HassTabsSubpage extends LitElement { | |||||||
|           inset-inline-start: initial; |           inset-inline-start: initial; | ||||||
|           bottom: calc(16px + env(safe-area-inset-bottom)); |           bottom: calc(16px + env(safe-area-inset-bottom)); | ||||||
|           z-index: 1; |           z-index: 1; | ||||||
|  |           display: flex; | ||||||
|  |           flex-wrap: wrap; | ||||||
|  |           justify-content: flex-end; | ||||||
|  |           gap: 8px; | ||||||
|         } |         } | ||||||
|         :host([narrow]) #fab.tabs { |         :host([narrow]) #fab.tabs { | ||||||
|           bottom: calc(84px + env(safe-area-inset-bottom)); |           bottom: calc(84px + env(safe-area-inset-bottom)); | ||||||
|   | |||||||
| @@ -27,7 +27,7 @@ class NotificationManager extends LitElement { | |||||||
|   @query("ha-toast") private _toast!: HaToast | undefined; |   @query("ha-toast") private _toast!: HaToast | undefined; | ||||||
|  |  | ||||||
|   public async showDialog(parameters: ShowToastParams) { |   public async showDialog(parameters: ShowToastParams) { | ||||||
|     if (this._parameters) { |     if (this._parameters && this._parameters.message !== parameters.message) { | ||||||
|       this._parameters = undefined; |       this._parameters = undefined; | ||||||
|       await this.updateComplete; |       await this.updateComplete; | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -52,7 +52,9 @@ class DialogAreaDetail extends LitElement { | |||||||
|   ): Promise<void> { |   ): Promise<void> { | ||||||
|     this._params = params; |     this._params = params; | ||||||
|     this._error = undefined; |     this._error = undefined; | ||||||
|     this._name = this._params.entry ? this._params.entry.name : ""; |     this._name = this._params.entry | ||||||
|  |       ? this._params.entry.name | ||||||
|  |       : this._params.suggestedName || ""; | ||||||
|     this._aliases = this._params.entry ? this._params.entry.aliases : []; |     this._aliases = this._params.entry ? this._params.entry.aliases : []; | ||||||
|     this._labels = this._params.entry ? this._params.entry.labels : []; |     this._labels = this._params.entry ? this._params.entry.labels : []; | ||||||
|     this._picture = this._params.entry?.picture || null; |     this._picture = this._params.entry?.picture || null; | ||||||
|   | |||||||
| @@ -38,7 +38,9 @@ class DialogFloorDetail extends LitElement { | |||||||
|   ): Promise<void> { |   ): Promise<void> { | ||||||
|     this._params = params; |     this._params = params; | ||||||
|     this._error = undefined; |     this._error = undefined; | ||||||
|     this._name = this._params.entry ? this._params.entry.name : ""; |     this._name = this._params.entry | ||||||
|  |       ? this._params.entry.name | ||||||
|  |       : this._params.suggestedName || ""; | ||||||
|     this._aliases = this._params.entry?.aliases || []; |     this._aliases = this._params.entry?.aliases || []; | ||||||
|     this._icon = this._params.entry?.icon || null; |     this._icon = this._params.entry?.icon || null; | ||||||
|     this._level = this._params.entry?.level ?? null; |     this._level = this._params.entry?.level ?? null; | ||||||
| @@ -213,6 +215,9 @@ class DialogFloorDetail extends LitElement { | |||||||
|           display: block; |           display: block; | ||||||
|           margin-bottom: 16px; |           margin-bottom: 16px; | ||||||
|         } |         } | ||||||
|  |         ha-floor-icon { | ||||||
|  |           color: var(--secondary-text-color); | ||||||
|  |         } | ||||||
|       `, |       `, | ||||||
|     ]; |     ]; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -23,9 +23,11 @@ import "../../../components/ha-fab"; | |||||||
| import "../../../components/ha-floor-icon"; | import "../../../components/ha-floor-icon"; | ||||||
| import "../../../components/ha-icon-button"; | import "../../../components/ha-icon-button"; | ||||||
| import "../../../components/ha-svg-icon"; | import "../../../components/ha-svg-icon"; | ||||||
|  | import "../../../components/ha-sortable"; | ||||||
| import { | import { | ||||||
|   AreaRegistryEntry, |   AreaRegistryEntry, | ||||||
|   createAreaRegistryEntry, |   createAreaRegistryEntry, | ||||||
|  |   updateAreaRegistryEntry, | ||||||
| } from "../../../data/area_registry"; | } from "../../../data/area_registry"; | ||||||
| import { | import { | ||||||
|   FloorRegistryEntry, |   FloorRegistryEntry, | ||||||
| @@ -50,6 +52,10 @@ import { | |||||||
| } from "./show-dialog-area-registry-detail"; | } from "./show-dialog-area-registry-detail"; | ||||||
| import { showFloorRegistryDetailDialog } from "./show-dialog-floor-registry-detail"; | import { showFloorRegistryDetailDialog } from "./show-dialog-floor-registry-detail"; | ||||||
|  |  | ||||||
|  | const UNASSIGNED_PATH = ["__unassigned__"]; | ||||||
|  |  | ||||||
|  | const SORT_OPTIONS = { sort: false, delay: 500, delayOnTouchOnly: true }; | ||||||
|  |  | ||||||
| @customElement("ha-config-areas-dashboard") | @customElement("ha-config-areas-dashboard") | ||||||
| export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) { | export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) { | ||||||
|   @property({ attribute: false }) public hass!: HomeAssistant; |   @property({ attribute: false }) public hass!: HomeAssistant; | ||||||
| @@ -187,13 +193,22 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) { | |||||||
|                     > |                     > | ||||||
|                   </ha-button-menu> |                   </ha-button-menu> | ||||||
|                 </div> |                 </div> | ||||||
|                 <div class="areas"> |                 <ha-sortable | ||||||
|                   ${floor.areas.map((area) => this._renderArea(area))} |                   handle-selector="a" | ||||||
|                 </div> |                   draggable-selector="a" | ||||||
|  |                   @item-moved=${this._areaMoved} | ||||||
|  |                   group="floor" | ||||||
|  |                   .options=${SORT_OPTIONS} | ||||||
|  |                   .path=${[floor.floor_id]} | ||||||
|  |                 > | ||||||
|  |                   <div class="areas"> | ||||||
|  |                     ${floor.areas.map((area) => this._renderArea(area))} | ||||||
|  |                   </div> | ||||||
|  |                 </ha-sortable> | ||||||
|               </div>` |               </div>` | ||||||
|           )} |           )} | ||||||
|           ${areasAndFloors?.unassisgnedAreas.length |           ${areasAndFloors?.unassisgnedAreas.length | ||||||
|             ? html`<div class="unassigned"> |             ? html`<div class="floor"> | ||||||
|                 <div class="header"> |                 <div class="header"> | ||||||
|                   <h2> |                   <h2> | ||||||
|                     ${this.hass.localize( |                     ${this.hass.localize( | ||||||
| @@ -201,11 +216,20 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) { | |||||||
|                     )} |                     )} | ||||||
|                   </h2> |                   </h2> | ||||||
|                 </div> |                 </div> | ||||||
|                 <div class="areas"> |                 <ha-sortable | ||||||
|                   ${areasAndFloors?.unassisgnedAreas.map((area) => |                   handle-selector="a" | ||||||
|                     this._renderArea(area) |                   draggable-selector="a" | ||||||
|                   )} |                   @item-moved=${this._areaMoved} | ||||||
|                 </div> |                   group="floor" | ||||||
|  |                   .options=${SORT_OPTIONS} | ||||||
|  |                   .path=${UNASSIGNED_PATH} | ||||||
|  |                 > | ||||||
|  |                   <div class="areas"> | ||||||
|  |                     ${areasAndFloors?.unassisgnedAreas.map((area) => | ||||||
|  |                       this._renderArea(area) | ||||||
|  |                     )} | ||||||
|  |                   </div> | ||||||
|  |                 </ha-sortable> | ||||||
|               </div>` |               </div>` | ||||||
|             : nothing} |             : nothing} | ||||||
|         </div> |         </div> | ||||||
| @@ -281,6 +305,29 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) { | |||||||
|     loadAreaRegistryDetailDialog(); |     loadAreaRegistryDetailDialog(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   private async _areaMoved(ev) { | ||||||
|  |     const areasAndFloors = this._processAreas( | ||||||
|  |       this.hass.areas, | ||||||
|  |       this.hass.devices, | ||||||
|  |       this.hass.entities, | ||||||
|  |       this._floors! | ||||||
|  |     ); | ||||||
|  |     let area: AreaRegistryEntry; | ||||||
|  |     if (ev.detail.oldPath === UNASSIGNED_PATH) { | ||||||
|  |       area = areasAndFloors.unassisgnedAreas[ev.detail.oldIndex]; | ||||||
|  |     } else { | ||||||
|  |       const oldFloor = areasAndFloors.floors!.find( | ||||||
|  |         (floor) => floor.floor_id === ev.detail.oldPath[0] | ||||||
|  |       ); | ||||||
|  |       area = oldFloor!.areas[ev.detail.oldIndex]; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     await updateAreaRegistryEntry(this.hass, area.area_id, { | ||||||
|  |       floor_id: | ||||||
|  |         ev.detail.newPath === UNASSIGNED_PATH ? null : ev.detail.newPath[0], | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   private _handleFloorAction(ev: CustomEvent<ActionDetail>) { |   private _handleFloorAction(ev: CustomEvent<ActionDetail>) { | ||||||
|     const floor = (ev.currentTarget as any).floor; |     const floor = (ev.currentTarget as any).floor; | ||||||
|     switch (ev.detail.index) { |     switch (ev.detail.index) { | ||||||
| @@ -424,7 +471,6 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) { | |||||||
|       } |       } | ||||||
|       .floor { |       .floor { | ||||||
|         --primary-color: var(--secondary-text-color); |         --primary-color: var(--secondary-text-color); | ||||||
|         margin-inline-end: 8px; |  | ||||||
|       } |       } | ||||||
|       .warning { |       .warning { | ||||||
|         color: var(--error-color); |         color: var(--error-color); | ||||||
|   | |||||||
| @@ -6,6 +6,7 @@ import { | |||||||
|  |  | ||||||
| export interface AreaRegistryDetailDialogParams { | export interface AreaRegistryDetailDialogParams { | ||||||
|   entry?: AreaRegistryEntry; |   entry?: AreaRegistryEntry; | ||||||
|  |   suggestedName?: string; | ||||||
|   createEntry?: (values: AreaRegistryEntryMutableParams) => Promise<unknown>; |   createEntry?: (values: AreaRegistryEntryMutableParams) => Promise<unknown>; | ||||||
|   updateEntry?: ( |   updateEntry?: ( | ||||||
|     updates: Partial<AreaRegistryEntryMutableParams> |     updates: Partial<AreaRegistryEntryMutableParams> | ||||||
|   | |||||||
| @@ -6,6 +6,7 @@ import { | |||||||
|  |  | ||||||
| export interface FloorRegistryDetailDialogParams { | export interface FloorRegistryDetailDialogParams { | ||||||
|   entry?: FloorRegistryEntry; |   entry?: FloorRegistryEntry; | ||||||
|  |   suggestedName?: string; | ||||||
|   createEntry?: (values: FloorRegistryEntryMutableParams) => Promise<unknown>; |   createEntry?: (values: FloorRegistryEntryMutableParams) => Promise<unknown>; | ||||||
|   updateEntry?: ( |   updateEntry?: ( | ||||||
|     updates: Partial<FloorRegistryEntryMutableParams> |     updates: Partial<FloorRegistryEntryMutableParams> | ||||||
|   | |||||||
| @@ -556,7 +556,7 @@ class DialogAddAutomationElement extends LitElement implements HassDialog { | |||||||
|                   ></ha-svg-icon |                   ></ha-svg-icon | ||||||
|                   ><ha-svg-icon slot="end" .path=${mdiPlus}></ha-svg-icon> |                   ><ha-svg-icon slot="end" .path=${mdiPlus}></ha-svg-icon> | ||||||
|                 </ha-list-item-new> |                 </ha-list-item-new> | ||||||
|                 <md-divider></md-divider>` |                 <md-divider role="separator" tabindex="-1"></md-divider>` | ||||||
|             : ""} |             : ""} | ||||||
|           ${repeat( |           ${repeat( | ||||||
|             items, |             items, | ||||||
|   | |||||||
| @@ -1,16 +1,21 @@ | |||||||
| import { consume } from "@lit-labs/context"; | import { consume } from "@lit-labs/context"; | ||||||
| import "@lrnwebcomponents/simple-tooltip/simple-tooltip"; | import "@lrnwebcomponents/simple-tooltip/simple-tooltip"; | ||||||
|  | import "@material/web/divider/divider"; | ||||||
| import { | import { | ||||||
|  |   mdiChevronRight, | ||||||
|  |   mdiCog, | ||||||
|   mdiContentDuplicate, |   mdiContentDuplicate, | ||||||
|   mdiDelete, |   mdiDelete, | ||||||
|  |   mdiDotsVertical, | ||||||
|   mdiHelpCircle, |   mdiHelpCircle, | ||||||
|   mdiInformationOutline, |   mdiInformationOutline, | ||||||
|  |   mdiMenuDown, | ||||||
|   mdiPlay, |   mdiPlay, | ||||||
|   mdiPlayCircleOutline, |  | ||||||
|   mdiPlus, |   mdiPlus, | ||||||
|   mdiRobotHappy, |   mdiRobotHappy, | ||||||
|   mdiStopCircleOutline, |  | ||||||
|   mdiTag, |   mdiTag, | ||||||
|  |   mdiToggleSwitch, | ||||||
|  |   mdiToggleSwitchOffOutline, | ||||||
|   mdiTransitConnection, |   mdiTransitConnection, | ||||||
| } from "@mdi/js"; | } from "@mdi/js"; | ||||||
| import { differenceInDays } from "date-fns/esm"; | import { differenceInDays } from "date-fns/esm"; | ||||||
| @@ -24,9 +29,10 @@ import { | |||||||
|   html, |   html, | ||||||
|   nothing, |   nothing, | ||||||
| } from "lit"; | } from "lit"; | ||||||
| import { customElement, property, state } from "lit/decorators"; | import { customElement, property, query, state } from "lit/decorators"; | ||||||
| import { styleMap } from "lit/directives/style-map"; | import { styleMap } from "lit/directives/style-map"; | ||||||
| import memoizeOne from "memoize-one"; | import memoizeOne from "memoize-one"; | ||||||
|  | import { computeCssColor } from "../../../common/color/compute-color"; | ||||||
| import { isComponentLoaded } from "../../../common/config/is_component_loaded"; | import { isComponentLoaded } from "../../../common/config/is_component_loaded"; | ||||||
| import { formatShortDateTime } from "../../../common/datetime/format_date_time"; | import { formatShortDateTime } from "../../../common/datetime/format_date_time"; | ||||||
| import { relativeTime } from "../../../common/datetime/relative_time"; | import { relativeTime } from "../../../common/datetime/relative_time"; | ||||||
| @@ -38,6 +44,7 @@ import "../../../components/chips/ha-assist-chip"; | |||||||
| import type { | import type { | ||||||
|   DataTableColumnContainer, |   DataTableColumnContainer, | ||||||
|   RowClickedEvent, |   RowClickedEvent, | ||||||
|  |   SelectionChangedEvent, | ||||||
| } from "../../../components/data-table/ha-data-table"; | } from "../../../components/data-table/ha-data-table"; | ||||||
| import "../../../components/data-table/ha-data-table-labels"; | import "../../../components/data-table/ha-data-table-labels"; | ||||||
| import "../../../components/entity/ha-entity-toggle"; | import "../../../components/entity/ha-entity-toggle"; | ||||||
| @@ -50,6 +57,10 @@ import "../../../components/ha-filter-floor-areas"; | |||||||
| import "../../../components/ha-filter-labels"; | import "../../../components/ha-filter-labels"; | ||||||
| import "../../../components/ha-icon-button"; | import "../../../components/ha-icon-button"; | ||||||
| import "../../../components/ha-icon-overflow-menu"; | import "../../../components/ha-icon-overflow-menu"; | ||||||
|  | import "../../../components/ha-menu"; | ||||||
|  | import type { HaMenu } from "../../../components/ha-menu"; | ||||||
|  | import "../../../components/ha-menu-item"; | ||||||
|  | import "../../../components/ha-sub-menu"; | ||||||
| import "../../../components/ha-svg-icon"; | import "../../../components/ha-svg-icon"; | ||||||
| import { | import { | ||||||
|   AutomationEntity, |   AutomationEntity, | ||||||
| @@ -66,7 +77,11 @@ import { | |||||||
| } from "../../../data/category_registry"; | } from "../../../data/category_registry"; | ||||||
| import { fullEntitiesContext } from "../../../data/context"; | import { fullEntitiesContext } from "../../../data/context"; | ||||||
| import { UNAVAILABLE } from "../../../data/entity"; | import { UNAVAILABLE } from "../../../data/entity"; | ||||||
| import { EntityRegistryEntry } from "../../../data/entity_registry"; | import { | ||||||
|  |   EntityRegistryEntry, | ||||||
|  |   UpdateEntityRegistryEntryResult, | ||||||
|  |   updateEntityRegistryEntry, | ||||||
|  | } from "../../../data/entity_registry"; | ||||||
| import { | import { | ||||||
|   LabelRegistryEntry, |   LabelRegistryEntry, | ||||||
|   subscribeLabelRegistry, |   subscribeLabelRegistry, | ||||||
| @@ -79,8 +94,9 @@ import { | |||||||
| import "../../../layouts/hass-tabs-subpage-data-table"; | import "../../../layouts/hass-tabs-subpage-data-table"; | ||||||
| import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; | import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; | ||||||
| import { haStyle } from "../../../resources/styles"; | import { haStyle } from "../../../resources/styles"; | ||||||
| import { HomeAssistant, Route } from "../../../types"; | import { HomeAssistant, Route, ServiceCallResponse } from "../../../types"; | ||||||
| import { documentationUrl } from "../../../util/documentation-url"; | 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 { showAssignCategoryDialog } from "../category/show-dialog-assign-category"; | ||||||
| import { configSections } from "../ha-panel-config"; | import { configSections } from "../ha-panel-config"; | ||||||
| import { showNewAutomationDialog } from "./show-dialog-new-automation"; | import { showNewAutomationDialog } from "./show-dialog-new-automation"; | ||||||
| @@ -116,6 +132,8 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { | |||||||
|  |  | ||||||
|   @state() private _expandedFilter?: string; |   @state() private _expandedFilter?: string; | ||||||
|  |  | ||||||
|  |   @state() private _selected: string[] = []; | ||||||
|  |  | ||||||
|   @state() |   @state() | ||||||
|   _categories!: CategoryRegistryEntry[]; |   _categories!: CategoryRegistryEntry[]; | ||||||
|  |  | ||||||
| @@ -126,6 +144,10 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { | |||||||
|   @consume({ context: fullEntitiesContext, subscribe: true }) |   @consume({ context: fullEntitiesContext, subscribe: true }) | ||||||
|   _entityReg!: EntityRegistryEntry[]; |   _entityReg!: EntityRegistryEntry[]; | ||||||
|  |  | ||||||
|  |   @state() private _overflowAutomation?: AutomationItem; | ||||||
|  |  | ||||||
|  |   @query("#overflow-menu") private _overflowMenu!: HaMenu; | ||||||
|  |  | ||||||
|   private _automations = memoizeOne( |   private _automations = memoizeOne( | ||||||
|     ( |     ( | ||||||
|       automations: AutomationEntity[], |       automations: AutomationEntity[], | ||||||
| @@ -274,82 +296,33 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { | |||||||
|       columns.actions = { |       columns.actions = { | ||||||
|         title: "", |         title: "", | ||||||
|         width: "64px", |         width: "64px", | ||||||
|         type: "overflow-menu", |         type: "icon-button", | ||||||
|         template: (automation) => html` |         template: (automation) => html` | ||||||
|           <ha-icon-overflow-menu |           <ha-icon-button | ||||||
|             .hass=${this.hass} |             .automation=${automation} | ||||||
|             narrow |             .label=${this.hass.localize("ui.common.overflow_menu")} | ||||||
|             .items=${[ |             .path=${mdiDotsVertical} | ||||||
|               { |             @click=${this._showOverflowMenu} | ||||||
|                 path: mdiInformationOutline, |           ></ha-icon-button> | ||||||
|                 label: this.hass.localize( |  | ||||||
|                   "ui.panel.config.automation.editor.show_info" |  | ||||||
|                 ), |  | ||||||
|                 action: () => this._showInfo(automation), |  | ||||||
|               }, |  | ||||||
|               { |  | ||||||
|                 path: mdiTag, |  | ||||||
|                 label: this.hass.localize( |  | ||||||
|                   `ui.panel.config.automation.picker.${automation.category ? "edit_category" : "assign_category"}` |  | ||||||
|                 ), |  | ||||||
|                 action: () => this._editCategory(automation), |  | ||||||
|               }, |  | ||||||
|               { |  | ||||||
|                 path: mdiPlay, |  | ||||||
|                 label: this.hass.localize( |  | ||||||
|                   "ui.panel.config.automation.editor.run" |  | ||||||
|                 ), |  | ||||||
|                 action: () => this._runActions(automation), |  | ||||||
|               }, |  | ||||||
|               { |  | ||||||
|                 path: mdiTransitConnection, |  | ||||||
|                 label: this.hass.localize( |  | ||||||
|                   "ui.panel.config.automation.editor.show_trace" |  | ||||||
|                 ), |  | ||||||
|                 action: () => this._showTrace(automation), |  | ||||||
|               }, |  | ||||||
|               { |  | ||||||
|                 divider: true, |  | ||||||
|               }, |  | ||||||
|               { |  | ||||||
|                 path: mdiContentDuplicate, |  | ||||||
|                 label: this.hass.localize( |  | ||||||
|                   "ui.panel.config.automation.picker.duplicate" |  | ||||||
|                 ), |  | ||||||
|                 action: () => this.duplicate(automation), |  | ||||||
|               }, |  | ||||||
|               { |  | ||||||
|                 path: |  | ||||||
|                   automation.state === "off" |  | ||||||
|                     ? mdiPlayCircleOutline |  | ||||||
|                     : mdiStopCircleOutline, |  | ||||||
|                 label: |  | ||||||
|                   automation.state === "off" |  | ||||||
|                     ? this.hass.localize( |  | ||||||
|                         "ui.panel.config.automation.editor.enable" |  | ||||||
|                       ) |  | ||||||
|                     : this.hass.localize( |  | ||||||
|                         "ui.panel.config.automation.editor.disable" |  | ||||||
|                       ), |  | ||||||
|                 action: () => this._toggle(automation), |  | ||||||
|               }, |  | ||||||
|               { |  | ||||||
|                 label: this.hass.localize( |  | ||||||
|                   "ui.panel.config.automation.picker.delete" |  | ||||||
|                 ), |  | ||||||
|                 path: mdiDelete, |  | ||||||
|                 action: () => this._deleteConfirm(automation), |  | ||||||
|                 warning: true, |  | ||||||
|               }, |  | ||||||
|             ]} |  | ||||||
|           > |  | ||||||
|           </ha-icon-overflow-menu> |  | ||||||
|         `, |         `, | ||||||
|       }; |       }; | ||||||
|       return columns; |       return columns; | ||||||
|     } |     } | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
|  |   private _showOverflowMenu = (ev) => { | ||||||
|  |     if ( | ||||||
|  |       this._overflowMenu.open && | ||||||
|  |       ev.target === this._overflowMenu.anchorElement | ||||||
|  |     ) { | ||||||
|  |       this._overflowMenu.close(); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     this._overflowAutomation = ev.target.automation; | ||||||
|  |     this._overflowMenu.anchorElement = ev.target; | ||||||
|  |     this._overflowMenu.show(); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|   protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] { |   protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] { | ||||||
|     return [ |     return [ | ||||||
|       subscribeCategoryRegistry( |       subscribeCategoryRegistry( | ||||||
| @@ -366,18 +339,58 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   protected render(): TemplateResult { |   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>`; | ||||||
|  |     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` |     return html` | ||||||
|       <hass-tabs-subpage-data-table |       <hass-tabs-subpage-data-table | ||||||
|         .hass=${this.hass} |         .hass=${this.hass} | ||||||
|         .narrow=${this.narrow} |         .narrow=${this.narrow} | ||||||
|         back-path="/config" |         .backPath=${ | ||||||
|  |           this._searchParms.has("historyBack") ? undefined : "/config" | ||||||
|  |         } | ||||||
|         id="entity_id" |         id="entity_id" | ||||||
|         .route=${this.route} |         .route=${this.route} | ||||||
|         .tabs=${configSections.automations} |         .tabs=${configSections.automations} | ||||||
|  |         selectable | ||||||
|  |         .selected=${this._selected.length} | ||||||
|  |         @selection-changed=${this._handleSelectionChanged} | ||||||
|         hasFilters |         hasFilters | ||||||
|         .filters=${Object.values(this._filters).filter( |         .filters=${ | ||||||
|           (filter) => filter.value?.length |           Object.values(this._filters).filter((filter) => filter.value?.length) | ||||||
|         ).length} |             .length | ||||||
|  |         } | ||||||
|         .columns=${this._columns( |         .columns=${this._columns( | ||||||
|           this.narrow, |           this.narrow, | ||||||
|           this.hass.localize, |           this.hass.localize, | ||||||
| @@ -466,36 +479,156 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { | |||||||
|           .narrow=${this.narrow} |           .narrow=${this.narrow} | ||||||
|           @expanded-changed=${this._filterExpanded} |           @expanded-changed=${this._filterExpanded} | ||||||
|         ></ha-filter-blueprints> |         ></ha-filter-blueprints> | ||||||
|         ${!this.automations.length |           ${ | ||||||
|           ? html`<div class="empty" slot="empty"> |             !this.narrow | ||||||
|               <ha-svg-icon .path=${mdiRobotHappy}></ha-svg-icon> |               ? html`<ha-button-menu-new slot="selection-bar"> | ||||||
|               <h1> |                     <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> | ||||||
|  |                   ${this.hass.dockedSidebar === "docked" | ||||||
|  |                     ? 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 | ||||||
|  |           } | ||||||
|  |           <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-menu-item @click=${this._handleBulkEnable}> | ||||||
|  |               <ha-svg-icon slot="start" .path=${mdiToggleSwitch}></ha-svg-icon> | ||||||
|  |               <div slot="headline"> | ||||||
|                 ${this.hass.localize( |                 ${this.hass.localize( | ||||||
|                   "ui.panel.config.automation.picker.empty_header" |                   "ui.panel.config.automation.picker.bulk_actions.enable" | ||||||
|                 )} |                 )} | ||||||
|               </h1> |               </div> | ||||||
|               <p> |             </ha-menu-item> | ||||||
|  |             <ha-menu-item @click=${this._handleBulkDisable}> | ||||||
|  |               <ha-svg-icon | ||||||
|  |                 slot="start" | ||||||
|  |                 .path=${mdiToggleSwitchOffOutline} | ||||||
|  |               ></ha-svg-icon> | ||||||
|  |               <div slot="headline"> | ||||||
|                 ${this.hass.localize( |                 ${this.hass.localize( | ||||||
|                   "ui.panel.config.automation.picker.empty_text_1" |                   "ui.panel.config.automation.picker.bulk_actions.disable" | ||||||
|                 )} |                 )} | ||||||
|               </p> |               </div> | ||||||
|               <p> |             </ha-menu-item> | ||||||
|                 ${this.hass.localize( |           </ha-button-menu-new> | ||||||
|                   "ui.panel.config.automation.picker.empty_text_2", |         ${ | ||||||
|                   { user: this.hass.user?.name || "Alice" } |           !this.automations.length | ||||||
|                 )} |             ? html`<div class="empty" slot="empty"> | ||||||
|               </p> |                 <ha-svg-icon .path=${mdiRobotHappy}></ha-svg-icon> | ||||||
|               <a |                 <h1> | ||||||
|                 href=${documentationUrl(this.hass, "/docs/automation/editor/")} |                   ${this.hass.localize( | ||||||
|                 target="_blank" |                     "ui.panel.config.automation.picker.empty_header" | ||||||
|                 rel="noreferrer" |                   )} | ||||||
|               > |                 </h1> | ||||||
|                 <ha-button> |                 <p> | ||||||
|                   ${this.hass.localize("ui.panel.config.common.learn_more")} |                   ${this.hass.localize( | ||||||
|                 </ha-button> |                     "ui.panel.config.automation.picker.empty_text_1" | ||||||
|               </a> |                   )} | ||||||
|             </div>` |                 </p> | ||||||
|           : nothing} |                 <p> | ||||||
|  |                   ${this.hass.localize( | ||||||
|  |                     "ui.panel.config.automation.picker.empty_text_2", | ||||||
|  |                     { user: this.hass.user?.name || "Alice" } | ||||||
|  |                   )} | ||||||
|  |                 </p> | ||||||
|  |                 <a | ||||||
|  |                   href=${documentationUrl( | ||||||
|  |                     this.hass, | ||||||
|  |                     "/docs/automation/editor/" | ||||||
|  |                   )} | ||||||
|  |                   target="_blank" | ||||||
|  |                   rel="noreferrer" | ||||||
|  |                 > | ||||||
|  |                   <ha-button> | ||||||
|  |                     ${this.hass.localize("ui.panel.config.common.learn_more")} | ||||||
|  |                   </ha-button> | ||||||
|  |                 </a> | ||||||
|  |               </div>` | ||||||
|  |             : nothing | ||||||
|  |         } | ||||||
|         <ha-fab |         <ha-fab | ||||||
|           slot="fab" |           slot="fab" | ||||||
|           .label=${this.hass.localize( |           .label=${this.hass.localize( | ||||||
| @@ -507,6 +640,80 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { | |||||||
|           <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> |           <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> | ||||||
|         </ha-fab> |         </ha-fab> | ||||||
|       </hass-tabs-subpage-data-table> |       </hass-tabs-subpage-data-table> | ||||||
|  |       <ha-menu id="overflow-menu" positioning="fixed"> | ||||||
|  |         <ha-menu-item @click=${this._showInfo}> | ||||||
|  |           <ha-svg-icon | ||||||
|  |             .path=${mdiInformationOutline} | ||||||
|  |             slot="start" | ||||||
|  |           ></ha-svg-icon> | ||||||
|  |           <div slot="headline"> | ||||||
|  |             ${this.hass.localize("ui.panel.config.automation.editor.show_info")} | ||||||
|  |           </div> | ||||||
|  |         </ha-menu-item> | ||||||
|  |  | ||||||
|  |         <ha-menu-item @click=${this._showSettings}> | ||||||
|  |           <ha-svg-icon .path=${mdiCog} slot="start"></ha-svg-icon> | ||||||
|  |           <div slot="headline"> | ||||||
|  |             ${this.hass.localize( | ||||||
|  |               "ui.panel.config.automation.picker.show_settings" | ||||||
|  |             )} | ||||||
|  |           </div> | ||||||
|  |         </ha-menu-item> | ||||||
|  |         <ha-menu-item @click=${this._editCategory}> | ||||||
|  |           <ha-svg-icon .path=${mdiTag} slot="start"></ha-svg-icon> | ||||||
|  |           <div slot="headline"> | ||||||
|  |             ${this.hass.localize( | ||||||
|  |               `ui.panel.config.automation.picker.${this._overflowAutomation?.category ? "edit_category" : "assign_category"}` | ||||||
|  |             )} | ||||||
|  |           </div> | ||||||
|  |         </ha-menu-item> | ||||||
|  |         <ha-menu-item @click=${this._runActions}> | ||||||
|  |           <ha-svg-icon .path=${mdiPlay} slot="start"></ha-svg-icon> | ||||||
|  |           <div slot="headline"> | ||||||
|  |             ${this.hass.localize("ui.panel.config.automation.editor.run")} | ||||||
|  |           </div> | ||||||
|  |         </ha-menu-item> | ||||||
|  |         <ha-menu-item @click=${this._showTrace}> | ||||||
|  |           <ha-svg-icon .path=${mdiTransitConnection} slot="start"></ha-svg-icon> | ||||||
|  |           <div slot="headline"> | ||||||
|  |             ${this.hass.localize( | ||||||
|  |               "ui.panel.config.automation.editor.show_trace" | ||||||
|  |             )} | ||||||
|  |           </div> | ||||||
|  |         </ha-menu-item> | ||||||
|  |         <md-divider role="separator" tabindex="-1"></md-divider> | ||||||
|  |         <ha-menu-item @click=${this._duplicate}> | ||||||
|  |           <ha-svg-icon .path=${mdiContentDuplicate} slot="start"></ha-svg-icon> | ||||||
|  |           <div slot="headline"> | ||||||
|  |             ${this.hass.localize("ui.panel.config.automation.picker.duplicate")} | ||||||
|  |           </div> | ||||||
|  |         </ha-menu-item> | ||||||
|  |         <ha-menu-item @click=${this._toggle}> | ||||||
|  |           <ha-svg-icon | ||||||
|  |             .path=${ | ||||||
|  |               this._overflowAutomation?.state === "off" | ||||||
|  |                 ? mdiToggleSwitch | ||||||
|  |                 : mdiToggleSwitchOffOutline | ||||||
|  |             } | ||||||
|  |             slot="start" | ||||||
|  |           ></ha-svg-icon> | ||||||
|  |           <div slot="headline"> | ||||||
|  |             ${ | ||||||
|  |               this._overflowAutomation?.state === "off" | ||||||
|  |                 ? this.hass.localize("ui.panel.config.automation.editor.enable") | ||||||
|  |                 : this.hass.localize( | ||||||
|  |                     "ui.panel.config.automation.editor.disable" | ||||||
|  |                   ) | ||||||
|  |             } | ||||||
|  |           </div> | ||||||
|  |         </ha-menu-item> | ||||||
|  |         <ha-menu-item @click=${this._deleteConfirm} class="warning"> | ||||||
|  |           <ha-svg-icon .path=${mdiDelete} slot="start"></ha-svg-icon> | ||||||
|  |           <div slot="headline"> | ||||||
|  |             ${this.hass.localize("ui.panel.config.automation.picker.delete")} | ||||||
|  |           </div> | ||||||
|  |         </ha-menu-item> | ||||||
|  |       </ha-menu> | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -521,6 +728,9 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { | |||||||
|     if (this._searchParms.has("blueprint")) { |     if (this._searchParms.has("blueprint")) { | ||||||
|       this._filterBlueprint(); |       this._filterBlueprint(); | ||||||
|     } |     } | ||||||
|  |     if (this._searchParms.has("label")) { | ||||||
|  |       this._filterLabel(); | ||||||
|  |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _filterExpanded(ev) { |   private _filterExpanded(ev) { | ||||||
| @@ -608,6 +818,21 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { | |||||||
|     this._filteredAutomations = items ? [...items] : undefined; |     this._filteredAutomations = items ? [...items] : undefined; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   private _filterLabel() { | ||||||
|  |     const label = this._searchParms.get("label"); | ||||||
|  |     if (!label) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     this._filters = { | ||||||
|  |       ...this._filters, | ||||||
|  |       "ha-filter-labels": { | ||||||
|  |         value: [label], | ||||||
|  |         items: undefined, | ||||||
|  |       }, | ||||||
|  |     }; | ||||||
|  |     this._applyFilters(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   private async _filterBlueprint() { |   private async _filterBlueprint() { | ||||||
|     const blueprint = this._searchParms.get("blueprint"); |     const blueprint = this._searchParms.get("blueprint"); | ||||||
|     if (!blueprint) { |     if (!blueprint) { | ||||||
| @@ -633,15 +858,29 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { | |||||||
|     this._applyFilters(); |     this._applyFilters(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _showInfo(automation: any) { |   private _showInfo(ev) { | ||||||
|  |     const automation = ev.currentTarget.parentElement.anchorElement.automation; | ||||||
|     fireEvent(this, "hass-more-info", { entityId: automation.entity_id }); |     fireEvent(this, "hass-more-info", { entityId: automation.entity_id }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _runActions(automation: any) { |   private _showSettings(ev) { | ||||||
|  |     const automation = ev.currentTarget.parentElement.anchorElement.automation; | ||||||
|  |  | ||||||
|  |     fireEvent(this, "hass-more-info", { | ||||||
|  |       entityId: automation.entity_id, | ||||||
|  |       view: "settings", | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _runActions(ev) { | ||||||
|  |     const automation = ev.currentTarget.parentElement.anchorElement.automation; | ||||||
|  |  | ||||||
|     triggerAutomationActions(this.hass, automation.entity_id); |     triggerAutomationActions(this.hass, automation.entity_id); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _editCategory(automation: any) { |   private _editCategory(ev) { | ||||||
|  |     const automation = ev.currentTarget.parentElement.anchorElement.automation; | ||||||
|  |  | ||||||
|     const entityReg = this._entityReg.find( |     const entityReg = this._entityReg.find( | ||||||
|       (reg) => reg.entity_id === automation.entity_id |       (reg) => reg.entity_id === automation.entity_id | ||||||
|     ); |     ); | ||||||
| @@ -662,7 +901,9 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { | |||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _showTrace(automation: any) { |   private _showTrace(ev) { | ||||||
|  |     const automation = ev.currentTarget.parentElement.anchorElement.automation; | ||||||
|  |  | ||||||
|     if (!automation.attributes.id) { |     if (!automation.attributes.id) { | ||||||
|       showAlertDialog(this, { |       showAlertDialog(this, { | ||||||
|         text: this.hass.localize( |         text: this.hass.localize( | ||||||
| @@ -676,14 +917,18 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private async _toggle(automation): Promise<void> { |   private async _toggle(ev): Promise<void> { | ||||||
|  |     const automation = ev.currentTarget.parentElement.anchorElement.automation; | ||||||
|  |  | ||||||
|     const service = automation.state === "off" ? "turn_on" : "turn_off"; |     const service = automation.state === "off" ? "turn_on" : "turn_off"; | ||||||
|     await this.hass.callService("automation", service, { |     await this.hass.callService("automation", service, { | ||||||
|       entity_id: automation.entity_id, |       entity_id: automation.entity_id, | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private async _deleteConfirm(automation) { |   private async _deleteConfirm(ev) { | ||||||
|  |     const automation = ev.currentTarget.parentElement.anchorElement.automation; | ||||||
|  |  | ||||||
|     showConfirmationDialog(this, { |     showConfirmationDialog(this, { | ||||||
|       title: this.hass.localize( |       title: this.hass.localize( | ||||||
|         "ui.panel.config.automation.picker.delete_confirm_title" |         "ui.panel.config.automation.picker.delete_confirm_title" | ||||||
| @@ -717,7 +962,9 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private async duplicate(automation) { |   private async _duplicate(ev) { | ||||||
|  |     const automation = ev.currentTarget.parentElement.anchorElement.automation; | ||||||
|  |  | ||||||
|     try { |     try { | ||||||
|       const config = await fetchAutomationFileConfig( |       const config = await fetchAutomationFileConfig( | ||||||
|         this.hass, |         this.hass, | ||||||
| @@ -776,6 +1023,12 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   private _handleSelectionChanged( | ||||||
|  |     ev: HASSDomEvent<SelectionChangedEvent> | ||||||
|  |   ): void { | ||||||
|  |     this._selected = ev.detail.value; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   private _createNew() { |   private _createNew() { | ||||||
|     if (isComponentLoaded(this.hass, "blueprint")) { |     if (isComponentLoaded(this.hass, "blueprint")) { | ||||||
|       showNewAutomationDialog(this, { mode: "automation" }); |       showNewAutomationDialog(this, { mode: "automation" }); | ||||||
| @@ -784,6 +1037,48 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   private async _handleBulkCategory(ev) { | ||||||
|  |     const category = ev.currentTarget.value; | ||||||
|  |     const promises: Promise<UpdateEntityRegistryEntryResult>[] = []; | ||||||
|  |     this._selected.forEach((entityId) => { | ||||||
|  |       promises.push( | ||||||
|  |         updateEntityRegistryEntry(this.hass, entityId, { | ||||||
|  |           categories: { automation: category }, | ||||||
|  |         }) | ||||||
|  |       ); | ||||||
|  |     }); | ||||||
|  |     await Promise.all(promises); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private async _handleBulkLabel(ev) { | ||||||
|  |     const label = ev.currentTarget.value; | ||||||
|  |     const promises: Promise<UpdateEntityRegistryEntryResult>[] = []; | ||||||
|  |     this._selected.forEach((entityId) => { | ||||||
|  |       promises.push( | ||||||
|  |         updateEntityRegistryEntry(this.hass, entityId, { | ||||||
|  |           labels: this.hass.entities[entityId].labels.concat(label), | ||||||
|  |         }) | ||||||
|  |       ); | ||||||
|  |     }); | ||||||
|  |     await Promise.all(promises); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private async _handleBulkEnable() { | ||||||
|  |     const promises: Promise<ServiceCallResponse>[] = []; | ||||||
|  |     this._selected.forEach((entityId) => { | ||||||
|  |       promises.push(turnOnOffEntity(this.hass, entityId, true)); | ||||||
|  |     }); | ||||||
|  |     await Promise.all(promises); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private async _handleBulkDisable() { | ||||||
|  |     const promises: Promise<ServiceCallResponse>[] = []; | ||||||
|  |     this._selected.forEach((entityId) => { | ||||||
|  |       promises.push(turnOnOffEntity(this.hass, entityId, false)); | ||||||
|  |     }); | ||||||
|  |     await Promise.all(promises); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   static get styles(): CSSResultGroup { |   static get styles(): CSSResultGroup { | ||||||
|     return [ |     return [ | ||||||
|       haStyle, |       haStyle, | ||||||
| @@ -799,6 +1094,16 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { | |||||||
|           --mdc-icon-size: 80px; |           --mdc-icon-size: 80px; | ||||||
|           max-width: 500px; |           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; | ||||||
|  |         } | ||||||
|       `, |       `, | ||||||
|     ]; |     ]; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -188,9 +188,7 @@ export class HaCategoryPicker extends SubscribeMixin(LitElement) { | |||||||
|         this.comboBox.filteredItems = [ |         this.comboBox.filteredItems = [ | ||||||
|           { |           { | ||||||
|             category_id: NO_CATEGORIES_ID, |             category_id: NO_CATEGORIES_ID, | ||||||
|             name: this.hass.localize( |             name: this.hass.localize("ui.components.category-picker.no_match"), | ||||||
|               "ui.components.category-picker.no_categories" |  | ||||||
|             ), |  | ||||||
|             icon: null, |             icon: null, | ||||||
|           }, |           }, | ||||||
|         ] as ScorableCategoryRegistryEntry[]; |         ] as ScorableCategoryRegistryEntry[]; | ||||||
|   | |||||||
| @@ -185,6 +185,23 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) { | |||||||
|         }, |         }, | ||||||
|       }; |       }; | ||||||
|     } |     } | ||||||
|  |     if (this._searchParms.has("label")) { | ||||||
|  |       this._filterLabel(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _filterLabel() { | ||||||
|  |     const label = this._searchParms.get("label"); | ||||||
|  |     if (!label) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     this._filters = { | ||||||
|  |       ...this._filters, | ||||||
|  |       "ha-filter-labels": { | ||||||
|  |         value: [label], | ||||||
|  |         items: undefined, | ||||||
|  |       }, | ||||||
|  |     }; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _clearFilter() { |   private _clearFilter() { | ||||||
|   | |||||||
| @@ -527,11 +527,11 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { | |||||||
|         .filters=${Object.values(this._filters).filter( |         .filters=${Object.values(this._filters).filter( | ||||||
|           (filter) => filter.value?.length |           (filter) => filter.value?.length | ||||||
|         ).length} |         ).length} | ||||||
|         .selected=${this._selectedEntities.length} |  | ||||||
|         .filter=${this._filter} |         .filter=${this._filter} | ||||||
|         selectable |         selectable | ||||||
|         clickable |         .selected=${this._selectedEntities.length} | ||||||
|         @selection-changed=${this._handleSelectionChanged} |         @selection-changed=${this._handleSelectionChanged} | ||||||
|  |         clickable | ||||||
|         @clear-filter=${this._clearFilter} |         @clear-filter=${this._clearFilter} | ||||||
|         @search-changed=${this._handleSearchChange} |         @search-changed=${this._handleSearchChange} | ||||||
|         @row-click=${this._openEditEntry} |         @row-click=${this._openEditEntry} | ||||||
| @@ -758,6 +758,23 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { | |||||||
|         }, |         }, | ||||||
|       }; |       }; | ||||||
|     } |     } | ||||||
|  |     if (this._searchParms.has("label")) { | ||||||
|  |       this._filterLabel(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _filterLabel() { | ||||||
|  |     const label = this._searchParms.get("label"); | ||||||
|  |     if (!label) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     this._filters = { | ||||||
|  |       ...this._filters, | ||||||
|  |       "ha-filter-labels": { | ||||||
|  |         value: [label], | ||||||
|  |         items: undefined, | ||||||
|  |       }, | ||||||
|  |     }; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _clearFilter() { |   private _clearFilter() { | ||||||
|   | |||||||
| @@ -1,8 +1,17 @@ | |||||||
| import "@lrnwebcomponents/simple-tooltip/simple-tooltip"; | import "@lrnwebcomponents/simple-tooltip/simple-tooltip"; | ||||||
| import { mdiAlertCircle, mdiPencilOff, mdiPlus } from "@mdi/js"; | import { mdiAlertCircle, mdiPencilOff, mdiPlus } from "@mdi/js"; | ||||||
| import { HassEntity } from "home-assistant-js-websocket"; | import { HassEntity } from "home-assistant-js-websocket"; | ||||||
| import { LitElement, PropertyValues, TemplateResult, html } from "lit"; | import { | ||||||
|  |   CSSResultGroup, | ||||||
|  |   LitElement, | ||||||
|  |   PropertyValues, | ||||||
|  |   TemplateResult, | ||||||
|  |   css, | ||||||
|  |   html, | ||||||
|  |   nothing, | ||||||
|  | } from "lit"; | ||||||
| import { customElement, property, state } from "lit/decorators"; | import { customElement, property, state } from "lit/decorators"; | ||||||
|  | import { consume } from "@lit-labs/context"; | ||||||
| import memoizeOne from "memoize-one"; | import memoizeOne from "memoize-one"; | ||||||
| import { computeStateDomain } from "../../../common/entity/compute_state_domain"; | import { computeStateDomain } from "../../../common/entity/compute_state_domain"; | ||||||
| import { navigate } from "../../../common/navigate"; | import { navigate } from "../../../common/navigate"; | ||||||
| @@ -15,6 +24,7 @@ import { | |||||||
|   DataTableColumnContainer, |   DataTableColumnContainer, | ||||||
|   RowClickedEvent, |   RowClickedEvent, | ||||||
| } from "../../../components/data-table/ha-data-table"; | } from "../../../components/data-table/ha-data-table"; | ||||||
|  | import "../../../components/data-table/ha-data-table-labels"; | ||||||
| import "../../../components/ha-fab"; | import "../../../components/ha-fab"; | ||||||
| import "../../../components/ha-icon"; | import "../../../components/ha-icon"; | ||||||
| import "../../../components/ha-state-icon"; | import "../../../components/ha-state-icon"; | ||||||
| @@ -44,6 +54,13 @@ import { configSections } from "../ha-panel-config"; | |||||||
| import "../integrations/ha-integration-overflow-menu"; | import "../integrations/ha-integration-overflow-menu"; | ||||||
| import { isHelperDomain } from "./const"; | import { isHelperDomain } from "./const"; | ||||||
| import { showHelperDetailDialog } from "./show-dialog-helper-detail"; | 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 = { | type HelperItem = { | ||||||
|   id: string; |   id: string; | ||||||
| @@ -54,6 +71,7 @@ type HelperItem = { | |||||||
|   type: string; |   type: string; | ||||||
|   configEntry?: ConfigEntry; |   configEntry?: ConfigEntry; | ||||||
|   entity?: HassEntity; |   entity?: HassEntity; | ||||||
|  |   label_entries: LabelRegistryEntry[]; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| // This groups items by a key but only returns last entry per key. | // This groups items by a key but only returns last entry per key. | ||||||
| @@ -93,6 +111,24 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { | |||||||
|  |  | ||||||
|   @state() private _configEntries?: Record<string, ConfigEntry>; |   @state() private _configEntries?: Record<string, ConfigEntry>; | ||||||
|  |  | ||||||
|  |   @state() private _activeFilters?: string[]; | ||||||
|  |  | ||||||
|  |   @state() private _filters: Record< | ||||||
|  |     string, | ||||||
|  |     { value: string[] | undefined; items: Set<string> | undefined } | ||||||
|  |   > = {}; | ||||||
|  |  | ||||||
|  |   @state() private _expandedFilter?: string; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|  |   _labels!: LabelRegistryEntry[]; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|  |   @consume({ context: fullEntitiesContext, subscribe: true }) | ||||||
|  |   _entityReg!: EntityRegistryEntry[]; | ||||||
|  |  | ||||||
|  |   @state() private _filteredStateItems?: string[] | null; | ||||||
|  |  | ||||||
|   public hassSubscribe() { |   public hassSubscribe() { | ||||||
|     return [ |     return [ | ||||||
|       subscribeConfigEntries( |       subscribeConfigEntries( | ||||||
| @@ -117,6 +153,9 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { | |||||||
|       subscribeEntityRegistry(this.hass.connection!, (entries) => { |       subscribeEntityRegistry(this.hass.connection!, (entries) => { | ||||||
|         this._entityEntries = groupByOne(entries, (entry) => entry.entity_id); |         this._entityEntries = groupByOne(entries, (entry) => entry.entity_id); | ||||||
|       }), |       }), | ||||||
|  |       subscribeLabelRegistry(this.hass.connection, (labels) => { | ||||||
|  |         this._labels = labels; | ||||||
|  |       }), | ||||||
|     ]; |     ]; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -146,10 +185,17 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { | |||||||
|           grows: true, |           grows: true, | ||||||
|           direction: "asc", |           direction: "asc", | ||||||
|           template: (helper) => html` |           template: (helper) => html` | ||||||
|             ${helper.name} |             <div style="font-size: 14px;">${helper.name}</div> | ||||||
|             ${narrow |             ${narrow | ||||||
|               ? html`<div class="secondary">${helper.entity_id}</div> ` |               ? 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} | ||||||
|           `, |           `, | ||||||
|         }, |         }, | ||||||
|       }; |       }; | ||||||
| @@ -201,8 +247,15 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { | |||||||
|       localize: LocalizeFunc, |       localize: LocalizeFunc, | ||||||
|       stateItems: HassEntity[], |       stateItems: HassEntity[], | ||||||
|       entityEntries: Record<string, EntityRegistryEntry>, |       entityEntries: Record<string, EntityRegistryEntry>, | ||||||
|       configEntries: Record<string, ConfigEntry> |       configEntries: Record<string, ConfigEntry>, | ||||||
|  |       entityReg: EntityRegistryEntry[], | ||||||
|  |       labelReg?: LabelRegistryEntry[], | ||||||
|  |       filteredStateItems?: string[] | null | ||||||
|     ): HelperItem[] => { |     ): HelperItem[] => { | ||||||
|  |       if (filteredStateItems === null) { | ||||||
|  |         return []; | ||||||
|  |       } | ||||||
|  |  | ||||||
|       const configEntriesCopy = { ...configEntries }; |       const configEntriesCopy = { ...configEntries }; | ||||||
|  |  | ||||||
|       const states = stateItems.map((entityState) => { |       const states = stateItems.map((entityState) => { | ||||||
| @@ -241,14 +294,29 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { | |||||||
|         entity: undefined, |         entity: undefined, | ||||||
|       })); |       })); | ||||||
|  |  | ||||||
|       return [...states, ...entries].map((item) => ({ |       return [...states, ...entries] | ||||||
|         ...item, |         .filter((item) => | ||||||
|         localized_type: item.configEntry |           filteredStateItems | ||||||
|           ? domainToName(localize, item.type) |             ? filteredStateItems?.includes(item.entity_id) | ||||||
|           : localize( |             : true | ||||||
|               `ui.panel.config.helpers.types.${item.type}` as LocalizeKeys |         ) | ||||||
|             ) || item.type, |         .map((item) => { | ||||||
|       })); |           const entityRegEntry = entityReg.find( | ||||||
|  |             (reg) => reg.entity_id === item.entity_id | ||||||
|  |           ); | ||||||
|  |           const labels = labelReg && entityRegEntry?.labels; | ||||||
|  |           return { | ||||||
|  |             ...item, | ||||||
|  |             localized_type: item.configEntry | ||||||
|  |               ? domainToName(localize, item.type) | ||||||
|  |               : localize( | ||||||
|  |                   `ui.panel.config.helpers.types.${item.type}` as LocalizeKeys | ||||||
|  |                 ) || item.type, | ||||||
|  |             label_entries: (labels || []).map( | ||||||
|  |               (lbl) => labelReg!.find((label) => label.label_id === lbl)! | ||||||
|  |             ), | ||||||
|  |           }; | ||||||
|  |         }); | ||||||
|     } |     } | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
| @@ -269,20 +337,40 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { | |||||||
|         back-path="/config" |         back-path="/config" | ||||||
|         .route=${this.route} |         .route=${this.route} | ||||||
|         .tabs=${configSections.devices} |         .tabs=${configSections.devices} | ||||||
|  |         hasFilters | ||||||
|  |         .filters=${Object.values(this._filters).filter( | ||||||
|  |           (filter) => filter.value?.length | ||||||
|  |         ).length} | ||||||
|         .columns=${this._columns(this.narrow, this.hass.localize)} |         .columns=${this._columns(this.narrow, this.hass.localize)} | ||||||
|         .data=${this._getItems( |         .data=${this._getItems( | ||||||
|           this.hass.localize, |           this.hass.localize, | ||||||
|           this._stateItems, |           this._stateItems, | ||||||
|           this._entityEntries, |           this._entityEntries, | ||||||
|           this._configEntries |           this._configEntries, | ||||||
|  |           this._entityReg, | ||||||
|  |           this._labels, | ||||||
|  |           this._filteredStateItems | ||||||
|         )} |         )} | ||||||
|  |         .activeFilters=${this._activeFilters} | ||||||
|  |         @clear-filter=${this._clearFilter} | ||||||
|         @row-click=${this._openEditDialog} |         @row-click=${this._openEditDialog} | ||||||
|         hasFab |         hasFab | ||||||
|         clickable |         clickable | ||||||
|         .noDataText=${this.hass.localize( |         .noDataText=${this.hass.localize( | ||||||
|           "ui.panel.config.helpers.picker.no_helpers" |           "ui.panel.config.helpers.picker.no_helpers" | ||||||
|         )} |         )} | ||||||
|  |         class=${this.narrow ? "narrow" : ""} | ||||||
|       > |       > | ||||||
|  |         <ha-filter-labels | ||||||
|  |           .hass=${this.hass} | ||||||
|  |           .value=${this._filters["ha-filter-labels"]?.value} | ||||||
|  |           @data-table-filter-changed=${this._filterChanged} | ||||||
|  |           slot="filter-pane" | ||||||
|  |           .expanded=${this._expandedFilter === "ha-filter-labels"} | ||||||
|  |           .narrow=${this.narrow} | ||||||
|  |           @expanded-changed=${this._filterExpanded} | ||||||
|  |         ></ha-filter-labels> | ||||||
|  |  | ||||||
|         <ha-integration-overflow-menu |         <ha-integration-overflow-menu | ||||||
|           .hass=${this.hass} |           .hass=${this.hass} | ||||||
|           slot="toolbar-icon" |           slot="toolbar-icon" | ||||||
| @@ -293,7 +381,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { | |||||||
|             "ui.panel.config.helpers.picker.create_helper" |             "ui.panel.config.helpers.picker.create_helper" | ||||||
|           )} |           )} | ||||||
|           extended |           extended | ||||||
|           @click=${this._createHelpler} |           @click=${this._createHelper} | ||||||
|         > |         > | ||||||
|           <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> |           <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> | ||||||
|         </ha-fab> |         </ha-fab> | ||||||
| @@ -301,6 +389,63 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { | |||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   private _filterExpanded(ev) { | ||||||
|  |     if (ev.detail.expanded) { | ||||||
|  |       this._expandedFilter = ev.target.localName; | ||||||
|  |     } else if (this._expandedFilter === ev.target.localName) { | ||||||
|  |       this._expandedFilter = undefined; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _filterChanged(ev) { | ||||||
|  |     const type = ev.target.localName; | ||||||
|  |     this._filters[type] = ev.detail; | ||||||
|  |     this._applyFilters(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _applyFilters() { | ||||||
|  |     const filters = Object.entries(this._filters); | ||||||
|  |     let items: Set<string> | undefined; | ||||||
|  |     for (const [key, filter] of filters) { | ||||||
|  |       if (filter.items) { | ||||||
|  |         if (!items) { | ||||||
|  |           items = filter.items; | ||||||
|  |           continue; | ||||||
|  |         } | ||||||
|  |         items = | ||||||
|  |           "intersection" in items | ||||||
|  |             ? // @ts-ignore | ||||||
|  |               items.intersection(filter.items) | ||||||
|  |             : new Set([...items].filter((x) => filter.items!.has(x))); | ||||||
|  |       } | ||||||
|  |       if (key === "ha-filter-labels" && filter.value?.length) { | ||||||
|  |         const labelItems: Set<string> = new Set(); | ||||||
|  |         this._stateItems | ||||||
|  |           .filter((stateItem) => | ||||||
|  |             this._entityReg | ||||||
|  |               .find((reg) => reg.entity_id === stateItem.entity_id) | ||||||
|  |               ?.labels.some((lbl) => filter.value!.includes(lbl)) | ||||||
|  |           ) | ||||||
|  |           .forEach((stateItem) => labelItems.add(stateItem.entity_id)); | ||||||
|  |         if (!items) { | ||||||
|  |           items = labelItems; | ||||||
|  |           continue; | ||||||
|  |         } | ||||||
|  |         items = | ||||||
|  |           "intersection" in items | ||||||
|  |             ? // @ts-ignore | ||||||
|  |               items.intersection(labelItems) | ||||||
|  |             : new Set([...items].filter((x) => labelItems!.has(x))); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     this._filteredStateItems = items ? [...items] : undefined; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _clearFilter() { | ||||||
|  |     this._filters = {}; | ||||||
|  |     this._applyFilters(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   protected firstUpdated(changedProps: PropertyValues) { |   protected firstUpdated(changedProps: PropertyValues) { | ||||||
|     super.firstUpdated(changedProps); |     super.firstUpdated(changedProps); | ||||||
|     if (this.route.path === "/add") { |     if (this.route.path === "/add") { | ||||||
| @@ -418,9 +563,23 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _createHelpler() { |   private _createHelper() { | ||||||
|     showHelperDetailDialog(this, {}); |     showHelperDetailDialog(this, {}); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   static get styles(): CSSResultGroup { | ||||||
|  |     return [ | ||||||
|  |       haStyle, | ||||||
|  |       css` | ||||||
|  |         hass-tabs-subpage-data-table { | ||||||
|  |           --data-table-row-height: 60px; | ||||||
|  |         } | ||||||
|  |         hass-tabs-subpage-data-table.narrow { | ||||||
|  |           --data-table-row-height: 72px; | ||||||
|  |         } | ||||||
|  |       `, | ||||||
|  |     ]; | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| declare global { | declare global { | ||||||
|   | |||||||
| @@ -49,11 +49,19 @@ class DialogLabelDetail | |||||||
|       this._icon = ""; |       this._icon = ""; | ||||||
|       this._color = ""; |       this._color = ""; | ||||||
|     } |     } | ||||||
|  |     document.body.addEventListener("keydown", this._handleKeyPress); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   private _handleKeyPress = (ev: KeyboardEvent) => { | ||||||
|  |     if (ev.key === "Escape") { | ||||||
|  |       ev.stopPropagation(); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |  | ||||||
|   public closeDialog(): void { |   public closeDialog(): void { | ||||||
|     this._params = undefined; |     this._params = undefined; | ||||||
|     fireEvent(this, "dialog-closed", { dialog: this.localName }); |     fireEvent(this, "dialog-closed", { dialog: this.localName }); | ||||||
|  |     document.body.removeEventListener("keydown", this._handleKeyPress); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   protected render() { |   protected render() { | ||||||
|   | |||||||
| @@ -1,4 +1,11 @@ | |||||||
| import { mdiDelete, mdiHelpCircle, mdiPlus } from "@mdi/js"; | import { | ||||||
|  |   mdiDelete, | ||||||
|  |   mdiDevices, | ||||||
|  |   mdiHelpCircle, | ||||||
|  |   mdiPlus, | ||||||
|  |   mdiRobot, | ||||||
|  |   mdiShape, | ||||||
|  | } from "@mdi/js"; | ||||||
| import { LitElement, PropertyValues, html, nothing } from "lit"; | import { LitElement, PropertyValues, html, nothing } from "lit"; | ||||||
| import { customElement, property, state } from "lit/decorators"; | import { customElement, property, state } from "lit/decorators"; | ||||||
| import memoizeOne from "memoize-one"; | import memoizeOne from "memoize-one"; | ||||||
| @@ -28,6 +35,7 @@ import "../../../layouts/hass-tabs-subpage-data-table"; | |||||||
| import { HomeAssistant, Route } from "../../../types"; | import { HomeAssistant, Route } from "../../../types"; | ||||||
| import { configSections } from "../ha-panel-config"; | import { configSections } from "../ha-panel-config"; | ||||||
| import { showLabelDetailDialog } from "./show-dialog-label-detail"; | import { showLabelDetailDialog } from "./show-dialog-label-detail"; | ||||||
|  | import { navigate } from "../../../common/navigate"; | ||||||
|  |  | ||||||
| @customElement("ha-config-labels") | @customElement("ha-config-labels") | ||||||
| export class HaConfigLabels extends LitElement { | export class HaConfigLabels extends LitElement { | ||||||
| @@ -81,6 +89,21 @@ export class HaConfigLabels extends LitElement { | |||||||
|             .hass=${this.hass} |             .hass=${this.hass} | ||||||
|             narrow |             narrow | ||||||
|             .items=${[ |             .items=${[ | ||||||
|  |               { | ||||||
|  |                 label: this.hass.localize("ui.panel.config.entities.caption"), | ||||||
|  |                 path: mdiShape, | ||||||
|  |                 action: () => this._navigateEntities(label), | ||||||
|  |               }, | ||||||
|  |               { | ||||||
|  |                 label: this.hass.localize("ui.panel.config.devices.caption"), | ||||||
|  |                 path: mdiDevices, | ||||||
|  |                 action: () => this._navigateDevices(label), | ||||||
|  |               }, | ||||||
|  |               { | ||||||
|  |                 label: this.hass.localize("ui.panel.config.automation.caption"), | ||||||
|  |                 path: mdiRobot, | ||||||
|  |                 action: () => this._navigateAutomations(label), | ||||||
|  |               }, | ||||||
|               { |               { | ||||||
|                 label: this.hass.localize("ui.common.delete"), |                 label: this.hass.localize("ui.common.delete"), | ||||||
|                 path: mdiDelete, |                 path: mdiDelete, | ||||||
| @@ -225,6 +248,20 @@ export class HaConfigLabels extends LitElement { | |||||||
|       return false; |       return false; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   private _navigateEntities(label: LabelRegistryEntry) { | ||||||
|  |     navigate(`/config/entities?historyBack=1&label=${label.label_id}`); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _navigateDevices(label: LabelRegistryEntry) { | ||||||
|  |     navigate(`/config/devices/dashboard?historyBack=1&label=${label.label_id}`); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _navigateAutomations(label: LabelRegistryEntry) { | ||||||
|  |     navigate( | ||||||
|  |       `/config/automation/dashboard?historyBack=1&label=${label.label_id}` | ||||||
|  |     ); | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| declare global { | declare global { | ||||||
|   | |||||||
| @@ -96,6 +96,8 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) { | |||||||
|  |  | ||||||
|   @property({ attribute: false }) public scenes!: SceneEntity[]; |   @property({ attribute: false }) public scenes!: SceneEntity[]; | ||||||
|  |  | ||||||
|  |   @state() private _searchParms = new URLSearchParams(window.location.search); | ||||||
|  |  | ||||||
|   @state() private _activeFilters?: string[]; |   @state() private _activeFilters?: string[]; | ||||||
|  |  | ||||||
|   @state() private _filteredScenes?: string[] | null; |   @state() private _filteredScenes?: string[] | null; | ||||||
| @@ -530,6 +532,27 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) { | |||||||
|     this._applyFilters(); |     this._applyFilters(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   firstUpdated() { | ||||||
|  |     if (this._searchParms.has("label")) { | ||||||
|  |       this._filterLabel(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _filterLabel() { | ||||||
|  |     const label = this._searchParms.get("label"); | ||||||
|  |     if (!label) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     this._filters = { | ||||||
|  |       ...this._filters, | ||||||
|  |       "ha-filter-labels": { | ||||||
|  |         value: [label], | ||||||
|  |         items: undefined, | ||||||
|  |       }, | ||||||
|  |     }; | ||||||
|  |     this._applyFilters(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   private _handleRowClicked(ev: HASSDomEvent<RowClickedEvent>) { |   private _handleRowClicked(ev: HASSDomEvent<RowClickedEvent>) { | ||||||
|     const scene = this.scenes.find((a) => a.entity_id === ev.detail.id); |     const scene = this.scenes.find((a) => a.entity_id === ev.detail.id); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -572,6 +572,24 @@ class HaScriptPicker extends SubscribeMixin(LitElement) { | |||||||
|     if (this._searchParms.has("blueprint")) { |     if (this._searchParms.has("blueprint")) { | ||||||
|       this._filterBlueprint(); |       this._filterBlueprint(); | ||||||
|     } |     } | ||||||
|  |     if (this._searchParms.has("label")) { | ||||||
|  |       this._filterLabel(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _filterLabel() { | ||||||
|  |     const label = this._searchParms.get("label"); | ||||||
|  |     if (!label) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     this._filters = { | ||||||
|  |       ...this._filters, | ||||||
|  |       "ha-filter-labels": { | ||||||
|  |         value: [label], | ||||||
|  |         items: undefined, | ||||||
|  |       }, | ||||||
|  |     }; | ||||||
|  |     this._applyFilters(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private async _filterBlueprint() { |   private async _filterBlueprint() { | ||||||
|   | |||||||
| @@ -35,6 +35,9 @@ export abstract class HuiStackCard<T extends StackCardConfig = StackCardConfig> | |||||||
|  |  | ||||||
|   @state() protected _config?: T; |   @state() protected _config?: T; | ||||||
|  |  | ||||||
|  |   @property({ type: Boolean, reflect: true }) | ||||||
|  |   public isPanel = false; | ||||||
|  |  | ||||||
|   public getCardSize(): number | Promise<number> { |   public getCardSize(): number | Promise<number> { | ||||||
|     return 1; |     return 1; | ||||||
|   } |   } | ||||||
| @@ -98,10 +101,10 @@ export abstract class HuiStackCard<T extends StackCardConfig = StackCardConfig> | |||||||
|         display: block; |         display: block; | ||||||
|         padding: 24px 16px 16px; |         padding: 24px 16px 16px; | ||||||
|       } |       } | ||||||
|       #root { |       :host([ispanel]) #root { | ||||||
|         --ha-card-border-radius: var(--restore-card-border-radius, inherit); |         --ha-card-border-radius: var(--restore-card-border-radius); | ||||||
|         --ha-card-border-width: var(--restore-card-border-width, inherit); |         --ha-card-border-width: var(--restore-card-border-width); | ||||||
|         --ha-card-box-shadow: var(--restore-card-border-shadow, inherit); |         --ha-card-box-shadow: var(--restore-card-border-shadow); | ||||||
|       } |       } | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -108,7 +108,6 @@ export class PanelView extends LitElement implements LovelaceViewElement { | |||||||
|  |  | ||||||
|     const card: LovelaceCard = this.cards[0]; |     const card: LovelaceCard = this.cards[0]; | ||||||
|     card.isPanel = true; |     card.isPanel = true; | ||||||
|     card.toggleAttribute("no-border", true); |  | ||||||
|  |  | ||||||
|     if (this.isStrategy || !this.lovelace?.editMode) { |     if (this.isStrategy || !this.lovelace?.editMode) { | ||||||
|       card.editMode = false; |       card.editMode = false; | ||||||
| @@ -117,7 +116,6 @@ export class PanelView extends LitElement implements LovelaceViewElement { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     const wrapper = document.createElement("hui-card-options"); |     const wrapper = document.createElement("hui-card-options"); | ||||||
|     wrapper.toggleAttribute("no-border", true); |  | ||||||
|     wrapper.hass = this.hass; |     wrapper.hass = this.hass; | ||||||
|     wrapper.lovelace = this.lovelace; |     wrapper.lovelace = this.lovelace; | ||||||
|     wrapper.path = [this.index!, 0]; |     wrapper.path = [this.index!, 0]; | ||||||
| @@ -137,7 +135,7 @@ export class PanelView extends LitElement implements LovelaceViewElement { | |||||||
|         --restore-card-box-shadow: var(--ha-card-box-shadow, none); |         --restore-card-box-shadow: var(--ha-card-box-shadow, none); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       [no-border] { |       * { | ||||||
|         --ha-card-border-radius: 0; |         --ha-card-border-radius: 0; | ||||||
|         --ha-card-border-width: 0; |         --ha-card-border-width: 0; | ||||||
|         --ha-card-box-shadow: none; |         --ha-card-box-shadow: none; | ||||||
|   | |||||||
| @@ -143,7 +143,10 @@ export const derivedStyles = { | |||||||
|   "mdc-select-disabled-ink-color": "var(--input-disabled-ink-color)", |   "mdc-select-disabled-ink-color": "var(--input-disabled-ink-color)", | ||||||
|   "mdc-select-dropdown-icon-color": "var(--input-dropdown-icon-color)", |   "mdc-select-dropdown-icon-color": "var(--input-dropdown-icon-color)", | ||||||
|   "mdc-select-disabled-dropdown-icon-color": "var(--input-disabled-ink-color)", |   "mdc-select-disabled-dropdown-icon-color": "var(--input-disabled-ink-color)", | ||||||
|  |   "ha-assist-chip-filled-container-color": | ||||||
|  |     "rgba(var(--rgb-primary-text-color),0.15)", | ||||||
|  |   "ha-assist-chip-active-container-color": | ||||||
|  |     "rgba(var(--rgb-primary-color),0.15)", | ||||||
|   "chip-background-color": "rgba(var(--rgb-primary-text-color), 0.15)", |   "chip-background-color": "rgba(var(--rgb-primary-text-color), 0.15)", | ||||||
|   // Vaadin |   // Vaadin | ||||||
|   "material-body-text-color": "var(--primary-text-color)", |   "material-body-text-color": "var(--primary-text-color)", | ||||||
|   | |||||||
| @@ -501,11 +501,18 @@ | |||||||
|       }, |       }, | ||||||
|       "subpage-data-table": { |       "subpage-data-table": { | ||||||
|         "filters": "Filters", |         "filters": "Filters", | ||||||
|  |         "clear_filter": "Clear filter", | ||||||
|  |         "close_filter": "Close filters", | ||||||
|  |         "exit_selection_mode": "Exit selection mode", | ||||||
|  |         "enter_selection_mode": "Enter selection mode", | ||||||
|         "sort_by": "Sort by {sortColumn}", |         "sort_by": "Sort by {sortColumn}", | ||||||
|         "group_by": "Group by {groupColumn}", |         "group_by": "Group by {groupColumn}", | ||||||
|         "dont_group_by": "Don't group", |         "dont_group_by": "Don't group", | ||||||
|         "select": "Select", |         "select": "Select", | ||||||
|         "selected": "Selected {selected}" |         "selected": "Selected {selected}", | ||||||
|  |         "close_select_mode": "Close selection mode", | ||||||
|  |         "select_all": "Select all", | ||||||
|  |         "select_none": "Select none" | ||||||
|       }, |       }, | ||||||
|       "config-entry-picker": { |       "config-entry-picker": { | ||||||
|         "config_entry": "Integration" |         "config_entry": "Integration" | ||||||
| @@ -564,6 +571,7 @@ | |||||||
|         "add_new_sugestion": "Add new category ''{name}''", |         "add_new_sugestion": "Add new category ''{name}''", | ||||||
|         "add_new": "Add new category…", |         "add_new": "Add new category…", | ||||||
|         "no_categories": "You don't have any categories", |         "no_categories": "You don't have any categories", | ||||||
|  |         "no_match": "No matching categories found", | ||||||
|         "add_dialog": { |         "add_dialog": { | ||||||
|           "title": "Add new category", |           "title": "Add new category", | ||||||
|           "text": "Enter the name of the new category.", |           "text": "Enter the name of the new category.", | ||||||
| @@ -592,13 +600,7 @@ | |||||||
|         "no_areas": "You don't have any areas", |         "no_areas": "You don't have any areas", | ||||||
|         "no_match": "No matching areas found", |         "no_match": "No matching areas found", | ||||||
|         "unassigned_areas": "Unassigned areas", |         "unassigned_areas": "Unassigned areas", | ||||||
|         "add_dialog": { |         "failed_create_area": "Failed to create area." | ||||||
|           "title": "Add new area", |  | ||||||
|           "text": "Enter the name of the new area.", |  | ||||||
|           "name": "Name", |  | ||||||
|           "add": "Add", |  | ||||||
|           "failed_create_area": "Failed to create area." |  | ||||||
|         } |  | ||||||
|       }, |       }, | ||||||
|       "floor-picker": { |       "floor-picker": { | ||||||
|         "clear": "Clear", |         "clear": "Clear", | ||||||
| @@ -608,13 +610,7 @@ | |||||||
|         "add_new": "Add new floor…", |         "add_new": "Add new floor…", | ||||||
|         "no_floors": "You don't have any floors", |         "no_floors": "You don't have any floors", | ||||||
|         "no_match": "No matching floors found", |         "no_match": "No matching floors found", | ||||||
|         "add_dialog": { |         "failed_create_floor": "Failed to create floor." | ||||||
|           "title": "Add new floor", |  | ||||||
|           "text": "Enter the name of the new floor.", |  | ||||||
|           "name": "Name", |  | ||||||
|           "add": "Add", |  | ||||||
|           "failed_create_floor": "Failed to create floor." |  | ||||||
|         } |  | ||||||
|       }, |       }, | ||||||
|       "area-filter": { |       "area-filter": { | ||||||
|         "title": "Areas", |         "title": "Areas", | ||||||
| @@ -1118,6 +1114,7 @@ | |||||||
|         "edit": "Edit entity", |         "edit": "Edit entity", | ||||||
|         "details": "Details", |         "details": "Details", | ||||||
|         "back_to_info": "Back to info", |         "back_to_info": "Back to info", | ||||||
|  |         "info": "Information", | ||||||
|         "related": "Related", |         "related": "Related", | ||||||
|         "history": "History", |         "history": "History", | ||||||
|         "logbook": "Logbook", |         "logbook": "Logbook", | ||||||
| @@ -2669,6 +2666,7 @@ | |||||||
|             "edit_automation": "Edit automation", |             "edit_automation": "Edit automation", | ||||||
|             "dev_automation": "Debug automation", |             "dev_automation": "Debug automation", | ||||||
|             "show_info_automation": "Show info about automation", |             "show_info_automation": "Show info about automation", | ||||||
|  |             "show_settings": "Show settings", | ||||||
|             "delete": "[%key:ui::common::delete%]", |             "delete": "[%key:ui::common::delete%]", | ||||||
|             "delete_confirm_title": "Delete automation?", |             "delete_confirm_title": "Delete automation?", | ||||||
|             "delete_confirm_text": "{name} will be permanently deleted.", |             "delete_confirm_text": "{name} will be permanently deleted.", | ||||||
| @@ -2689,6 +2687,14 @@ | |||||||
|               "state": "State", |               "state": "State", | ||||||
|               "category": "Category" |               "category": "Category" | ||||||
|             }, |             }, | ||||||
|  |             "bulk_action": "Action", | ||||||
|  |             "bulk_actions": { | ||||||
|  |               "move_category": "Move to category", | ||||||
|  |               "no_category": "No category", | ||||||
|  |               "add_label": "Add label", | ||||||
|  |               "enable": "Enable", | ||||||
|  |               "disable": "Disable" | ||||||
|  |             }, | ||||||
|             "empty_header": "Start automating", |             "empty_header": "Start automating", | ||||||
|             "empty_text_1": "Automations make Home Assistant automatically respond to things happening in and around your home.", |             "empty_text_1": "Automations make Home Assistant automatically respond to things happening in and around your home.", | ||||||
|             "empty_text_2": "Automations connect triggers to actions in a ''when trigger then action'' fashion with optional conditions. For example: ''When the sun sets and if {user} is home, then turn on the lights''." |             "empty_text_2": "Automations connect triggers to actions in a ''when trigger then action'' fashion with optional conditions. For example: ''When the sun sets and if {user} is home, then turn on the lights''." | ||||||
|   | |||||||
							
								
								
									
										10
									
								
								yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								yarn.lock
									
									
									
									
									
								
							| @@ -1526,14 +1526,14 @@ __metadata: | |||||||
|   languageName: node |   languageName: node | ||||||
|   linkType: hard |   linkType: hard | ||||||
|  |  | ||||||
| "@codemirror/view@npm:6.26.0, @codemirror/view@npm:^6.0.0, @codemirror/view@npm:^6.17.0, @codemirror/view@npm:^6.23.0": | "@codemirror/view@npm:6.26.1, @codemirror/view@npm:^6.0.0, @codemirror/view@npm:^6.17.0, @codemirror/view@npm:^6.23.0": | ||||||
|   version: 6.26.0 |   version: 6.26.1 | ||||||
|   resolution: "@codemirror/view@npm:6.26.0" |   resolution: "@codemirror/view@npm:6.26.1" | ||||||
|   dependencies: |   dependencies: | ||||||
|     "@codemirror/state": "npm:^6.4.0" |     "@codemirror/state": "npm:^6.4.0" | ||||||
|     style-mod: "npm:^4.1.0" |     style-mod: "npm:^4.1.0" | ||||||
|     w3c-keyname: "npm:^2.2.4" |     w3c-keyname: "npm:^2.2.4" | ||||||
|   checksum: 10/d4ef249044cbc293a7267c83e08671a68646fd7bbe1efb8d205c01385f157c93918eabeaedb62a4cc10598ab63818ac749cec4f6355fe0404d9d4beb7857c31f |   checksum: 10/6d2b19b2439c36b2712d3560eeb0c198ad2ee442ad22641c2b4bce94077812cffbb52ca12328219d3b9663b2dd0ffc63481432a2550839e5c7a7a53704e82a9a | ||||||
|   languageName: node |   languageName: node | ||||||
|   linkType: hard |   linkType: hard | ||||||
|  |  | ||||||
| @@ -9604,7 +9604,7 @@ __metadata: | |||||||
|     "@codemirror/legacy-modes": "npm:6.3.3" |     "@codemirror/legacy-modes": "npm:6.3.3" | ||||||
|     "@codemirror/search": "npm:6.5.6" |     "@codemirror/search": "npm:6.5.6" | ||||||
|     "@codemirror/state": "npm:6.4.1" |     "@codemirror/state": "npm:6.4.1" | ||||||
|     "@codemirror/view": "npm:6.26.0" |     "@codemirror/view": "npm:6.26.1" | ||||||
|     "@egjs/hammerjs": "npm:2.0.17" |     "@egjs/hammerjs": "npm:2.0.17" | ||||||
|     "@formatjs/intl-datetimeformat": "npm:6.12.3" |     "@formatjs/intl-datetimeformat": "npm:6.12.3" | ||||||
|     "@formatjs/intl-displaynames": "npm:6.6.6" |     "@formatjs/intl-displaynames": "npm:6.6.6" | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user