mirror of
				https://github.com/home-assistant/frontend.git
				synced 2025-10-31 22:49:37 +00:00 
			
		
		
		
	Compare commits
	
		
			41 Commits
		
	
	
		
			20240328.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 | ||
|   | bc14b8468d | ||
|   | f924f81ec1 | ||
|   | 3a6382df55 | ||
|   | 1dba049038 | ||
|   | f539516252 | ||
|   | abd02eda0f | ||
|   | 99695d6cb3 | ||
|   | cb1c2b59df | ||
|   | 8368f977b9 | ||
|   | e05595f318 | ||
|   | 11cf2ec39d | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | e5c43fcfcd | ||
|   | 520581c165 | ||
|   | d1119a3b61 | ||
|   | 5dd029cc05 | ||
|   | 510e010f97 | ||
|   | 1300cffa3b | ||
|   | 8fbcbb0b68 | 
| @@ -4,4 +4,11 @@ import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; | ||||
| export const mockAreaRegistry = ( | ||||
|   hass: MockHomeAssistant, | ||||
|   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 = ( | ||||
|   hass: MockHomeAssistant, | ||||
|   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 type { HomeAssistant } from "../../../../src/types"; | ||||
| 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 = [ | ||||
|   getEntity("alarm_control_panel", "alarm", "disarmed", { | ||||
| @@ -100,7 +104,7 @@ const DEVICES = [ | ||||
| const AREAS: AreaRegistryEntry[] = [ | ||||
|   { | ||||
|     area_id: "backyard", | ||||
|     floor_id: null, | ||||
|     floor_id: "ground", | ||||
|     name: "Backyard", | ||||
|     icon: null, | ||||
|     picture: null, | ||||
| @@ -109,7 +113,7 @@ const AREAS: AreaRegistryEntry[] = [ | ||||
|   }, | ||||
|   { | ||||
|     area_id: "bedroom", | ||||
|     floor_id: null, | ||||
|     floor_id: "first", | ||||
|     name: "Bedroom", | ||||
|     icon: "mdi:bed", | ||||
|     picture: null, | ||||
| @@ -118,7 +122,7 @@ const AREAS: AreaRegistryEntry[] = [ | ||||
|   }, | ||||
|   { | ||||
|     area_id: "livingroom", | ||||
|     floor_id: null, | ||||
|     floor_id: "ground", | ||||
|     name: "Livingroom", | ||||
|     icon: "mdi:sofa", | ||||
|     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: { | ||||
|   name: string; | ||||
|   input: Record<string, (BlueprintInput & { required?: boolean }) | null>; | ||||
| @@ -134,7 +177,12 @@ const SCHEMAS: { | ||||
|   { | ||||
|     name: "One of each", | ||||
|     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: {} } }, | ||||
|       target: { name: "Target", selector: { target: {} } }, | ||||
|       state: { | ||||
|         name: "State", | ||||
|         selector: { state: { entity_id: "alarm_control_panel.alarm" } }, | ||||
| @@ -143,15 +191,12 @@ const SCHEMAS: { | ||||
|         name: "Attribute", | ||||
|         selector: { attribute: { entity_id: "" } }, | ||||
|       }, | ||||
|       device: { name: "Device", selector: { device: {} } }, | ||||
|       config_entry: { | ||||
|         name: "Integration", | ||||
|         selector: { config_entry: {} }, | ||||
|       }, | ||||
|       duration: { name: "Duration", selector: { duration: {} } }, | ||||
|       addon: { name: "Addon", selector: { addon: {} } }, | ||||
|       area: { name: "Area", selector: { area: {} } }, | ||||
|       target: { name: "Target", selector: { target: {} } }, | ||||
|       number_box: { | ||||
|         name: "Number Box", | ||||
|         selector: { | ||||
| @@ -300,6 +345,8 @@ const SCHEMAS: { | ||||
|       entity: { name: "Entity", selector: { entity: { multiple: true } } }, | ||||
|       device: { name: "Device", selector: { device: { multiple: true } } }, | ||||
|       area: { name: "Area", selector: { area: { multiple: true } } }, | ||||
|       floor: { name: "Floor", selector: { floor: { multiple: true } } }, | ||||
|       label: { name: "Label", selector: { label: { multiple: true } } }, | ||||
|       select: { | ||||
|         name: "Select Multiple", | ||||
|         selector: { | ||||
| @@ -356,6 +403,8 @@ class DemoHaSelector extends LitElement implements ProvideHassElement { | ||||
|     mockDeviceRegistry(hass, DEVICES); | ||||
|     mockConfigEntries(hass); | ||||
|     mockAreaRegistry(hass, AREAS); | ||||
|     mockFloorRegistry(hass, FLOORS); | ||||
|     mockLabelRegistry(hass, LABELS); | ||||
|     mockHassioSupervisor(hass); | ||||
|     hass.mockWS("auth/sign_path", (params) => params); | ||||
|     hass.mockWS("media_player/browse_media", this._browseMedia); | ||||
|   | ||||
| @@ -33,7 +33,7 @@ | ||||
|     "@codemirror/legacy-modes": "6.3.3", | ||||
|     "@codemirror/search": "6.5.6", | ||||
|     "@codemirror/state": "6.4.1", | ||||
|     "@codemirror/view": "6.26.0", | ||||
|     "@codemirror/view": "6.26.1", | ||||
|     "@egjs/hammerjs": "2.0.17", | ||||
|     "@formatjs/intl-datetimeformat": "6.12.3", | ||||
|     "@formatjs/intl-displaynames": "6.6.6", | ||||
| @@ -185,8 +185,8 @@ | ||||
|     "@types/tar": "6.1.11", | ||||
|     "@types/ua-parser-js": "0.7.39", | ||||
|     "@types/webspeechapi": "0.0.29", | ||||
|     "@typescript-eslint/eslint-plugin": "7.3.1", | ||||
|     "@typescript-eslint/parser": "7.3.1", | ||||
|     "@typescript-eslint/eslint-plugin": "7.4.0", | ||||
|     "@typescript-eslint/parser": "7.4.0", | ||||
|     "@web/dev-server": "0.1.38", | ||||
|     "@web/dev-server-rollup": "0.4.1", | ||||
|     "babel-loader": "9.1.3", | ||||
|   | ||||
| @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" | ||||
|  | ||||
| [project] | ||||
| name         = "home-assistant-frontend" | ||||
| version      = "20240328.0" | ||||
| version      = "20240402.0" | ||||
| license      = {text = "Apache-2.0"} | ||||
| description  = "The Home Assistant frontend" | ||||
| readme       = "README.md" | ||||
|   | ||||
| @@ -22,14 +22,6 @@ export class HaAssistChip extends MdAssistChip { | ||||
|         ); | ||||
|         --md-assist-chip-outline-color: var(--outline-color); | ||||
|         --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 **/ | ||||
|       .filled { | ||||
| @@ -52,10 +44,17 @@ export class HaAssistChip extends MdAssistChip { | ||||
|         margin-inline-end: unset; | ||||
|         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 { | ||||
|         background: var(--ha-assist-chip-active-container-color); | ||||
|         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 { fireEvent } from "../../common/dom/fire_event"; | ||||
| import "../ha-label"; | ||||
| import { stringCompare } from "../../common/string/compare"; | ||||
|  | ||||
| @customElement("ha-data-table-labels") | ||||
| class HaDataTableLabels extends LitElement { | ||||
|   @property({ attribute: false }) public labels!: LabelRegistryEntry[]; | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|     const labels = this.labels.sort((a, b) => stringCompare(a.name, b.name)); | ||||
|     return html` | ||||
|       <ha-chip-set> | ||||
|         ${repeat( | ||||
|           this.labels.slice(0, 2), | ||||
|           labels.slice(0, 2), | ||||
|           (label) => label.label_id, | ||||
|           (label) => this._renderLabel(label, true) | ||||
|         )} | ||||
|         ${this.labels.length > 2 | ||||
|         ${labels.length > 2 | ||||
|           ? html`<ha-button-menu | ||||
|               absolute | ||||
|               role="button" | ||||
| @@ -27,10 +29,10 @@ class HaDataTableLabels extends LitElement { | ||||
|               @closed=${this._handleIconOverflowMenuClosed} | ||||
|             > | ||||
|               <ha-label slot="trigger" class="plus" dense> | ||||
|                 +${this.labels.length - 2} | ||||
|                 +${labels.length - 2} | ||||
|               </ha-label> | ||||
|               ${repeat( | ||||
|                 this.labels.slice(2), | ||||
|                 labels.slice(2), | ||||
|                 (label) => label.label_id, | ||||
|                 (label) => html` | ||||
|                   <ha-list-item @click=${this._labelClicked} .item=${label}> | ||||
|   | ||||
| @@ -181,6 +181,13 @@ export class HaDataTable extends LitElement { | ||||
|     this._checkedRowsChanged(); | ||||
|   } | ||||
|  | ||||
|   public selectAll(): void { | ||||
|     this._checkedRows = this._filteredData | ||||
|       .filter((data) => data.selectable !== false) | ||||
|       .map((data) => data[this.id]); | ||||
|     this._checkedRowsChanged(); | ||||
|   } | ||||
|  | ||||
|   public connectedCallback() { | ||||
|     super.connectedCallback(); | ||||
|     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) => { | ||||
|     // not sure how this happens... | ||||
| @@ -512,10 +519,6 @@ export class HaDataTable extends LitElement { | ||||
|         items.push({ append: true, content: this.appendRow }); | ||||
|       } | ||||
|  | ||||
|       if (this.hasFab) { | ||||
|         items.push({ empty: true }); | ||||
|       } | ||||
|  | ||||
|       if (this.groupColumn) { | ||||
|         const grouped = groupBy(items, (item) => item[this.groupColumn!]); | ||||
|         if (grouped.undefined) { | ||||
| @@ -555,6 +558,10 @@ export class HaDataTable extends LitElement { | ||||
|       } else { | ||||
|         this._items = items; | ||||
|       } | ||||
|  | ||||
|       if (this.hasFab) { | ||||
|         this._items = [...this._items, { empty: true }]; | ||||
|       } | ||||
|     } else { | ||||
|       this._items = data; | ||||
|     } | ||||
| @@ -593,10 +600,7 @@ export class HaDataTable extends LitElement { | ||||
|   private _handleHeaderRowCheckboxClick(ev: Event) { | ||||
|     const checkbox = ev.target as HaCheckbox; | ||||
|     if (checkbox.checked) { | ||||
|       this._checkedRows = this._filteredData | ||||
|         .filter((data) => data.selectable !== false) | ||||
|         .map((data) => data[this.id]); | ||||
|       this._checkedRowsChanged(); | ||||
|       this.selectAll(); | ||||
|     } else { | ||||
|       this._checkedRows = []; | ||||
|       this._checkedRowsChanged(); | ||||
| @@ -623,9 +627,13 @@ export class HaDataTable extends LitElement { | ||||
|       ev | ||||
|         .composedPath() | ||||
|         .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; | ||||
|   | ||||
| @@ -21,10 +21,8 @@ import { | ||||
|   getDeviceEntityDisplayLookup, | ||||
| } from "../data/device_registry"; | ||||
| import { EntityRegistryDisplayEntry } from "../data/entity_registry"; | ||||
| import { | ||||
|   showAlertDialog, | ||||
|   showPromptDialog, | ||||
| } from "../dialogs/generic/show-dialog-box"; | ||||
| import { showAlertDialog } from "../dialogs/generic/show-dialog-box"; | ||||
| import { showAreaRegistryDetailDialog } from "../panels/config/areas/show-dialog-area-registry-detail"; | ||||
| import { HomeAssistant, ValueChangedEvent } from "../types"; | ||||
| import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker"; | ||||
| import "./ha-combo-box"; | ||||
| @@ -38,7 +36,7 @@ type ScorableAreaRegistryEntry = ScorableTextItem & AreaRegistryEntry; | ||||
| const rowRenderer: ComboBoxLitRenderer<AreaRegistryEntry> = (item) => | ||||
|   html`<ha-list-item | ||||
|     graphic="icon" | ||||
|     class=${classMap({ "add-new": item.area_id === "add_new" })} | ||||
|     class=${classMap({ "add-new": item.area_id === ADD_NEW_ID })} | ||||
|   > | ||||
|     ${item.icon | ||||
|       ? html`<ha-icon slot="graphic" .icon=${item.icon}></ha-icon>` | ||||
| @@ -46,6 +44,10 @@ const rowRenderer: ComboBoxLitRenderer<AreaRegistryEntry> = (item) => | ||||
|     ${item.name} | ||||
|   </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") | ||||
| export class HaAreaPicker extends LitElement { | ||||
|   @property({ attribute: false }) public hass!: HomeAssistant; | ||||
| @@ -134,20 +136,6 @@ export class HaAreaPicker extends LitElement { | ||||
|       noAdd: this["noAdd"], | ||||
|       excludeAreas: this["excludeAreas"] | ||||
|     ): 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 inputDevices: DeviceRegistryEntry[] | undefined; | ||||
|       let inputEntities: EntityRegistryDisplayEntry[] | undefined; | ||||
| @@ -284,9 +272,9 @@ export class HaAreaPicker extends LitElement { | ||||
|       if (!outputAreas.length) { | ||||
|         outputAreas = [ | ||||
|           { | ||||
|             area_id: "no_areas", | ||||
|             area_id: NO_ITEMS_ID, | ||||
|             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, | ||||
|             icon: null, | ||||
|             aliases: [], | ||||
| @@ -300,7 +288,7 @@ export class HaAreaPicker extends LitElement { | ||||
|         : [ | ||||
|             ...outputAreas, | ||||
|             { | ||||
|               area_id: "add_new", | ||||
|               area_id: ADD_NEW_ID, | ||||
|               floor_id: null, | ||||
|               name: this.hass.localize("ui.components.area-picker.add_new"), | ||||
|               picture: null, | ||||
| @@ -374,20 +362,40 @@ export class HaAreaPicker extends LitElement { | ||||
|  | ||||
|     const filteredItems = fuzzyFilterSort<ScorableAreaRegistryEntry>( | ||||
|       filterString, | ||||
|       target.items || [] | ||||
|       target.items?.filter( | ||||
|         (item) => ![NO_ITEMS_ID, ADD_NEW_ID].includes(item.label_id) | ||||
|       ) || [] | ||||
|     ); | ||||
|     if (!this.noAdd && filteredItems?.length === 0) { | ||||
|       this._suggestion = filterString; | ||||
|       this.comboBox.filteredItems = [ | ||||
|         { | ||||
|           area_id: "add_new_suggestion", | ||||
|           name: this.hass.localize( | ||||
|             "ui.components.area-picker.add_new_sugestion", | ||||
|             { name: this._suggestion } | ||||
|           ), | ||||
|           picture: null, | ||||
|         }, | ||||
|       ]; | ||||
|     if (filteredItems.length === 0) { | ||||
|       if (!this.noAdd) { | ||||
|         this.comboBox.filteredItems = [ | ||||
|           { | ||||
|             area_id: NO_ITEMS_ID, | ||||
|             floor_id: null, | ||||
|             name: this.hass.localize("ui.components.area-picker.no_match"), | ||||
|             icon: 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 { | ||||
|       this.comboBox.filteredItems = filteredItems; | ||||
|     } | ||||
| @@ -405,11 +413,13 @@ export class HaAreaPicker extends LitElement { | ||||
|     ev.stopPropagation(); | ||||
|     let newValue = ev.detail.value; | ||||
|  | ||||
|     if (newValue === "no_areas") { | ||||
|     if (newValue === NO_ITEMS_ID) { | ||||
|       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) { | ||||
|         this._setValue(newValue); | ||||
|       } | ||||
| @@ -417,25 +427,12 @@ export class HaAreaPicker extends LitElement { | ||||
|     } | ||||
|  | ||||
|     (ev.target as any).value = this._value; | ||||
|     showPromptDialog(this, { | ||||
|       title: this.hass.localize("ui.components.area-picker.add_dialog.title"), | ||||
|       text: this.hass.localize("ui.components.area-picker.add_dialog.text"), | ||||
|       confirmText: this.hass.localize( | ||||
|         "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; | ||||
|         } | ||||
|  | ||||
|     showAreaRegistryDetailDialog(this, { | ||||
|       suggestedName: newValue === ADD_NEW_SUGGESTION_ID ? this._suggestion : "", | ||||
|       createEntry: async (values) => { | ||||
|         try { | ||||
|           const area = await createAreaRegistryEntry(this.hass, { | ||||
|             name, | ||||
|           }); | ||||
|           const area = await createAreaRegistryEntry(this.hass, values); | ||||
|           const areas = [...Object.values(this.hass.areas), area]; | ||||
|           this.comboBox.filteredItems = this._getAreas( | ||||
|             areas, | ||||
| @@ -455,18 +452,16 @@ export class HaAreaPicker extends LitElement { | ||||
|         } catch (err: any) { | ||||
|           showAlertDialog(this, { | ||||
|             title: this.hass.localize( | ||||
|               "ui.components.area-picker.add_dialog.failed_create_area" | ||||
|               "ui.components.area-picker.failed_create_area" | ||||
|             ), | ||||
|             text: err.message, | ||||
|           }); | ||||
|         } | ||||
|       }, | ||||
|       cancel: () => { | ||||
|         this._setValue(undefined); | ||||
|         this._suggestion = undefined; | ||||
|         this.comboBox.setInputValue(""); | ||||
|       }, | ||||
|     }); | ||||
|  | ||||
|     this._suggestion = undefined; | ||||
|     this.comboBox.setInputValue(""); | ||||
|   } | ||||
|  | ||||
|   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 { stopPropagation } from "../common/dom/stop_propagation"; | ||||
| import "./ha-select"; | ||||
| import "./ha-list-item"; | ||||
| import { HomeAssistant } from "../types"; | ||||
| import { LocalizeKeys } from "../common/translations/localize"; | ||||
|  | ||||
| @@ -53,18 +54,18 @@ export class HaColorPicker extends LitElement { | ||||
|             ` | ||||
|           : nothing} | ||||
|         ${this.defaultColor | ||||
|           ? html` <mwc-list-item value="default"> | ||||
|           ? html` <ha-list-item value="default"> | ||||
|               ${this.hass.localize(`ui.components.color-picker.default_color`)} | ||||
|             </mwc-list-item>` | ||||
|             </ha-list-item>` | ||||
|           : nothing} | ||||
|         ${Array.from(THEME_COLORS).map( | ||||
|           (color) => html` | ||||
|             <mwc-list-item .value=${color} graphic="icon"> | ||||
|             <ha-list-item .value=${color} graphic="icon"> | ||||
|               ${this.hass.localize( | ||||
|                 `ui.components.color-picker.colors.${color}` as LocalizeKeys | ||||
|               ) || color} | ||||
|               <span slot="graphic">${this.renderColorCircle(color)}</span> | ||||
|             </mwc-list-item> | ||||
|             </ha-list-item> | ||||
|           ` | ||||
|         )} | ||||
|       </ha-select> | ||||
|   | ||||
| @@ -50,7 +50,7 @@ export class HaFilterBlueprints extends LitElement { | ||||
|                     ? nothing | ||||
|                     : html`<ha-check-list-item | ||||
|                         .value=${id} | ||||
|                         .selected=${this.value?.includes(id)} | ||||
|                         .selected=${(this.value || []).includes(id)} | ||||
|                       > | ||||
|                         ${blueprint.metadata.name || id} | ||||
|                       </ha-check-list-item>` | ||||
| @@ -157,11 +157,11 @@ export class HaFilterBlueprints extends LitElement { | ||||
|           border-radius: 50%; | ||||
|           font-weight: 400; | ||||
|           font-size: 11px; | ||||
|           background-color: var(--accent-color); | ||||
|           background-color: var(--primary-color); | ||||
|           line-height: 16px; | ||||
|           text-align: center; | ||||
|           padding: 0px 2px; | ||||
|           color: var(--text-accent-color, var(--text-primary-color)); | ||||
|           color: var(--text-primary-color); | ||||
|         } | ||||
|       `, | ||||
|     ]; | ||||
|   | ||||
| @@ -78,13 +78,15 @@ export class HaFilterCategories extends SubscribeMixin(LitElement) { | ||||
|                 class="ha-scrollbar" | ||||
|                 activatable | ||||
|               > | ||||
|                 <ha-list-item | ||||
|                   .selected=${!this.value?.length} | ||||
|                   .activated=${!this.value?.length} | ||||
|                   >${this.hass.localize( | ||||
|                     "ui.panel.config.category.filter.show_all" | ||||
|                   )}</ha-list-item | ||||
|                 > | ||||
|                 ${this._categories.length > 0 | ||||
|                   ? html`<ha-list-item | ||||
|                       .selected=${!this.value?.length} | ||||
|                       .activated=${!this.value?.length} | ||||
|                       >${this.hass.localize( | ||||
|                         "ui.panel.config.category.filter.show_all" | ||||
|                       )}</ha-list-item | ||||
|                     >` | ||||
|                   : nothing} | ||||
|                 ${this._categories.map( | ||||
|                   (category) => | ||||
|                     html`<ha-list-item | ||||
| @@ -142,7 +144,11 @@ export class HaFilterCategories extends SubscribeMixin(LitElement) { | ||||
|           : nothing} | ||||
|       </ha-expansion-panel> | ||||
|       ${this.expanded | ||||
|         ? html`<ha-list-item graphic="icon" @click=${this._addCategory}> | ||||
|         ? html`<ha-list-item | ||||
|             graphic="icon" | ||||
|             @click=${this._addCategory} | ||||
|             class="add" | ||||
|           > | ||||
|             <ha-svg-icon slot="graphic" .path=${mdiPlus}></ha-svg-icon> | ||||
|             ${this.hass.localize("ui.panel.config.category.editor.add")} | ||||
|           </ha-list-item>` | ||||
| @@ -254,6 +260,7 @@ export class HaFilterCategories extends SubscribeMixin(LitElement) { | ||||
|       css` | ||||
|         :host { | ||||
|           border-bottom: 1px solid var(--divider-color); | ||||
|           position: relative; | ||||
|         } | ||||
|         :host([expanded]) { | ||||
|           flex: 1; | ||||
| @@ -277,11 +284,11 @@ export class HaFilterCategories extends SubscribeMixin(LitElement) { | ||||
|           border-radius: 50%; | ||||
|           font-weight: 400; | ||||
|           font-size: 11px; | ||||
|           background-color: var(--accent-color); | ||||
|           background-color: var(--primary-color); | ||||
|           line-height: 16px; | ||||
|           text-align: center; | ||||
|           padding: 0px 2px; | ||||
|           color: var(--text-accent-color, var(--text-primary-color)); | ||||
|           color: var(--text-primary-color); | ||||
|         } | ||||
|         mwc-list { | ||||
|           --mdc-list-item-meta-size: auto; | ||||
| @@ -291,6 +298,12 @@ export class HaFilterCategories extends SubscribeMixin(LitElement) { | ||||
|         .warning { | ||||
|           color: var(--error-color); | ||||
|         } | ||||
|         .add { | ||||
|           position: absolute; | ||||
|           bottom: 0; | ||||
|           right: 0; | ||||
|           left: 0; | ||||
|         } | ||||
|       `, | ||||
|     ]; | ||||
|   } | ||||
|   | ||||
| @@ -57,7 +57,8 @@ export class HaFilterDevices extends LitElement { | ||||
|         ${this._shouldRender | ||||
|           ? html`<mwc-list class="ha-scrollbar"> | ||||
|               <lit-virtualizer | ||||
|                 .items=${this._devices(this.hass.devices)} | ||||
|                 .items=${this._devices(this.hass.devices, this.value)} | ||||
|                 .keyFunction=${this._keyFunction} | ||||
|                 .renderItem=${this._renderItem} | ||||
|                 @click=${this._handleItemClick} | ||||
|               > | ||||
| @@ -68,6 +69,8 @@ export class HaFilterDevices extends LitElement { | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   private _keyFunction = (device) => device?.id; | ||||
|  | ||||
|   private _renderItem = (device) => | ||||
|     html`<ha-check-list-item | ||||
|       .value=${device.id} | ||||
| @@ -109,7 +112,7 @@ export class HaFilterDevices extends LitElement { | ||||
|     this.expanded = ev.detail.expanded; | ||||
|   } | ||||
|  | ||||
|   private _devices = memoizeOne((devices: HomeAssistant["devices"]) => { | ||||
|   private _devices = memoizeOne((devices: HomeAssistant["devices"], _value) => { | ||||
|     const values = Object.values(devices); | ||||
|     return values.sort((a, b) => | ||||
|       stringCompare( | ||||
| @@ -185,11 +188,11 @@ export class HaFilterDevices extends LitElement { | ||||
|           border-radius: 50%; | ||||
|           font-weight: 400; | ||||
|           font-size: 11px; | ||||
|           background-color: var(--accent-color); | ||||
|           background-color: var(--primary-color); | ||||
|           line-height: 16px; | ||||
|           text-align: center; | ||||
|           padding: 0px 2px; | ||||
|           color: var(--text-accent-color, var(--text-primary-color)); | ||||
|           color: var(--text-primary-color); | ||||
|         } | ||||
|         ha-check-list-item { | ||||
|           width: 100%; | ||||
|   | ||||
| @@ -59,7 +59,12 @@ export class HaFilterEntities extends LitElement { | ||||
|           ? html` | ||||
|               <mwc-list class="ha-scrollbar"> | ||||
|                 <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} | ||||
|                   @click=${this._handleItemClick} | ||||
|                 > | ||||
| @@ -81,6 +86,8 @@ export class HaFilterEntities extends LitElement { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private _keyFunction = (entity) => entity?.entity_id; | ||||
|  | ||||
|   private _renderItem = (entity) => | ||||
|     html`<ha-check-list-item | ||||
|       .value=${entity.entity_id} | ||||
| @@ -119,7 +126,7 @@ export class HaFilterEntities extends LitElement { | ||||
|   } | ||||
|  | ||||
|   private _entities = memoizeOne( | ||||
|     (states: HomeAssistant["states"], type: this["type"]) => { | ||||
|     (states: HomeAssistant["states"], type: this["type"], _value) => { | ||||
|       const values = Object.values(states); | ||||
|       return values | ||||
|         .filter( | ||||
| @@ -199,11 +206,11 @@ export class HaFilterEntities extends LitElement { | ||||
|           border-radius: 50%; | ||||
|           font-weight: 400; | ||||
|           font-size: 11px; | ||||
|           background-color: var(--accent-color); | ||||
|           background-color: var(--primary-color); | ||||
|           line-height: 16px; | ||||
|           text-align: center; | ||||
|           padding: 0px 2px; | ||||
|           color: var(--text-accent-color, var(--text-primary-color)); | ||||
|           color: var(--text-primary-color); | ||||
|         } | ||||
|         ha-check-list-item { | ||||
|           --mdc-list-item-graphic-margin: 16px; | ||||
|   | ||||
| @@ -267,11 +267,11 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) { | ||||
|           border-radius: 50%; | ||||
|           font-weight: 400; | ||||
|           font-size: 11px; | ||||
|           background-color: var(--accent-color); | ||||
|           background-color: var(--primary-color); | ||||
|           line-height: 16px; | ||||
|           text-align: center; | ||||
|           padding: 0px 2px; | ||||
|           color: var(--text-accent-color, var(--text-primary-color)); | ||||
|           color: var(--text-primary-color); | ||||
|         } | ||||
|         ha-check-list-item { | ||||
|           --mdc-list-item-graphic-margin: 16px; | ||||
|   | ||||
| @@ -1,15 +1,16 @@ | ||||
| import { SelectedDetail } from "@material/mwc-list"; | ||||
| import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { repeat } from "lit/directives/repeat"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { fireEvent } from "../common/dom/fire_event"; | ||||
| import { stringCompare } from "../common/string/compare"; | ||||
| import { haStyleScrollbar } from "../resources/styles"; | ||||
| import type { HomeAssistant } from "../types"; | ||||
| import { | ||||
|   fetchIntegrationManifests, | ||||
|   IntegrationManifest, | ||||
| } from "../data/integration"; | ||||
| import { haStyleScrollbar } from "../resources/styles"; | ||||
| import type { HomeAssistant } from "../types"; | ||||
| import "./ha-domain-icon"; | ||||
|  | ||||
| @customElement("ha-filter-integrations") | ||||
| @@ -47,11 +48,15 @@ export class HaFilterIntegrations extends LitElement { | ||||
|                 multi | ||||
|                 class="ha-scrollbar" | ||||
|               > | ||||
|                 ${this._integrations(this._manifests).map( | ||||
|                 ${repeat( | ||||
|                   this._integrations(this._manifests, this.value), | ||||
|                   (i) => i.domain, | ||||
|                   (integration) => | ||||
|                     html`<ha-check-list-item | ||||
|                       .value=${integration.domain} | ||||
|                       .selected=${this.value?.includes(integration.domain)} | ||||
|                       .selected=${(this.value || []).includes( | ||||
|                         integration.domain | ||||
|                       )} | ||||
|                       graphic="icon" | ||||
|                     > | ||||
|                       <ha-domain-icon | ||||
| @@ -92,26 +97,27 @@ export class HaFilterIntegrations extends LitElement { | ||||
|     this._manifests = await fetchIntegrationManifests(this.hass); | ||||
|   } | ||||
|  | ||||
|   private _integrations = memoizeOne((manifest: IntegrationManifest[]) => | ||||
|     manifest | ||||
|       .filter( | ||||
|         (mnfst) => | ||||
|           !mnfst.integration_type || | ||||
|           !["entity", "system", "hardware"].includes(mnfst.integration_type) | ||||
|       ) | ||||
|       .sort((a, b) => | ||||
|         stringCompare( | ||||
|           a.name || a.domain, | ||||
|           b.name || b.domain, | ||||
|           this.hass.locale.language | ||||
|   private _integrations = memoizeOne( | ||||
|     (manifest: IntegrationManifest[], _value) => | ||||
|       manifest | ||||
|         .filter( | ||||
|           (mnfst) => | ||||
|             !mnfst.integration_type || | ||||
|             !["entity", "system", "hardware"].includes(mnfst.integration_type) | ||||
|         ) | ||||
|         .sort((a, b) => | ||||
|           stringCompare( | ||||
|             a.name || a.domain, | ||||
|             b.name || b.domain, | ||||
|             this.hass.locale.language | ||||
|           ) | ||||
|         ) | ||||
|       ) | ||||
|   ); | ||||
|  | ||||
|   private async _integrationsSelected( | ||||
|     ev: CustomEvent<SelectedDetail<Set<number>>> | ||||
|   ) { | ||||
|     const integrations = this._integrations(this._manifests!); | ||||
|     const integrations = this._integrations(this._manifests!, this.value); | ||||
|  | ||||
|     if (!ev.detail.index.size) { | ||||
|       fireEvent(this, "data-table-filter-changed", { | ||||
| @@ -165,11 +171,11 @@ export class HaFilterIntegrations extends LitElement { | ||||
|           border-radius: 50%; | ||||
|           font-weight: 400; | ||||
|           font-size: 11px; | ||||
|           background-color: var(--accent-color); | ||||
|           background-color: var(--primary-color); | ||||
|           line-height: 16px; | ||||
|           text-align: center; | ||||
|           padding: 0px 2px; | ||||
|           color: var(--text-accent-color, var(--text-primary-color)); | ||||
|           color: var(--text-primary-color); | ||||
|         } | ||||
|       `, | ||||
|     ]; | ||||
|   | ||||
| @@ -1,15 +1,19 @@ | ||||
| import { SelectedDetail } from "@material/mwc-list"; | ||||
| import "@material/mwc-menu/mwc-menu-surface"; | ||||
| import { mdiPlus } from "@mdi/js"; | ||||
| import { UnsubscribeFunc } from "home-assistant-js-websocket"; | ||||
| import { CSSResultGroup, LitElement, css, html, nothing } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { repeat } from "lit/directives/repeat"; | ||||
| import { computeCssColor } from "../common/color/compute-color"; | ||||
| import { fireEvent } from "../common/dom/fire_event"; | ||||
| import { | ||||
|   LabelRegistryEntry, | ||||
|   createLabelRegistryEntry, | ||||
|   subscribeLabelRegistry, | ||||
| } from "../data/label_registry"; | ||||
| import { SubscribeMixin } from "../mixins/subscribe-mixin"; | ||||
| import { showLabelDetailDialog } from "../panels/config/labels/show-dialog-label-detail"; | ||||
| import { haStyleScrollbar } from "../resources/styles"; | ||||
| import type { HomeAssistant } from "../types"; | ||||
| import "./ha-check-list-item"; | ||||
| @@ -60,30 +64,44 @@ export class HaFilterLabels extends SubscribeMixin(LitElement) { | ||||
|                 class="ha-scrollbar" | ||||
|                 multi | ||||
|               > | ||||
|                 ${this._labels.map((label) => { | ||||
|                   const color = label.color | ||||
|                     ? computeCssColor(label.color) | ||||
|                     : undefined; | ||||
|                   return html`<ha-check-list-item | ||||
|                     .value=${label.label_id} | ||||
|                     .selected=${this.value?.includes(label.label_id)} | ||||
|                     hasMeta | ||||
|                   > | ||||
|                     <ha-label style=${color ? `--color: ${color}` : ""}> | ||||
|                       ${label.icon | ||||
|                         ? html`<ha-icon | ||||
|                             slot="icon" | ||||
|                             .icon=${label.icon} | ||||
|                           ></ha-icon>` | ||||
|                         : nothing} | ||||
|                       ${label.name} | ||||
|                     </ha-label> | ||||
|                   </ha-check-list-item>`; | ||||
|                 })} | ||||
|                 ${repeat( | ||||
|                   this._labels, | ||||
|                   (label) => label.label_id, | ||||
|                   (label) => { | ||||
|                     const color = label.color | ||||
|                       ? computeCssColor(label.color) | ||||
|                       : undefined; | ||||
|                     return html`<ha-check-list-item | ||||
|                       .value=${label.label_id} | ||||
|                       .selected=${(this.value || []).includes(label.label_id)} | ||||
|                       hasMeta | ||||
|                     > | ||||
|                       <ha-label style=${color ? `--color: ${color}` : ""}> | ||||
|                         ${label.icon | ||||
|                           ? html`<ha-icon | ||||
|                               slot="icon" | ||||
|                               .icon=${label.icon} | ||||
|                             ></ha-icon>` | ||||
|                           : nothing} | ||||
|                         ${label.name} | ||||
|                       </ha-label> | ||||
|                     </ha-check-list-item>`; | ||||
|                   } | ||||
|                 )} | ||||
|               </mwc-list> | ||||
|             ` | ||||
|           : nothing} | ||||
|       </ha-expansion-panel> | ||||
|       ${this.expanded | ||||
|         ? html`<ha-list-item | ||||
|             graphic="icon" | ||||
|             @click=${this._addLabel} | ||||
|             class="add" | ||||
|           > | ||||
|             <ha-svg-icon slot="graphic" .path=${mdiPlus}></ha-svg-icon> | ||||
|             ${this.hass.localize("ui.panel.config.labels.add_label")} | ||||
|           </ha-list-item>` | ||||
|         : nothing} | ||||
|     `; | ||||
|   } | ||||
|  | ||||
| @@ -92,11 +110,17 @@ export class HaFilterLabels extends SubscribeMixin(LitElement) { | ||||
|       setTimeout(() => { | ||||
|         if (!this.expanded) return; | ||||
|         this.renderRoot.querySelector("mwc-list")!.style.height = | ||||
|           `${this.clientHeight - 49}px`; | ||||
|           `${this.clientHeight - (49 + 48)}px`; | ||||
|       }, 300); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private _addLabel() { | ||||
|     showLabelDetailDialog(this, { | ||||
|       createEntry: (values) => createLabelRegistryEntry(this.hass, values), | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private _expandedWillChange(ev) { | ||||
|     this._shouldRender = ev.detail.expanded; | ||||
|   } | ||||
| @@ -134,6 +158,7 @@ export class HaFilterLabels extends SubscribeMixin(LitElement) { | ||||
|       haStyleScrollbar, | ||||
|       css` | ||||
|         :host { | ||||
|           position: relative; | ||||
|           border-bottom: 1px solid var(--divider-color); | ||||
|         } | ||||
|         :host([expanded]) { | ||||
| @@ -158,11 +183,11 @@ export class HaFilterLabels extends SubscribeMixin(LitElement) { | ||||
|           border-radius: 50%; | ||||
|           font-weight: 400; | ||||
|           font-size: 11px; | ||||
|           background-color: var(--accent-color); | ||||
|           background-color: var(--primary-color); | ||||
|           line-height: 16px; | ||||
|           text-align: center; | ||||
|           padding: 0px 2px; | ||||
|           color: var(--text-accent-color, var(--text-primary-color)); | ||||
|           color: var(--text-primary-color); | ||||
|         } | ||||
|         .warning { | ||||
|           color: var(--error-color); | ||||
| @@ -171,6 +196,12 @@ export class HaFilterLabels extends SubscribeMixin(LitElement) { | ||||
|           --ha-label-background-color: var(--color, var(--grey-color)); | ||||
|           --ha-label-background-opacity: 0.5; | ||||
|         } | ||||
|         .add { | ||||
|           position: absolute; | ||||
|           bottom: 0; | ||||
|           right: 0; | ||||
|           left: 0; | ||||
|         } | ||||
|       `, | ||||
|     ]; | ||||
|   } | ||||
|   | ||||
| @@ -147,11 +147,11 @@ export class HaFilterStates extends LitElement { | ||||
|           border-radius: 50%; | ||||
|           font-weight: 400; | ||||
|           font-size: 11px; | ||||
|           background-color: var(--accent-color); | ||||
|           background-color: var(--primary-color); | ||||
|           line-height: 16px; | ||||
|           text-align: center; | ||||
|           padding: 0px 2px; | ||||
|           color: var(--text-accent-color, var(--text-primary-color)); | ||||
|           color: var(--text-primary-color); | ||||
|         } | ||||
|       `, | ||||
|     ]; | ||||
|   | ||||
| @@ -23,11 +23,9 @@ import { | ||||
|   getFloorAreaLookup, | ||||
|   subscribeFloorRegistry, | ||||
| } from "../data/floor_registry"; | ||||
| import { | ||||
|   showAlertDialog, | ||||
|   showPromptDialog, | ||||
| } from "../dialogs/generic/show-dialog-box"; | ||||
| import { showAlertDialog } from "../dialogs/generic/show-dialog-box"; | ||||
| import { SubscribeMixin } from "../mixins/subscribe-mixin"; | ||||
| import { showFloorRegistryDetailDialog } from "../panels/config/areas/show-dialog-floor-registry-detail"; | ||||
| import { HomeAssistant, ValueChangedEvent } from "../types"; | ||||
| import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker"; | ||||
| import "./ha-combo-box"; | ||||
| @@ -38,10 +36,14 @@ import "./ha-list-item"; | ||||
|  | ||||
| type ScorableFloorRegistryEntry = ScorableTextItem & FloorRegistryEntry; | ||||
|  | ||||
| const ADD_NEW_ID = "___ADD_NEW___"; | ||||
| const NO_FLOORS_ID = "___NO_FLOORS___"; | ||||
| const ADD_NEW_SUGGESTION_ID = "___ADD_NEW_SUGGESTION___"; | ||||
|  | ||||
| const rowRenderer: ComboBoxLitRenderer<FloorRegistryEntry> = (item) => | ||||
|   html`<ha-list-item | ||||
|     graphic="icon" | ||||
|     class=${classMap({ "add-new": item.floor_id === "add_new" })} | ||||
|     class=${classMap({ "add-new": item.floor_id === ADD_NEW_ID })} | ||||
|   > | ||||
|     <ha-floor-icon slot="graphic" .floor=${item}></ha-floor-icon> | ||||
|     ${item.name} | ||||
| @@ -146,18 +148,6 @@ export class HaFloorPicker extends SubscribeMixin(LitElement) { | ||||
|       noAdd: this["noAdd"], | ||||
|       excludeFloors: this["excludeFloors"] | ||||
|     ): FloorRegistryEntry[] => { | ||||
|       if (!floors.length) { | ||||
|         return [ | ||||
|           { | ||||
|             floor_id: "no_floors", | ||||
|             name: this.hass.localize("ui.components.floor-picker.no_floors"), | ||||
|             icon: null, | ||||
|             level: 0, | ||||
|             aliases: [], | ||||
|           }, | ||||
|         ]; | ||||
|       } | ||||
|  | ||||
|       let deviceEntityLookup: DeviceEntityDisplayLookup = {}; | ||||
|       let inputDevices: DeviceRegistryEntry[] | undefined; | ||||
|       let inputEntities: EntityRegistryDisplayEntry[] | undefined; | ||||
| @@ -282,7 +272,7 @@ export class HaFloorPicker extends SubscribeMixin(LitElement) { | ||||
|       if (areaIds) { | ||||
|         const floorAreaLookup = getFloorAreaLookup(areas); | ||||
|         outputFloors = outputFloors.filter((floor) => | ||||
|           floorAreaLookup[floor.floor_id].some((area) => | ||||
|           floorAreaLookup[floor.floor_id]?.some((area) => | ||||
|             areaIds!.includes(area.area_id) | ||||
|           ) | ||||
|         ); | ||||
| @@ -297,10 +287,10 @@ export class HaFloorPicker extends SubscribeMixin(LitElement) { | ||||
|       if (!outputFloors.length) { | ||||
|         outputFloors = [ | ||||
|           { | ||||
|             floor_id: "no_floors", | ||||
|             name: this.hass.localize("ui.components.floor-picker.no_match"), | ||||
|             floor_id: NO_FLOORS_ID, | ||||
|             name: this.hass.localize("ui.components.floor-picker.no_floors"), | ||||
|             icon: null, | ||||
|             level: 0, | ||||
|             level: null, | ||||
|             aliases: [], | ||||
|           }, | ||||
|         ]; | ||||
| @@ -311,10 +301,10 @@ export class HaFloorPicker extends SubscribeMixin(LitElement) { | ||||
|         : [ | ||||
|             ...outputFloors, | ||||
|             { | ||||
|               floor_id: "add_new", | ||||
|               floor_id: ADD_NEW_ID, | ||||
|               name: this.hass.localize("ui.components.floor-picker.add_new"), | ||||
|               icon: "mdi:plus", | ||||
|               level: 0, | ||||
|               level: null, | ||||
|               aliases: [], | ||||
|             }, | ||||
|           ]; | ||||
| @@ -341,7 +331,7 @@ export class HaFloorPicker extends SubscribeMixin(LitElement) { | ||||
|         this.excludeFloors | ||||
|       ).map((floor) => ({ | ||||
|         ...floor, | ||||
|         strings: [floor.floor_id, floor.name], // ...floor.aliases | ||||
|         strings: [floor.floor_id, floor.name, ...floor.aliases], | ||||
|       })); | ||||
|       this.comboBox.items = floors; | ||||
|       this.comboBox.filteredItems = floors; | ||||
| @@ -385,20 +375,36 @@ export class HaFloorPicker extends SubscribeMixin(LitElement) { | ||||
|  | ||||
|     const filteredItems = fuzzyFilterSort<ScorableFloorRegistryEntry>( | ||||
|       filterString, | ||||
|       target.items || [] | ||||
|       target.items?.filter( | ||||
|         (item) => ![NO_FLOORS_ID, ADD_NEW_ID].includes(item.label_id) | ||||
|       ) || [] | ||||
|     ); | ||||
|     if (!this.noAdd && filteredItems?.length === 0) { | ||||
|       this._suggestion = filterString; | ||||
|       this.comboBox.filteredItems = [ | ||||
|         { | ||||
|           floor_id: "add_new_suggestion", | ||||
|           name: this.hass.localize( | ||||
|             "ui.components.floor-picker.add_new_sugestion", | ||||
|             { name: this._suggestion } | ||||
|           ), | ||||
|           picture: null, | ||||
|         }, | ||||
|       ]; | ||||
|     if (filteredItems.length === 0) { | ||||
|       if (this.noAdd) { | ||||
|         this.comboBox.filteredItems = [ | ||||
|           { | ||||
|             floor_id: NO_FLOORS_ID, | ||||
|             name: this.hass.localize("ui.components.floor-picker.no_match"), | ||||
|             icon: null, | ||||
|             level: null, | ||||
|             aliases: [], | ||||
|           }, | ||||
|         ] as FloorRegistryEntry[]; | ||||
|       } else { | ||||
|         this._suggestion = filterString; | ||||
|         this.comboBox.filteredItems = [ | ||||
|           { | ||||
|             floor_id: ADD_NEW_SUGGESTION_ID, | ||||
|             name: this.hass.localize( | ||||
|               "ui.components.floor-picker.add_new_sugestion", | ||||
|               { name: this._suggestion } | ||||
|             ), | ||||
|             icon: "mdi:plus", | ||||
|             level: null, | ||||
|             aliases: [], | ||||
|           }, | ||||
|         ] as FloorRegistryEntry[]; | ||||
|       } | ||||
|     } else { | ||||
|       this.comboBox.filteredItems = filteredItems; | ||||
|     } | ||||
| @@ -416,11 +422,13 @@ export class HaFloorPicker extends SubscribeMixin(LitElement) { | ||||
|     ev.stopPropagation(); | ||||
|     let newValue = ev.detail.value; | ||||
|  | ||||
|     if (newValue === "no_floors") { | ||||
|     if (newValue === NO_FLOORS_ID) { | ||||
|       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) { | ||||
|         this._setValue(newValue); | ||||
|       } | ||||
| @@ -428,25 +436,12 @@ export class HaFloorPicker extends SubscribeMixin(LitElement) { | ||||
|     } | ||||
|  | ||||
|     (ev.target as any).value = this._value; | ||||
|     showPromptDialog(this, { | ||||
|       title: this.hass.localize("ui.components.floor-picker.add_dialog.title"), | ||||
|       text: this.hass.localize("ui.components.floor-picker.add_dialog.text"), | ||||
|       confirmText: this.hass.localize( | ||||
|         "ui.components.floor-picker.add_dialog.add" | ||||
|       ), | ||||
|       inputLabel: this.hass.localize( | ||||
|         "ui.components.floor-picker.add_dialog.name" | ||||
|       ), | ||||
|       defaultValue: | ||||
|         newValue === "add_new_suggestion" ? this._suggestion : undefined, | ||||
|       confirm: async (name) => { | ||||
|         if (!name) { | ||||
|           return; | ||||
|         } | ||||
|  | ||||
|     showFloorRegistryDetailDialog(this, { | ||||
|       suggestedName: newValue === ADD_NEW_SUGGESTION_ID ? this._suggestion : "", | ||||
|       createEntry: async (values) => { | ||||
|         try { | ||||
|           const floor = await createFloorRegistryEntry(this.hass, { | ||||
|             name, | ||||
|           }); | ||||
|           const floor = await createFloorRegistryEntry(this.hass, values); | ||||
|           const floors = [...this._floors!, floor]; | ||||
|           this.comboBox.filteredItems = this._getFloors( | ||||
|             floors, | ||||
| @@ -467,18 +462,16 @@ export class HaFloorPicker extends SubscribeMixin(LitElement) { | ||||
|         } catch (err: any) { | ||||
|           showAlertDialog(this, { | ||||
|             title: this.hass.localize( | ||||
|               "ui.components.floor-picker.add_dialog.failed_create_floor" | ||||
|               "ui.components.floor-picker.failed_create_floor" | ||||
|             ), | ||||
|             text: err.message, | ||||
|           }); | ||||
|         } | ||||
|       }, | ||||
|       cancel: () => { | ||||
|         this._setValue(undefined); | ||||
|         this._suggestion = undefined; | ||||
|         this.comboBox.setInputValue(""); | ||||
|       }, | ||||
|     }); | ||||
|  | ||||
|     this._suggestion = undefined; | ||||
|     this.comboBox.setInputValue(""); | ||||
|   } | ||||
|  | ||||
|   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; | ||||
|   } | ||||
| } | ||||
| @@ -385,8 +385,8 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) { | ||||
|  | ||||
|     const filteredItems = fuzzyFilterSort<ScorableLabelItem>( | ||||
|       filterString, | ||||
|       target.items?.filter((item) => | ||||
|         [NO_LABELS_ID, ADD_NEW_ID].includes(item.ignoreFilter) | ||||
|       target.items?.filter( | ||||
|         (item) => ![NO_LABELS_ID, ADD_NEW_ID].includes(item.label_id) | ||||
|       ) || [] | ||||
|     ); | ||||
|     if (filteredItems.length === 0) { | ||||
|   | ||||
| @@ -43,6 +43,7 @@ class HaLabel extends LitElement { | ||||
|           border-radius: 18px; | ||||
|           color: var(--ha-label-text-color); | ||||
|           --mdc-icon-size: 12px; | ||||
|           text-wrap: nowrap; | ||||
|         } | ||||
|         .content > * { | ||||
|           position: relative; | ||||
|   | ||||
| @@ -17,6 +17,7 @@ import "./chips/ha-input-chip"; | ||||
| import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker"; | ||||
| import "./ha-label-picker"; | ||||
| import type { HaLabelPicker } from "./ha-label-picker"; | ||||
| import { stringCompare } from "../common/string/compare"; | ||||
|  | ||||
| @customElement("ha-labels-picker") | ||||
| export class HaLabelsPicker extends SubscribeMixin(LitElement) { | ||||
| @@ -75,7 +76,7 @@ export class HaLabelsPicker extends SubscribeMixin(LitElement) { | ||||
|  | ||||
|   @property({ type: Boolean }) public required = false; | ||||
|  | ||||
|   @state() private _labels?: LabelRegistryEntry[]; | ||||
|   @state() private _labels?: { [id: string]: LabelRegistryEntry }; | ||||
|  | ||||
|   @query("ha-label-picker", true) public labelPicker!: HaLabelPicker; | ||||
|  | ||||
| @@ -92,22 +93,28 @@ export class HaLabelsPicker extends SubscribeMixin(LitElement) { | ||||
|   protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] { | ||||
|     return [ | ||||
|       subscribeLabelRegistry(this.hass.connection, (labels) => { | ||||
|         this._labels = labels; | ||||
|         const lookUp = {}; | ||||
|         labels.forEach((label) => { | ||||
|           lookUp[label.label_id] = label; | ||||
|         }); | ||||
|         this._labels = lookUp; | ||||
|       }), | ||||
|     ]; | ||||
|   } | ||||
|  | ||||
|   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` | ||||
|       ${this.value?.length | ||||
|       ${labels?.length | ||||
|         ? html`<ha-chip-set> | ||||
|             ${repeat( | ||||
|               this.value, | ||||
|               (item) => item, | ||||
|               (item, idx) => { | ||||
|                 const label = this._labels?.find( | ||||
|                   (lbl) => lbl.label_id === item | ||||
|                 ); | ||||
|               labels, | ||||
|               (label) => label?.label_id, | ||||
|               (label, idx) => { | ||||
|                 const color = label?.color | ||||
|                   ? computeCssColor(label.color) | ||||
|                   : undefined; | ||||
| @@ -168,9 +175,6 @@ export class HaLabelsPicker extends SubscribeMixin(LitElement) { | ||||
|           label.label_id, | ||||
|           values | ||||
|         ); | ||||
|         this._labels = this._labels!.map((lbl) => | ||||
|           lbl.label_id === updated.label_id ? updated : lbl | ||||
|         ); | ||||
|         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; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										41
									
								
								src/components/ha-outlined-text-field.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/components/ha-outlined-text-field.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| import { MdOutlinedTextField } from "@material/web/textfield/outlined-text-field"; | ||||
| import "element-internals-polyfill"; | ||||
| import { css } from "lit"; | ||||
| import { customElement } from "lit/decorators"; | ||||
|  | ||||
| @customElement("ha-outlined-text-field") | ||||
| export class HaOutlinedTextField extends MdOutlinedTextField { | ||||
|   static override styles = [ | ||||
|     ...super.styles, | ||||
|     css` | ||||
|       :host { | ||||
|         --md-sys-color-on-surface: var(--primary-text-color); | ||||
|         --md-sys-color-primary: var(--primary-text-color); | ||||
|         --md-outlined-text-field-input-text-color: var(--primary-text-color); | ||||
|         --md-sys-color-on-surface-variant: var(--secondary-text-color); | ||||
|         --md-outlined-field-outline-color: var(--outline-color); | ||||
|         --md-outlined-field-focus-outline-color: var(--primary-color); | ||||
|         --md-outlined-field-hover-outline-color: var(--outline-hover-color); | ||||
|       } | ||||
|       :host([dense]) { | ||||
|         --md-outlined-field-top-space: 5.5px; | ||||
|         --md-outlined-field-bottom-space: 5.5px; | ||||
|         --md-outlined-field-container-shape-start-start: 10px; | ||||
|         --md-outlined-field-container-shape-start-end: 10px; | ||||
|         --md-outlined-field-container-shape-end-end: 10px; | ||||
|         --md-outlined-field-container-shape-end-start: 10px; | ||||
|         --md-outlined-field-focus-outline-width: 1px; | ||||
|         --mdc-icon-size: var(--md-input-chip-icon-size, 18px); | ||||
|       } | ||||
|       .input { | ||||
|         font-family: Roboto, sans-serif; | ||||
|       } | ||||
|     `, | ||||
|   ]; | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   interface HTMLElementTagNameMap { | ||||
|     "ha-outlined-text-field": HaOutlinedTextField; | ||||
|   } | ||||
| } | ||||
| @@ -87,8 +87,12 @@ export class HaAreaSelector extends LitElement { | ||||
|           .label=${this.label} | ||||
|           .helper=${this.helper} | ||||
|           no-add | ||||
|           .deviceFilter=${this._filterDevices} | ||||
|           .entityFilter=${this._filterEntities} | ||||
|           .deviceFilter=${this.selector.area?.device | ||||
|             ? this._filterDevices | ||||
|             : undefined} | ||||
|           .entityFilter=${this.selector.area?.entity | ||||
|             ? this._filterEntities | ||||
|             : undefined} | ||||
|           .disabled=${this.disabled} | ||||
|           .required=${this.required} | ||||
|         ></ha-area-picker> | ||||
| @@ -102,8 +106,12 @@ export class HaAreaSelector extends LitElement { | ||||
|         .helper=${this.helper} | ||||
|         .pickAreaLabel=${this.label} | ||||
|         no-add | ||||
|         .deviceFilter=${this._filterDevices} | ||||
|         .entityFilter=${this._filterEntities} | ||||
|         .deviceFilter=${this.selector.area?.device | ||||
|           ? this._filterDevices | ||||
|           : undefined} | ||||
|         .entityFilter=${this.selector.area?.entity | ||||
|           ? this._filterEntities | ||||
|           : undefined} | ||||
|         .disabled=${this.disabled} | ||||
|         .required=${this.required} | ||||
|       ></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"), | ||||
|   statistic: () => import("./ha-selector-statistic"), | ||||
|   file: () => import("./ha-selector-file"), | ||||
|   floor: () => import("./ha-selector-floor"), | ||||
|   label: () => import("./ha-selector-label"), | ||||
|   language: () => import("./ha-selector-language"), | ||||
|   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; | ||||
|   } | ||||
| } | ||||
| @@ -1,11 +1,11 @@ | ||||
| import "@material/web/textfield/outlined-text-field"; | ||||
| import type { MdOutlinedTextField } from "@material/web/textfield/outlined-text-field"; | ||||
| import { mdiMagnify } from "@mdi/js"; | ||||
| import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit"; | ||||
| import { customElement, property, query } from "lit/decorators"; | ||||
| import { fireEvent } from "../common/dom/fire_event"; | ||||
| import { HomeAssistant } from "../types"; | ||||
| import "./ha-icon-button"; | ||||
| import "./ha-outlined-text-field"; | ||||
| import type { HaOutlinedTextField } from "./ha-outlined-text-field"; | ||||
| import "./ha-svg-icon"; | ||||
|  | ||||
| @customElement("search-input-outlined") | ||||
| @@ -30,19 +30,22 @@ class SearchInputOutlined extends LitElement { | ||||
|     this._input?.focus(); | ||||
|   } | ||||
|  | ||||
|   @query("md-outlined-text-field", true) private _input!: MdOutlinedTextField; | ||||
|   @query("ha-outlined-text-field", true) private _input!: HaOutlinedTextField; | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|     const placeholder = | ||||
|       this.placeholder || this.hass.localize("ui.common.search"); | ||||
|  | ||||
|     return html` | ||||
|       <md-outlined-text-field | ||||
|       <ha-outlined-text-field | ||||
|         .autofocus=${this.autofocus} | ||||
|         .aria-label=${this.label || this.hass.localize("ui.common.search")} | ||||
|         .placeholder=${this.placeholder || | ||||
|         this.hass.localize("ui.common.search")} | ||||
|         .placeholder=${placeholder} | ||||
|         .value=${this.filter || ""} | ||||
|         icon | ||||
|         .iconTrailing=${this.filter || this.suffix} | ||||
|         @input=${this._filterInputChanged} | ||||
|         dense | ||||
|       > | ||||
|         <slot name="prefix" slot="leading-icon"> | ||||
|           <ha-svg-icon | ||||
| @@ -51,7 +54,7 @@ class SearchInputOutlined extends LitElement { | ||||
|             .path=${mdiMagnify} | ||||
|           ></ha-svg-icon> | ||||
|         </slot> | ||||
|       </md-outlined-text-field> | ||||
|       </ha-outlined-text-field> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
| @@ -67,40 +70,21 @@ class SearchInputOutlined extends LitElement { | ||||
|     return css` | ||||
|       :host { | ||||
|         display: inline-flex; | ||||
|         /* For iOS */ | ||||
|         z-index: 0; | ||||
|       } | ||||
|       md-outlined-text-field { | ||||
|       ha-outlined-text-field { | ||||
|         display: block; | ||||
|         width: 100%; | ||||
|         --md-sys-color-on-surface: var(--primary-text-color); | ||||
|         --md-sys-color-primary: var(--primary-text-color); | ||||
|         --md-outlined-text-field-input-text-color: var(--primary-text-color); | ||||
|         --md-sys-color-on-surface-variant: var(--secondary-text-color); | ||||
|         --md-outlined-field-top-space: 5.5px; | ||||
|         --md-outlined-field-bottom-space: 5.5px; | ||||
|         --md-outlined-field-outline-color: var(--outline-color); | ||||
|         --md-outlined-field-container-shape-start-start: 10px; | ||||
|         --md-outlined-field-container-shape-start-end: 10px; | ||||
|         --md-outlined-field-container-shape-end-end: 10px; | ||||
|         --md-outlined-field-container-shape-end-start: 10px; | ||||
|         --md-outlined-field-focus-outline-width: 1px; | ||||
|         --md-outlined-field-focus-outline-color: var(--primary-color); | ||||
|       } | ||||
|       ha-svg-icon, | ||||
|       ha-icon-button { | ||||
|         display: flex; | ||||
|         --mdc-icon-size: var(--md-input-chip-icon-size, 18px); | ||||
|         color: var(--primary-text-color); | ||||
|       } | ||||
|       ha-svg-icon { | ||||
|         outline: none; | ||||
|       } | ||||
|       .clear-button { | ||||
|         --mdc-icon-size: 20px; | ||||
|       } | ||||
|       .trailing { | ||||
|         display: flex; | ||||
|         align-items: center; | ||||
|       } | ||||
|     `; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -10,7 +10,7 @@ import { computeDomain } from "../common/entity/compute_domain"; | ||||
|  | ||||
| export { subscribeEntityRegistryDisplay } from "./ws-entity_registry_display"; | ||||
|  | ||||
| type entityCategory = "config" | "diagnostic"; | ||||
| type EntityCategory = "config" | "diagnostic"; | ||||
|  | ||||
| export interface EntityRegistryDisplayEntry { | ||||
|   entity_id: string; | ||||
| @@ -20,7 +20,7 @@ export interface EntityRegistryDisplayEntry { | ||||
|   area_id?: string; | ||||
|   labels: string[]; | ||||
|   hidden?: boolean; | ||||
|   entity_category?: entityCategory; | ||||
|   entity_category?: EntityCategory; | ||||
|   translation_key?: string; | ||||
|   platform?: string; | ||||
|   display_precision?: number; | ||||
| @@ -40,7 +40,7 @@ export interface EntityRegistryDisplayEntryResponse { | ||||
|     hb?: boolean; | ||||
|     dp?: number; | ||||
|   }[]; | ||||
|   entity_categories: Record<number, entityCategory>; | ||||
|   entity_categories: Record<number, EntityCategory>; | ||||
| } | ||||
|  | ||||
| export interface EntityRegistryEntry { | ||||
| @@ -55,7 +55,7 @@ export interface EntityRegistryEntry { | ||||
|   labels: string[]; | ||||
|   disabled_by: "user" | "device" | "integration" | "config_entry" | null; | ||||
|   hidden_by: Exclude<EntityRegistryEntry["disabled_by"], "config_entry">; | ||||
|   entity_category: entityCategory | null; | ||||
|   entity_category: EntityCategory | null; | ||||
|   has_entity_name: boolean; | ||||
|   original_name?: string; | ||||
|   unique_id: string; | ||||
|   | ||||
| @@ -31,6 +31,7 @@ export type Selector = | ||||
|   | DateSelector | ||||
|   | DateTimeSelector | ||||
|   | DeviceSelector | ||||
|   | FloorSelector | ||||
|   | LegacyDeviceSelector | ||||
|   | DurationSelector | ||||
|   | EntitySelector | ||||
| @@ -170,6 +171,14 @@ export interface DeviceSelector { | ||||
|   } | null; | ||||
| } | ||||
|  | ||||
| export interface FloorSelector { | ||||
|   floor: { | ||||
|     entity?: EntitySelectorFilter | readonly EntitySelectorFilter[]; | ||||
|     device?: DeviceSelectorFilter | readonly DeviceSelectorFilter[]; | ||||
|     multiple?: boolean; | ||||
|   } | null; | ||||
| } | ||||
|  | ||||
| export interface LegacyDeviceSelector { | ||||
|   device: DeviceSelector["device"] & { | ||||
|     /** | ||||
|   | ||||
| @@ -77,6 +77,8 @@ declare global { | ||||
|   } | ||||
| } | ||||
|  | ||||
| const DEFAULT_VIEW: View = "info"; | ||||
|  | ||||
| @customElement("ha-more-info-dialog") | ||||
| export class MoreInfoDialog extends LitElement { | ||||
|   @property({ attribute: false }) public hass!: HomeAssistant; | ||||
| @@ -85,7 +87,9 @@ export class MoreInfoDialog extends LitElement { | ||||
|  | ||||
|   @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; | ||||
|  | ||||
| @@ -102,7 +106,8 @@ export class MoreInfoDialog extends LitElement { | ||||
|       this.closeDialog(); | ||||
|       return; | ||||
|     } | ||||
|     this._currView = params.view || "info"; | ||||
|     this._currView = params.view || DEFAULT_VIEW; | ||||
|     this._initialView = params.view || DEFAULT_VIEW; | ||||
|     this._childView = undefined; | ||||
|     this.large = false; | ||||
|     this._loadEntityRegistryEntry(); | ||||
| @@ -127,6 +132,7 @@ export class MoreInfoDialog extends LitElement { | ||||
|     this._entry = undefined; | ||||
|     this._childView = undefined; | ||||
|     this._infoEditMode = false; | ||||
|     this._initialView = DEFAULT_VIEW; | ||||
|     fireEvent(this, "dialog-closed", { dialog: this.localName }); | ||||
|   } | ||||
|  | ||||
| @@ -183,10 +189,15 @@ export class MoreInfoDialog extends LitElement { | ||||
|     if (this._childView) { | ||||
|       this._childView = undefined; | ||||
|     } else { | ||||
|       this.setView("info"); | ||||
|       this.setView(this._initialView); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private _resetInitialView() { | ||||
|     this._initialView = DEFAULT_VIEW; | ||||
|     this.setView(DEFAULT_VIEW); | ||||
|   } | ||||
|  | ||||
|   private _goToHistory() { | ||||
|     this.setView("history"); | ||||
|   } | ||||
| @@ -262,7 +273,10 @@ export class MoreInfoDialog extends LitElement { | ||||
|  | ||||
|     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` | ||||
|       <ha-dialog | ||||
| @@ -274,7 +288,7 @@ export class MoreInfoDialog extends LitElement { | ||||
|         flexContent | ||||
|       > | ||||
|         <ha-dialog-header slot="heading"> | ||||
|           ${isInfoView | ||||
|           ${showCloseIcon | ||||
|             ? html` | ||||
|                 <ha-icon-button | ||||
|                   slot="navigationIcon" | ||||
| @@ -297,7 +311,7 @@ export class MoreInfoDialog extends LitElement { | ||||
|           <span slot="title" .title=${title} @click=${this._enlarge}> | ||||
|             ${title} | ||||
|           </span> | ||||
|           ${isInfoView | ||||
|           ${isDefaultView | ||||
|             ? html` | ||||
|                 ${this.shouldShowHistory(domain) | ||||
|                   ? html` | ||||
| @@ -407,7 +421,34 @@ export class MoreInfoDialog extends LitElement { | ||||
|                     ` | ||||
|                   : 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> | ||||
|         <div | ||||
|           class="content" | ||||
|   | ||||
| @@ -142,9 +142,12 @@ class HassSubpage extends LitElement { | ||||
|           right: calc(16px + env(safe-area-inset-right)); | ||||
|           inset-inline-end: calc(16px + env(safe-area-inset-right)); | ||||
|           inset-inline-start: initial; | ||||
|  | ||||
|           bottom: calc(16px + env(safe-area-inset-bottom)); | ||||
|           z-index: 1; | ||||
|           display: flex; | ||||
|           flex-wrap: wrap; | ||||
|           justify-content: flex-end; | ||||
|           gap: 8px; | ||||
|         } | ||||
|         :host([narrow]) #fab.tabs { | ||||
|           bottom: calc(84px + env(safe-area-inset-bottom)); | ||||
|   | ||||
| @@ -1,12 +1,13 @@ | ||||
| import { ResizeController } from "@lit-labs/observers/resize-controller"; | ||||
| import "@lrnwebcomponents/simple-tooltip/simple-tooltip"; | ||||
| import "@material/mwc-button/mwc-button"; | ||||
| import "@material/web/divider/divider"; | ||||
| import { | ||||
|   mdiArrowDown, | ||||
|   mdiArrowUp, | ||||
|   mdiClose, | ||||
|   mdiFilterRemove, | ||||
|   mdiFilterVariant, | ||||
|   mdiFilterVariantRemove, | ||||
|   mdiFormatListChecks, | ||||
|   mdiMenuDown, | ||||
| } from "@mdi/js"; | ||||
| @@ -19,6 +20,7 @@ import { | ||||
|   nothing, | ||||
| } from "lit"; | ||||
| import { customElement, property, query, state } from "lit/decorators"; | ||||
| import { classMap } from "lit/directives/class-map"; | ||||
| import { fireEvent } from "../common/dom/fire_event"; | ||||
| import { LocalizeFunc } from "../common/translations/localize"; | ||||
| import "../components/chips/ha-assist-chip"; | ||||
| @@ -30,7 +32,10 @@ import type { | ||||
|   HaDataTable, | ||||
|   SortingDirection, | ||||
| } from "../components/data-table/ha-data-table"; | ||||
| import "../components/ha-button-menu-new"; | ||||
| import "../components/ha-dialog"; | ||||
| import { HaMenu } from "../components/ha-menu"; | ||||
| import "../components/ha-menu-item"; | ||||
| import "../components/search-input-outlined"; | ||||
| import type { HomeAssistant, Route } from "../types"; | ||||
| import "./hass-tabs-subpage"; | ||||
| @@ -173,6 +178,10 @@ export class HaTabsSubpageDataTable extends LitElement { | ||||
|  | ||||
|   @query("ha-data-table", true) private _dataTable!: HaDataTable; | ||||
|  | ||||
|   @query("#group-by-menu") private _groupByMenu!: HaMenu; | ||||
|  | ||||
|   @query("#sort-by-menu") private _sortByMenu!: HaMenu; | ||||
|  | ||||
|   private _showPaneController = new ResizeController(this, { | ||||
|     callback: (entries) => entries[0]?.contentRect.width > 750, | ||||
|   }); | ||||
| @@ -187,6 +196,14 @@ export class HaTabsSubpageDataTable extends LitElement { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private _toggleGroupBy() { | ||||
|     this._groupByMenu.open = !this._groupByMenu.open; | ||||
|   } | ||||
|  | ||||
|   private _toggleSortBy() { | ||||
|     this._sortByMenu.open = !this._sortByMenu.open; | ||||
|   } | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|     const localize = this.localizeFunc || this.hass.localize; | ||||
|     const showPane = this._showPaneController.value ?? !this.narrow; | ||||
| @@ -211,6 +228,9 @@ export class HaTabsSubpageDataTable extends LitElement { | ||||
|             class="has-dropdown select-mode-chip" | ||||
|             .active=${this._selectMode} | ||||
|             @click=${this._enableSelectMode} | ||||
|             .title=${localize( | ||||
|               "ui.components.subpage-data-table.enter_selection_mode" | ||||
|             )} | ||||
|           > | ||||
|             <ha-svg-icon slot="icon" .path=${mdiFormatListChecks}></ha-svg-icon> | ||||
|           </ha-assist-chip>` | ||||
| @@ -226,73 +246,38 @@ export class HaTabsSubpageDataTable extends LitElement { | ||||
|     </search-input-outlined>`; | ||||
|  | ||||
|     const sortByMenu = Object.values(this.columns).find((col) => col.sortable) | ||||
|       ? html`<ha-button-menu fixed> | ||||
|       ? html` | ||||
|           <ha-assist-chip | ||||
|             .label=${localize("ui.components.subpage-data-table.sort_by", { | ||||
|               sortColumn: this._sortColumn | ||||
|                 ? ` ${this.columns[this._sortColumn].title || this.columns[this._sortColumn].label}` | ||||
|                 : "", | ||||
|             })} | ||||
|             slot="trigger" | ||||
|             id="sort-by-anchor" | ||||
|             @click=${this._toggleSortBy} | ||||
|           > | ||||
|             <ha-svg-icon slot="trailing-icon" .path=${mdiMenuDown}></ha-svg-icon | ||||
|           ></ha-assist-chip> | ||||
|           ${Object.entries(this.columns).map(([id, column]) => | ||||
|             column.sortable | ||||
|               ? html`<ha-list-item | ||||
|                   .value=${id} | ||||
|                   @request-selected=${this._handleSortBy} | ||||
|                   hasMeta | ||||
|                   .activated=${id === this._sortColumn} | ||||
|                 > | ||||
|                   ${this._sortColumn === id | ||||
|                     ? html`<ha-svg-icon | ||||
|                         slot="meta" | ||||
|                         .path=${this._sortDirection === "desc" | ||||
|                           ? mdiArrowDown | ||||
|                           : mdiArrowUp} | ||||
|                       ></ha-svg-icon>` | ||||
|                     : nothing} | ||||
|                   ${column.title || column.label} | ||||
|                 </ha-list-item>` | ||||
|               : nothing | ||||
|           )} | ||||
|         </ha-button-menu>` | ||||
|             <ha-svg-icon | ||||
|               slot="trailing-icon" | ||||
|               .path=${mdiMenuDown} | ||||
|             ></ha-svg-icon> | ||||
|           </ha-assist-chip> | ||||
|         ` | ||||
|       : nothing; | ||||
|  | ||||
|     const groupByMenu = Object.values(this.columns).find((col) => col.groupable) | ||||
|       ? html`<ha-button-menu fixed> | ||||
|       ? html` | ||||
|           <ha-assist-chip | ||||
|             .label=${localize("ui.components.subpage-data-table.group_by", { | ||||
|               groupColumn: this._groupColumn | ||||
|                 ? ` ${this.columns[this._groupColumn].title || this.columns[this._groupColumn].label}` | ||||
|                 : "", | ||||
|             })} | ||||
|             slot="trigger" | ||||
|             id="group-by-anchor" | ||||
|             @click=${this._toggleGroupBy} | ||||
|           > | ||||
|             <ha-svg-icon slot="trailing-icon" .path=${mdiMenuDown}></ha-svg-icon | ||||
|           ></ha-assist-chip> | ||||
|           ${Object.entries(this.columns).map(([id, column]) => | ||||
|             column.groupable | ||||
|               ? html`<ha-list-item | ||||
|                   .value=${id} | ||||
|                   @request-selected=${this._handleGroupBy} | ||||
|                   .activated=${id === this._groupColumn} | ||||
|                 > | ||||
|                   ${column.title || column.label} | ||||
|                 </ha-list-item> ` | ||||
|               : nothing | ||||
|           )} | ||||
|           <li divider role="separator"></li> | ||||
|           <ha-list-item | ||||
|             .value=${undefined} | ||||
|             @request-selected=${this._handleGroupBy} | ||||
|             .activated=${this._groupColumn === undefined} | ||||
|             >${localize( | ||||
|               "ui.components.subpage-data-table.dont_group_by" | ||||
|             )}</ha-list-item | ||||
|           > | ||||
|         </ha-button-menu>` | ||||
|         ` | ||||
|       : nothing; | ||||
|  | ||||
|     return html` | ||||
| @@ -312,11 +297,45 @@ export class HaTabsSubpageDataTable extends LitElement { | ||||
|       > | ||||
|         ${this._selectMode | ||||
|           ? html`<div class="selection-bar" slot="toolbar"> | ||||
|               <div class="center-vertical"> | ||||
|               <div class="selection-controls"> | ||||
|                 <ha-icon-button | ||||
|                   .path=${mdiClose} | ||||
|                   @click=${this._disableSelectMode} | ||||
|                   .label=${localize( | ||||
|                     "ui.components.subpage-data-table.exit_selection_mode" | ||||
|                   )} | ||||
|                 ></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> | ||||
|                   ${localize("ui.components.subpage-data-table.selected", { | ||||
|                     selected: this.selected || "0", | ||||
| @@ -340,6 +359,9 @@ export class HaTabsSubpageDataTable extends LitElement { | ||||
|                     slot="navigationIcon" | ||||
|                     .path=${mdiClose} | ||||
|                     @click=${this._toggleFilters} | ||||
|                     .label=${localize( | ||||
|                       "ui.components.subpage-data-table.close_filter" | ||||
|                     )} | ||||
|                   ></ha-icon-button> | ||||
|                   <span slot="title" | ||||
|                     >${localize( | ||||
| @@ -348,7 +370,11 @@ export class HaTabsSubpageDataTable extends LitElement { | ||||
|                   > | ||||
|                   <ha-icon-button | ||||
|                     slot="actionItems" | ||||
|                     .path=${mdiFilterRemove} | ||||
|                     @click=${this._clearFilters} | ||||
|                     .path=${mdiFilterVariantRemove} | ||||
|                     .label=${localize( | ||||
|                       "ui.components.subpage-data-table.clear_filter" | ||||
|                     )} | ||||
|                   ></ha-icon-button> | ||||
|                 </ha-dialog-header> | ||||
|                 <div class="filter-dialog-content"> | ||||
| @@ -369,8 +395,11 @@ export class HaTabsSubpageDataTable extends LitElement { | ||||
|                     ></ha-svg-icon> | ||||
|                   </ha-assist-chip> | ||||
|                   <ha-icon-button | ||||
|                     .path=${mdiFilterRemove} | ||||
|                     .path=${mdiFilterVariantRemove} | ||||
|                     @click=${this._clearFilters} | ||||
|                     .label=${localize( | ||||
|                       "ui.components.subpage-data-table.clear_filter" | ||||
|                     )} | ||||
|                   ></ha-icon-button> | ||||
|                 </div> | ||||
|                 <div class="pane-content"> | ||||
| @@ -431,6 +460,58 @@ export class HaTabsSubpageDataTable extends LitElement { | ||||
|               </ha-data-table>`} | ||||
|         <div slot="fab"><slot name="fab"></slot></div> | ||||
|       </hass-tabs-subpage> | ||||
|       <ha-menu anchor="group-by-anchor" id="group-by-menu" positioning="fixed"> | ||||
|         ${Object.entries(this.columns).map(([id, column]) => | ||||
|           column.groupable | ||||
|             ? html` | ||||
|                 <ha-menu-item | ||||
|                   .value=${id} | ||||
|                   @click=${this._handleGroupBy} | ||||
|                   .selected=${id === this._groupColumn} | ||||
|                   class=${classMap({ selected: id === this._groupColumn })} | ||||
|                 > | ||||
|                   ${column.title || column.label} | ||||
|                 </ha-menu-item> | ||||
|               ` | ||||
|             : nothing | ||||
|         )} | ||||
|         <md-divider role="separator" tabindex="-1"></md-divider> | ||||
|         <ha-menu-item | ||||
|           .value=${undefined} | ||||
|           @click=${this._handleGroupBy} | ||||
|           .selected=${this._groupColumn === undefined} | ||||
|           class=${classMap({ selected: this._groupColumn === undefined })} | ||||
|         > | ||||
|           ${localize("ui.components.subpage-data-table.dont_group_by")} | ||||
|         </ha-menu-item> | ||||
|       </ha-menu> | ||||
|       <ha-menu anchor="sort-by-anchor" id="sort-by-menu" positioning="fixed"> | ||||
|         ${Object.entries(this.columns).map(([id, column]) => | ||||
|           column.sortable | ||||
|             ? html` | ||||
|                 <ha-menu-item | ||||
|                   .value=${id} | ||||
|                   @click=${this._handleSortBy} | ||||
|                   keep-open | ||||
|                   .selected=${id === this._sortColumn} | ||||
|                   class=${classMap({ selected: id === this._sortColumn })} | ||||
|                 > | ||||
|                   ${this._sortColumn === id | ||||
|                     ? html` | ||||
|                         <ha-svg-icon | ||||
|                           slot="end" | ||||
|                           .path=${this._sortDirection === "desc" | ||||
|                             ? mdiArrowDown | ||||
|                             : mdiArrowUp} | ||||
|                         ></ha-svg-icon> | ||||
|                       ` | ||||
|                     : nothing} | ||||
|                   ${column.title || column.label} | ||||
|                 </ha-menu-item> | ||||
|               ` | ||||
|             : nothing | ||||
|         )} | ||||
|       </ha-menu> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
| @@ -448,7 +529,6 @@ export class HaTabsSubpageDataTable extends LitElement { | ||||
|   } | ||||
|  | ||||
|   private _handleSortBy(ev) { | ||||
|     ev.stopPropagation(); | ||||
|     const columnId = ev.currentTarget.value; | ||||
|     if (!this._sortDirection || this._sortColumn !== columnId) { | ||||
|       this._sortDirection = "asc"; | ||||
| @@ -473,6 +553,14 @@ export class HaTabsSubpageDataTable extends LitElement { | ||||
|     this._dataTable.clearSelection(); | ||||
|   } | ||||
|  | ||||
|   private _selectAll() { | ||||
|     this._dataTable.selectAll(); | ||||
|   } | ||||
|  | ||||
|   private _selectNone() { | ||||
|     this._dataTable.clearSelection(); | ||||
|   } | ||||
|  | ||||
|   private _handleSearchChange(ev: CustomEvent) { | ||||
|     if (this.filter === ev.detail.value) { | ||||
|       return; | ||||
| @@ -606,16 +694,18 @@ export class HaTabsSubpageDataTable extends LitElement { | ||||
|         position: absolute; | ||||
|         top: -4px; | ||||
|         right: -4px; | ||||
|         inset-inline-end: -4px; | ||||
|         inset-inline-start: initial; | ||||
|         min-width: 16px; | ||||
|         box-sizing: border-box; | ||||
|         border-radius: 50%; | ||||
|         font-weight: 400; | ||||
|         font-size: 11px; | ||||
|         background-color: var(--accent-color); | ||||
|         background-color: var(--primary-color); | ||||
|         line-height: 16px; | ||||
|         text-align: center; | ||||
|         padding: 0px 2px; | ||||
|         color: var(--text-accent-color, var(--text-primary-color)); | ||||
|         color: var(--text-primary-color); | ||||
|       } | ||||
|  | ||||
|       .narrow-header-row { | ||||
| @@ -638,31 +728,34 @@ export class HaTabsSubpageDataTable extends LitElement { | ||||
|         padding: 8px 12px; | ||||
|         box-sizing: border-box; | ||||
|         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 { | ||||
|         display: flex; | ||||
|         align-items: center; | ||||
|         gap: 8px; | ||||
|       } | ||||
|  | ||||
|       .relative { | ||||
|         position: relative; | ||||
|       } | ||||
|  | ||||
|       .selection-bar p { | ||||
|         margin-left: 16px; | ||||
|       } | ||||
|  | ||||
|       ha-assist-chip { | ||||
|         --ha-assist-chip-container-shape: 10px; | ||||
|       } | ||||
|       ha-button-menu { | ||||
|         --mdc-list-item-meta-size: 16px; | ||||
|         --mdc-list-item-meta-display: flex; | ||||
|       } | ||||
|       ha-button-menu ha-assist-chip { | ||||
|         --md-assist-chip-trailing-space: 8px; | ||||
|       } | ||||
|  | ||||
|       .select-mode-chip { | ||||
|         --md-assist-chip-icon-label-space: 0; | ||||
| @@ -688,6 +781,12 @@ export class HaTabsSubpageDataTable extends LitElement { | ||||
|         display: flex; | ||||
|         flex-direction: column; | ||||
|       } | ||||
|  | ||||
|       #sort-by-anchor, | ||||
|       #group-by-anchor, | ||||
|       ha-button-menu-new ha-assist-chip { | ||||
|         --md-assist-chip-trailing-space: 8px; | ||||
|       } | ||||
|     `; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -344,6 +344,10 @@ class HassTabsSubpage extends LitElement { | ||||
|           inset-inline-start: initial; | ||||
|           bottom: calc(16px + env(safe-area-inset-bottom)); | ||||
|           z-index: 1; | ||||
|           display: flex; | ||||
|           flex-wrap: wrap; | ||||
|           justify-content: flex-end; | ||||
|           gap: 8px; | ||||
|         } | ||||
|         :host([narrow]) #fab.tabs { | ||||
|           bottom: calc(84px + env(safe-area-inset-bottom)); | ||||
|   | ||||
| @@ -27,7 +27,7 @@ class NotificationManager extends LitElement { | ||||
|   @query("ha-toast") private _toast!: HaToast | undefined; | ||||
|  | ||||
|   public async showDialog(parameters: ShowToastParams) { | ||||
|     if (this._parameters) { | ||||
|     if (this._parameters && this._parameters.message !== parameters.message) { | ||||
|       this._parameters = undefined; | ||||
|       await this.updateComplete; | ||||
|     } | ||||
|   | ||||
| @@ -52,7 +52,9 @@ class DialogAreaDetail extends LitElement { | ||||
|   ): Promise<void> { | ||||
|     this._params = params; | ||||
|     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._labels = this._params.entry ? this._params.entry.labels : []; | ||||
|     this._picture = this._params.entry?.picture || null; | ||||
|   | ||||
| @@ -38,7 +38,9 @@ class DialogFloorDetail extends LitElement { | ||||
|   ): Promise<void> { | ||||
|     this._params = params; | ||||
|     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._icon = this._params.entry?.icon || null; | ||||
|     this._level = this._params.entry?.level ?? null; | ||||
| @@ -213,6 +215,9 @@ class DialogFloorDetail extends LitElement { | ||||
|           display: block; | ||||
|           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-icon-button"; | ||||
| import "../../../components/ha-svg-icon"; | ||||
| import "../../../components/ha-sortable"; | ||||
| import { | ||||
|   AreaRegistryEntry, | ||||
|   createAreaRegistryEntry, | ||||
|   updateAreaRegistryEntry, | ||||
| } from "../../../data/area_registry"; | ||||
| import { | ||||
|   FloorRegistryEntry, | ||||
| @@ -50,6 +52,10 @@ import { | ||||
| } from "./show-dialog-area-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") | ||||
| export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) { | ||||
|   @property({ attribute: false }) public hass!: HomeAssistant; | ||||
| @@ -187,13 +193,22 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) { | ||||
|                     > | ||||
|                   </ha-button-menu> | ||||
|                 </div> | ||||
|                 <div class="areas"> | ||||
|                   ${floor.areas.map((area) => this._renderArea(area))} | ||||
|                 </div> | ||||
|                 <ha-sortable | ||||
|                   handle-selector="a" | ||||
|                   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>` | ||||
|           )} | ||||
|           ${areasAndFloors?.unassisgnedAreas.length | ||||
|             ? html`<div class="unassigned"> | ||||
|             ? html`<div class="floor"> | ||||
|                 <div class="header"> | ||||
|                   <h2> | ||||
|                     ${this.hass.localize( | ||||
| @@ -201,11 +216,20 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) { | ||||
|                     )} | ||||
|                   </h2> | ||||
|                 </div> | ||||
|                 <div class="areas"> | ||||
|                   ${areasAndFloors?.unassisgnedAreas.map((area) => | ||||
|                     this._renderArea(area) | ||||
|                   )} | ||||
|                 </div> | ||||
|                 <ha-sortable | ||||
|                   handle-selector="a" | ||||
|                   draggable-selector="a" | ||||
|                   @item-moved=${this._areaMoved} | ||||
|                   group="floor" | ||||
|                   .options=${SORT_OPTIONS} | ||||
|                   .path=${UNASSIGNED_PATH} | ||||
|                 > | ||||
|                   <div class="areas"> | ||||
|                     ${areasAndFloors?.unassisgnedAreas.map((area) => | ||||
|                       this._renderArea(area) | ||||
|                     )} | ||||
|                   </div> | ||||
|                 </ha-sortable> | ||||
|               </div>` | ||||
|             : nothing} | ||||
|         </div> | ||||
| @@ -281,6 +305,29 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) { | ||||
|     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>) { | ||||
|     const floor = (ev.currentTarget as any).floor; | ||||
|     switch (ev.detail.index) { | ||||
| @@ -424,7 +471,6 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) { | ||||
|       } | ||||
|       .floor { | ||||
|         --primary-color: var(--secondary-text-color); | ||||
|         margin-inline-end: 8px; | ||||
|       } | ||||
|       .warning { | ||||
|         color: var(--error-color); | ||||
|   | ||||
| @@ -6,6 +6,7 @@ import { | ||||
|  | ||||
| export interface AreaRegistryDetailDialogParams { | ||||
|   entry?: AreaRegistryEntry; | ||||
|   suggestedName?: string; | ||||
|   createEntry?: (values: AreaRegistryEntryMutableParams) => Promise<unknown>; | ||||
|   updateEntry?: ( | ||||
|     updates: Partial<AreaRegistryEntryMutableParams> | ||||
|   | ||||
| @@ -6,6 +6,7 @@ import { | ||||
|  | ||||
| export interface FloorRegistryDetailDialogParams { | ||||
|   entry?: FloorRegistryEntry; | ||||
|   suggestedName?: string; | ||||
|   createEntry?: (values: FloorRegistryEntryMutableParams) => Promise<unknown>; | ||||
|   updateEntry?: ( | ||||
|     updates: Partial<FloorRegistryEntryMutableParams> | ||||
|   | ||||
| @@ -556,7 +556,7 @@ class DialogAddAutomationElement extends LitElement implements HassDialog { | ||||
|                   ></ha-svg-icon | ||||
|                   ><ha-svg-icon slot="end" .path=${mdiPlus}></ha-svg-icon> | ||||
|                 </ha-list-item-new> | ||||
|                 <md-divider></md-divider>` | ||||
|                 <md-divider role="separator" tabindex="-1"></md-divider>` | ||||
|             : ""} | ||||
|           ${repeat( | ||||
|             items, | ||||
|   | ||||
| @@ -1,16 +1,21 @@ | ||||
| import { consume } from "@lit-labs/context"; | ||||
| import "@lrnwebcomponents/simple-tooltip/simple-tooltip"; | ||||
| import "@material/web/divider/divider"; | ||||
| import { | ||||
|   mdiChevronRight, | ||||
|   mdiCog, | ||||
|   mdiContentDuplicate, | ||||
|   mdiDelete, | ||||
|   mdiDotsVertical, | ||||
|   mdiHelpCircle, | ||||
|   mdiInformationOutline, | ||||
|   mdiMenuDown, | ||||
|   mdiPlay, | ||||
|   mdiPlayCircleOutline, | ||||
|   mdiPlus, | ||||
|   mdiRobotHappy, | ||||
|   mdiStopCircleOutline, | ||||
|   mdiTag, | ||||
|   mdiToggleSwitch, | ||||
|   mdiToggleSwitchOffOutline, | ||||
|   mdiTransitConnection, | ||||
| } from "@mdi/js"; | ||||
| import { differenceInDays } from "date-fns/esm"; | ||||
| @@ -18,14 +23,16 @@ import { UnsubscribeFunc } from "home-assistant-js-websocket"; | ||||
| import { | ||||
|   CSSResultGroup, | ||||
|   LitElement, | ||||
|   PropertyValues, | ||||
|   TemplateResult, | ||||
|   css, | ||||
|   html, | ||||
|   nothing, | ||||
| } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { customElement, property, query, state } from "lit/decorators"; | ||||
| import { styleMap } from "lit/directives/style-map"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { computeCssColor } from "../../../common/color/compute-color"; | ||||
| import { isComponentLoaded } from "../../../common/config/is_component_loaded"; | ||||
| import { formatShortDateTime } from "../../../common/datetime/format_date_time"; | ||||
| import { relativeTime } from "../../../common/datetime/relative_time"; | ||||
| @@ -37,16 +44,23 @@ import "../../../components/chips/ha-assist-chip"; | ||||
| import type { | ||||
|   DataTableColumnContainer, | ||||
|   RowClickedEvent, | ||||
|   SelectionChangedEvent, | ||||
| } from "../../../components/data-table/ha-data-table"; | ||||
| import "../../../components/data-table/ha-data-table-labels"; | ||||
| import "../../../components/entity/ha-entity-toggle"; | ||||
| import "../../../components/ha-fab"; | ||||
| import "../../../components/ha-filter-floor-areas"; | ||||
| import "../../../components/ha-filter-blueprints"; | ||||
| import "../../../components/ha-filter-categories"; | ||||
| import "../../../components/ha-filter-devices"; | ||||
| import "../../../components/ha-filter-entities"; | ||||
| import "../../../components/ha-filter-floor-areas"; | ||||
| import "../../../components/ha-filter-labels"; | ||||
| import "../../../components/ha-icon-button"; | ||||
| 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 { | ||||
|   AutomationEntity, | ||||
| @@ -63,7 +77,15 @@ import { | ||||
| } from "../../../data/category_registry"; | ||||
| import { fullEntitiesContext } from "../../../data/context"; | ||||
| import { UNAVAILABLE } from "../../../data/entity"; | ||||
| import { EntityRegistryEntry } from "../../../data/entity_registry"; | ||||
| import { | ||||
|   EntityRegistryEntry, | ||||
|   UpdateEntityRegistryEntryResult, | ||||
|   updateEntityRegistryEntry, | ||||
| } from "../../../data/entity_registry"; | ||||
| import { | ||||
|   LabelRegistryEntry, | ||||
|   subscribeLabelRegistry, | ||||
| } from "../../../data/label_registry"; | ||||
| import { findRelated } from "../../../data/search"; | ||||
| import { | ||||
|   showAlertDialog, | ||||
| @@ -72,17 +94,12 @@ import { | ||||
| import "../../../layouts/hass-tabs-subpage-data-table"; | ||||
| import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; | ||||
| import { haStyle } from "../../../resources/styles"; | ||||
| import { HomeAssistant, Route } from "../../../types"; | ||||
| import { HomeAssistant, Route, ServiceCallResponse } from "../../../types"; | ||||
| import { documentationUrl } from "../../../util/documentation-url"; | ||||
| import { turnOnOffEntity } from "../../lovelace/common/entity/turn-on-off-entity"; | ||||
| import { showAssignCategoryDialog } from "../category/show-dialog-assign-category"; | ||||
| import { configSections } from "../ha-panel-config"; | ||||
| import { showNewAutomationDialog } from "./show-dialog-new-automation"; | ||||
| import "../../../components/data-table/ha-data-table-labels"; | ||||
| import { | ||||
|   LabelRegistryEntry, | ||||
|   subscribeLabelRegistry, | ||||
| } from "../../../data/label_registry"; | ||||
| import "../../../components/ha-filter-labels"; | ||||
|  | ||||
| type AutomationItem = AutomationEntity & { | ||||
|   name: string; | ||||
| @@ -115,6 +132,8 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { | ||||
|  | ||||
|   @state() private _expandedFilter?: string; | ||||
|  | ||||
|   @state() private _selected: string[] = []; | ||||
|  | ||||
|   @state() | ||||
|   _categories!: CategoryRegistryEntry[]; | ||||
|  | ||||
| @@ -125,6 +144,10 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { | ||||
|   @consume({ context: fullEntitiesContext, subscribe: true }) | ||||
|   _entityReg!: EntityRegistryEntry[]; | ||||
|  | ||||
|   @state() private _overflowAutomation?: AutomationItem; | ||||
|  | ||||
|   @query("#overflow-menu") private _overflowMenu!: HaMenu; | ||||
|  | ||||
|   private _automations = memoizeOne( | ||||
|     ( | ||||
|       automations: AutomationEntity[], | ||||
| @@ -273,82 +296,33 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { | ||||
|       columns.actions = { | ||||
|         title: "", | ||||
|         width: "64px", | ||||
|         type: "overflow-menu", | ||||
|         type: "icon-button", | ||||
|         template: (automation) => html` | ||||
|           <ha-icon-overflow-menu | ||||
|             .hass=${this.hass} | ||||
|             narrow | ||||
|             .items=${[ | ||||
|               { | ||||
|                 path: mdiInformationOutline, | ||||
|                 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> | ||||
|           <ha-icon-button | ||||
|             .automation=${automation} | ||||
|             .label=${this.hass.localize("ui.common.overflow_menu")} | ||||
|             .path=${mdiDotsVertical} | ||||
|             @click=${this._showOverflowMenu} | ||||
|           ></ha-icon-button> | ||||
|         `, | ||||
|       }; | ||||
|       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>)[] { | ||||
|     return [ | ||||
|       subscribeCategoryRegistry( | ||||
| @@ -365,18 +339,58 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { | ||||
|   } | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|     const categoryItems = html`${this._categories?.map( | ||||
|         (category) => | ||||
|           html`<ha-menu-item | ||||
|             .value=${category.category_id} | ||||
|             @click=${this._handleBulkCategory} | ||||
|           > | ||||
|             ${category.icon | ||||
|               ? html`<ha-icon slot="start" .icon=${category.icon}></ha-icon>` | ||||
|               : html`<ha-svg-icon slot="start" .path=${mdiTag}></ha-svg-icon>`} | ||||
|             <div slot="headline">${category.name}</div> | ||||
|           </ha-menu-item>` | ||||
|       )} | ||||
|       <ha-menu-item .value=${null} @click=${this._handleBulkCategory}> | ||||
|         <div slot="headline"> | ||||
|           ${this.hass.localize( | ||||
|             "ui.panel.config.automation.picker.bulk_actions.no_category" | ||||
|           )} | ||||
|         </div> | ||||
|       </ha-menu-item>`; | ||||
|     const labelItems = html` ${this._labels?.map((label) => { | ||||
|       const color = label.color ? computeCssColor(label.color) : undefined; | ||||
|       return html`<ha-menu-item | ||||
|         .value=${label.label_id} | ||||
|         @click=${this._handleBulkLabel} | ||||
|       > | ||||
|         <ha-label style=${color ? `--color: ${color}` : ""}> | ||||
|           ${label.icon | ||||
|             ? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>` | ||||
|             : nothing} | ||||
|           ${label.name} | ||||
|         </ha-label> | ||||
|       </ha-menu-item>`; | ||||
|     })}`; | ||||
|  | ||||
|     return html` | ||||
|       <hass-tabs-subpage-data-table | ||||
|         .hass=${this.hass} | ||||
|         .narrow=${this.narrow} | ||||
|         back-path="/config" | ||||
|         .backPath=${ | ||||
|           this._searchParms.has("historyBack") ? undefined : "/config" | ||||
|         } | ||||
|         id="entity_id" | ||||
|         .route=${this.route} | ||||
|         .tabs=${configSections.automations} | ||||
|         selectable | ||||
|         .selected=${this._selected.length} | ||||
|         @selection-changed=${this._handleSelectionChanged} | ||||
|         hasFilters | ||||
|         .filters=${Object.values(this._filters).filter( | ||||
|           (filter) => filter.value?.length | ||||
|         ).length} | ||||
|         .filters=${ | ||||
|           Object.values(this._filters).filter((filter) => filter.value?.length) | ||||
|             .length | ||||
|         } | ||||
|         .columns=${this._columns( | ||||
|           this.narrow, | ||||
|           this.hass.localize, | ||||
| @@ -465,36 +479,156 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { | ||||
|           .narrow=${this.narrow} | ||||
|           @expanded-changed=${this._filterExpanded} | ||||
|         ></ha-filter-blueprints> | ||||
|         ${!this.automations.length | ||||
|           ? html`<div class="empty" slot="empty"> | ||||
|               <ha-svg-icon .path=${mdiRobotHappy}></ha-svg-icon> | ||||
|               <h1> | ||||
|           ${ | ||||
|             !this.narrow | ||||
|               ? html`<ha-button-menu-new slot="selection-bar"> | ||||
|                     <ha-assist-chip | ||||
|                       slot="trigger" | ||||
|                       .label=${this.hass.localize( | ||||
|                         "ui.panel.config.automation.picker.bulk_actions.move_category" | ||||
|                       )} | ||||
|                     > | ||||
|                       <ha-svg-icon | ||||
|                         slot="trailing-icon" | ||||
|                         .path=${mdiMenuDown} | ||||
|                       ></ha-svg-icon> | ||||
|                     </ha-assist-chip> | ||||
|                     ${categoryItems} | ||||
|                   </ha-button-menu-new> | ||||
|                   ${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( | ||||
|                   "ui.panel.config.automation.picker.empty_header" | ||||
|                   "ui.panel.config.automation.picker.bulk_actions.enable" | ||||
|                 )} | ||||
|               </h1> | ||||
|               <p> | ||||
|               </div> | ||||
|             </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( | ||||
|                   "ui.panel.config.automation.picker.empty_text_1" | ||||
|                   "ui.panel.config.automation.picker.bulk_actions.disable" | ||||
|                 )} | ||||
|               </p> | ||||
|               <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} | ||||
|               </div> | ||||
|             </ha-menu-item> | ||||
|           </ha-button-menu-new> | ||||
|         ${ | ||||
|           !this.automations.length | ||||
|             ? html`<div class="empty" slot="empty"> | ||||
|                 <ha-svg-icon .path=${mdiRobotHappy}></ha-svg-icon> | ||||
|                 <h1> | ||||
|                   ${this.hass.localize( | ||||
|                     "ui.panel.config.automation.picker.empty_header" | ||||
|                   )} | ||||
|                 </h1> | ||||
|                 <p> | ||||
|                   ${this.hass.localize( | ||||
|                     "ui.panel.config.automation.picker.empty_text_1" | ||||
|                   )} | ||||
|                 </p> | ||||
|                 <p> | ||||
|                   ${this.hass.localize( | ||||
|                     "ui.panel.config.automation.picker.empty_text_2", | ||||
|                     { user: this.hass.user?.name || "Alice" } | ||||
|                   )} | ||||
|                 </p> | ||||
|                 <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 | ||||
|           slot="fab" | ||||
|           .label=${this.hass.localize( | ||||
| @@ -506,13 +640,97 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { | ||||
|           <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> | ||||
|         </ha-fab> | ||||
|       </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> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   protected updated(changedProps: PropertyValues) { | ||||
|     super.updated(changedProps); | ||||
|     if (changedProps.has("_entityReg")) { | ||||
|       this._applyFilters(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   firstUpdated() { | ||||
|     if (this._searchParms.has("blueprint")) { | ||||
|       this._filterBlueprint(); | ||||
|     } | ||||
|     if (this._searchParms.has("label")) { | ||||
|       this._filterLabel(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private _filterExpanded(ev) { | ||||
| @@ -600,6 +818,21 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { | ||||
|     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() { | ||||
|     const blueprint = this._searchParms.get("blueprint"); | ||||
|     if (!blueprint) { | ||||
| @@ -625,15 +858,29 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { | ||||
|     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 }); | ||||
|   } | ||||
|  | ||||
|   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); | ||||
|   } | ||||
|  | ||||
|   private _editCategory(automation: any) { | ||||
|   private _editCategory(ev) { | ||||
|     const automation = ev.currentTarget.parentElement.anchorElement.automation; | ||||
|  | ||||
|     const entityReg = this._entityReg.find( | ||||
|       (reg) => reg.entity_id === automation.entity_id | ||||
|     ); | ||||
| @@ -654,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) { | ||||
|       showAlertDialog(this, { | ||||
|         text: this.hass.localize( | ||||
| @@ -668,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"; | ||||
|     await this.hass.callService("automation", service, { | ||||
|       entity_id: automation.entity_id, | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private async _deleteConfirm(automation) { | ||||
|   private async _deleteConfirm(ev) { | ||||
|     const automation = ev.currentTarget.parentElement.anchorElement.automation; | ||||
|  | ||||
|     showConfirmationDialog(this, { | ||||
|       title: this.hass.localize( | ||||
|         "ui.panel.config.automation.picker.delete_confirm_title" | ||||
| @@ -709,7 +962,9 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private async duplicate(automation) { | ||||
|   private async _duplicate(ev) { | ||||
|     const automation = ev.currentTarget.parentElement.anchorElement.automation; | ||||
|  | ||||
|     try { | ||||
|       const config = await fetchAutomationFileConfig( | ||||
|         this.hass, | ||||
| @@ -768,6 +1023,12 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private _handleSelectionChanged( | ||||
|     ev: HASSDomEvent<SelectionChangedEvent> | ||||
|   ): void { | ||||
|     this._selected = ev.detail.value; | ||||
|   } | ||||
|  | ||||
|   private _createNew() { | ||||
|     if (isComponentLoaded(this.hass, "blueprint")) { | ||||
|       showNewAutomationDialog(this, { mode: "automation" }); | ||||
| @@ -776,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 { | ||||
|     return [ | ||||
|       haStyle, | ||||
| @@ -791,6 +1094,16 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { | ||||
|           --mdc-icon-size: 80px; | ||||
|           max-width: 500px; | ||||
|         } | ||||
|         ha-assist-chip { | ||||
|           --ha-assist-chip-container-shape: 10px; | ||||
|         } | ||||
|         ha-button-menu-new ha-assist-chip { | ||||
|           --md-assist-chip-trailing-space: 8px; | ||||
|         } | ||||
|         ha-label { | ||||
|           --ha-label-background-color: var(--color, var(--grey-color)); | ||||
|           --ha-label-background-opacity: 0.5; | ||||
|         } | ||||
|       `, | ||||
|     ]; | ||||
|   } | ||||
|   | ||||
| @@ -179,16 +179,16 @@ export class HaCategoryPicker extends SubscribeMixin(LitElement) { | ||||
|  | ||||
|     const filteredItems = fuzzyFilterSort<ScorableCategoryRegistryEntry>( | ||||
|       filterString, | ||||
|       target.items || [] | ||||
|       target.items?.filter( | ||||
|         (item) => ![NO_CATEGORIES_ID, ADD_NEW_ID].includes(item.category_id) | ||||
|       ) || [] | ||||
|     ); | ||||
|     if (filteredItems?.length === 0) { | ||||
|       if (this.noAdd) { | ||||
|         this.comboBox.filteredItems = [ | ||||
|           { | ||||
|             category_id: NO_CATEGORIES_ID, | ||||
|             name: this.hass.localize( | ||||
|               "ui.components.category-picker.no_categories" | ||||
|             ), | ||||
|             name: this.hass.localize("ui.components.category-picker.no_match"), | ||||
|             icon: null, | ||||
|           }, | ||||
|         ] as ScorableCategoryRegistryEntry[]; | ||||
| @@ -224,6 +224,8 @@ export class HaCategoryPicker extends SubscribeMixin(LitElement) { | ||||
|  | ||||
|     if (newValue === NO_CATEGORIES_ID) { | ||||
|       newValue = ""; | ||||
|       this.comboBox.setInputValue(""); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     if (![ADD_NEW_SUGGESTION_ID, ADD_NEW_ID].includes(newValue)) { | ||||
|   | ||||
| @@ -10,6 +10,7 @@ import { | ||||
|   nothing, | ||||
| } from "lit"; | ||||
|  | ||||
| import { UnsubscribeFunc } from "home-assistant-js-websocket"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { HASSDomEvent } from "../../../common/dom/fire_event"; | ||||
| @@ -24,16 +25,18 @@ import { | ||||
|   DataTableColumnContainer, | ||||
|   RowClickedEvent, | ||||
| } from "../../../components/data-table/ha-data-table"; | ||||
| import "../../../components/data-table/ha-data-table-labels"; | ||||
| import "../../../components/entity/ha-battery-icon"; | ||||
| import "../../../components/ha-alert"; | ||||
| import "../../../components/ha-button-menu"; | ||||
| import "../../../components/ha-check-list-item"; | ||||
| import "../../../components/ha-fab"; | ||||
| import "../../../components/ha-filter-devices"; | ||||
| import "../../../components/ha-filter-floor-areas"; | ||||
| import "../../../components/ha-filter-integrations"; | ||||
| import "../../../components/ha-filter-labels"; | ||||
| import "../../../components/ha-filter-states"; | ||||
| import "../../../components/ha-icon-button"; | ||||
| import "../../../components/ha-alert"; | ||||
| import { ConfigEntry, sortConfigEntries } from "../../../data/config_entries"; | ||||
| import { fullEntitiesContext } from "../../../data/context"; | ||||
| import { | ||||
| @@ -47,7 +50,12 @@ import { | ||||
|   findBatteryEntity, | ||||
| } from "../../../data/entity_registry"; | ||||
| import { IntegrationManifest } from "../../../data/integration"; | ||||
| import { | ||||
|   LabelRegistryEntry, | ||||
|   subscribeLabelRegistry, | ||||
| } from "../../../data/label_registry"; | ||||
| import "../../../layouts/hass-tabs-subpage-data-table"; | ||||
| import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; | ||||
| import { haStyle } from "../../../resources/styles"; | ||||
| import { HomeAssistant, Route } from "../../../types"; | ||||
| import { brandsUrl } from "../../../util/brands-url"; | ||||
| @@ -60,10 +68,11 @@ interface DeviceRowData extends DeviceRegistryEntry { | ||||
|   area?: string; | ||||
|   integration?: string; | ||||
|   battery_entity?: [string | undefined, string | undefined]; | ||||
|   label_entries: EntityRegistryEntry[]; | ||||
| } | ||||
|  | ||||
| @customElement("ha-config-devices-dashboard") | ||||
| export class HaConfigDeviceDashboard extends LitElement { | ||||
| export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) { | ||||
|   @property({ attribute: false }) public hass!: HomeAssistant; | ||||
|  | ||||
|   @property({ type: Boolean }) public narrow = false; | ||||
| @@ -91,6 +100,9 @@ export class HaConfigDeviceDashboard extends LitElement { | ||||
|  | ||||
|   @state() private _expandedFilter?: string; | ||||
|  | ||||
|   @state() | ||||
|   _labels!: LabelRegistryEntry[]; | ||||
|  | ||||
|   private _ignoreLocationChange = false; | ||||
|  | ||||
|   public connectedCallback() { | ||||
| @@ -173,6 +185,23 @@ export class HaConfigDeviceDashboard extends 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() { | ||||
| @@ -190,11 +219,17 @@ export class HaConfigDeviceDashboard extends LitElement { | ||||
|         string, | ||||
|         { value: string[] | undefined; items: Set<string> | undefined } | ||||
|       >, | ||||
|       localize: LocalizeFunc | ||||
|       localize: LocalizeFunc, | ||||
|       labelReg?: LabelRegistryEntry[] | ||||
|     ) => { | ||||
|       // Some older installations might have devices pointing at invalid entryIDs | ||||
|       // So we guard for that. | ||||
|       let outputDevices: DeviceRowData[] = Object.values(devices); | ||||
|       let outputDevices: DeviceRowData[] = Object.values(devices).map( | ||||
|         (device) => ({ | ||||
|           ...device, | ||||
|           label_entries: [], | ||||
|         }) | ||||
|       ); | ||||
|  | ||||
|       const deviceEntityLookup: DeviceEntityLookup = {}; | ||||
|       for (const entity of entities) { | ||||
| @@ -221,16 +256,16 @@ export class HaConfigDeviceDashboard extends LitElement { | ||||
|  | ||||
|       const filteredDomains = new Set<string>(); | ||||
|  | ||||
|       Object.entries(filters).forEach(([key, flter]) => { | ||||
|         if (key === "config_entry" && flter.value?.length) { | ||||
|       Object.entries(filters).forEach(([key, filter]) => { | ||||
|         if (key === "config_entry" && filter.value?.length) { | ||||
|           outputDevices = outputDevices.filter((device) => | ||||
|             device.config_entries.some((entryId) => | ||||
|               flter.value?.includes(entryId) | ||||
|               filter.value?.includes(entryId) | ||||
|             ) | ||||
|           ); | ||||
|  | ||||
|           const configEntries = entries.filter( | ||||
|             (entry) => entry.entry_id && flter.value?.includes(entry.entry_id) | ||||
|             (entry) => entry.entry_id && filter.value?.includes(entry.entry_id) | ||||
|           ); | ||||
|  | ||||
|           configEntries.forEach((configEntry) => { | ||||
| @@ -239,17 +274,21 @@ export class HaConfigDeviceDashboard extends LitElement { | ||||
|           if (configEntries.length === 1) { | ||||
|             filteredConfigEntry = configEntries[0]; | ||||
|           } | ||||
|         } else if (key === "ha-filter-integrations" && flter.value?.length) { | ||||
|         } else if (key === "ha-filter-integrations" && filter.value?.length) { | ||||
|           const entryIds = entries | ||||
|             .filter((entry) => flter.value!.includes(entry.domain)) | ||||
|             .filter((entry) => filter.value!.includes(entry.domain)) | ||||
|             .map((entry) => entry.entry_id); | ||||
|           outputDevices = outputDevices.filter((device) => | ||||
|             device.config_entries.some((entryId) => entryIds.includes(entryId)) | ||||
|           ); | ||||
|           flter.value!.forEach((domain) => filteredDomains.add(domain)); | ||||
|         } else if (flter.items) { | ||||
|           filter.value!.forEach((domain) => filteredDomains.add(domain)); | ||||
|         } else if (key === "ha-filter-labels" && filter.value?.length) { | ||||
|           outputDevices = outputDevices.filter((device) => | ||||
|             flter.items!.has(device.id) | ||||
|             device.labels.some((lbl) => filter.value!.includes(lbl)) | ||||
|           ); | ||||
|         } else if (filter.items) { | ||||
|           outputDevices = outputDevices.filter((device) => | ||||
|             filter.items!.has(device.id) | ||||
|           ); | ||||
|         } | ||||
|       }); | ||||
| @@ -270,6 +309,12 @@ export class HaConfigDeviceDashboard extends LitElement { | ||||
|             .map((entId) => entryLookup[entId]), | ||||
|           manifestLookup | ||||
|         ); | ||||
|  | ||||
|         const labels = labelReg && device?.labels; | ||||
|         const labelsEntries = (labels || []).map( | ||||
|           (lbl) => labelReg!.find((label) => label.label_id === lbl)! | ||||
|         ); | ||||
|  | ||||
|         return { | ||||
|           ...device, | ||||
|           name: computeDeviceName( | ||||
| @@ -306,6 +351,7 @@ export class HaConfigDeviceDashboard extends LitElement { | ||||
|             this.hass.states[ | ||||
|               this._batteryEntity(device.id, deviceEntityLookup) || "" | ||||
|             ]?.state, | ||||
|           label_entries: labelsEntries, | ||||
|         }; | ||||
|       }); | ||||
|  | ||||
| @@ -351,8 +397,15 @@ export class HaConfigDeviceDashboard extends LitElement { | ||||
|         direction: "asc", | ||||
|         grows: true, | ||||
|         template: (device) => html` | ||||
|           ${device.name} | ||||
|           <div style="font-size: 14px;">${device.name}</div> | ||||
|           <div class="secondary">${device.area} | ${device.integration}</div> | ||||
|           ${device.label_entries.length | ||||
|             ? html` | ||||
|                 <ha-data-table-labels | ||||
|                   .labels=${device.label_entries} | ||||
|                 ></ha-data-table-labels> | ||||
|               ` | ||||
|             : nothing} | ||||
|         `, | ||||
|       }; | ||||
|     } else { | ||||
| @@ -361,8 +414,18 @@ export class HaConfigDeviceDashboard extends LitElement { | ||||
|         main: true, | ||||
|         sortable: true, | ||||
|         filterable: true, | ||||
|         grows: true, | ||||
|         direction: "asc", | ||||
|         grows: true, | ||||
|         template: (device) => html` | ||||
|           <div style="font-size: 14px;">${device.name}</div> | ||||
|           ${device.label_entries.length | ||||
|             ? html` | ||||
|                 <ha-data-table-labels | ||||
|                   .labels=${device.label_entries} | ||||
|                 ></ha-data-table-labels> | ||||
|               ` | ||||
|             : nothing} | ||||
|         `, | ||||
|       }; | ||||
|     } | ||||
|  | ||||
| @@ -441,9 +504,25 @@ export class HaConfigDeviceDashboard extends LitElement { | ||||
|           ? this.hass.localize("ui.panel.config.devices.disabled") | ||||
|           : "", | ||||
|     }; | ||||
|     columns.labels = { | ||||
|       title: "", | ||||
|       hidden: true, | ||||
|       filterable: true, | ||||
|       template: (device) => | ||||
|         device.label_entries.map((lbl) => lbl.name).join(" "), | ||||
|     }; | ||||
|  | ||||
|     return columns; | ||||
|   }); | ||||
|  | ||||
|   protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] { | ||||
|     return [ | ||||
|       subscribeLabelRegistry(this.hass.connection, (labels) => { | ||||
|         this._labels = labels; | ||||
|       }), | ||||
|     ]; | ||||
|   } | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|     const { devicesOutput } = this._devicesAndFilterDomains( | ||||
|       this.hass.devices, | ||||
| @@ -452,7 +531,8 @@ export class HaConfigDeviceDashboard extends LitElement { | ||||
|       this.hass.areas, | ||||
|       this.manifests, | ||||
|       this._filters, | ||||
|       this.hass.localize | ||||
|       this.hass.localize, | ||||
|       this._labels | ||||
|     ); | ||||
|  | ||||
|     return html` | ||||
| @@ -479,6 +559,7 @@ export class HaConfigDeviceDashboard extends LitElement { | ||||
|         @row-click=${this._handleRowClicked} | ||||
|         clickable | ||||
|         hasFab | ||||
|         class=${this.narrow ? "narrow" : ""} | ||||
|       > | ||||
|         <ha-integration-overflow-menu | ||||
|           .hass=${this.hass} | ||||
| @@ -531,6 +612,15 @@ export class HaConfigDeviceDashboard extends LitElement { | ||||
|           .narrow=${this.narrow} | ||||
|           @expanded-changed=${this._filterExpanded} | ||||
|         ></ha-filter-states> | ||||
|         <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> | ||||
|       </hass-tabs-subpage-data-table> | ||||
|     `; | ||||
|   } | ||||
| @@ -590,8 +680,10 @@ export class HaConfigDeviceDashboard extends LitElement { | ||||
|         this.hass.areas, | ||||
|         this.manifests, | ||||
|         this._filters, | ||||
|         this.hass.localize | ||||
|         this.hass.localize, | ||||
|         this._labels | ||||
|       ); | ||||
|  | ||||
|     if ( | ||||
|       filteredDomains.size === 1 && | ||||
|       (PROTOCOL_INTEGRATIONS as ReadonlyArray<string>).includes( | ||||
| @@ -611,6 +703,12 @@ export class HaConfigDeviceDashboard extends LitElement { | ||||
|   static get styles(): CSSResultGroup { | ||||
|     return [ | ||||
|       css` | ||||
|         hass-tabs-subpage-data-table { | ||||
|           --data-table-row-height: 60px; | ||||
|         } | ||||
|         hass-tabs-subpage-data-table.narrow { | ||||
|           --data-table-row-height: 72px; | ||||
|         } | ||||
|         ha-button-menu { | ||||
|           margin-left: 8px; | ||||
|           margin-inline-start: 8px; | ||||
|   | ||||
| @@ -10,7 +10,7 @@ import { | ||||
|   mdiRestoreAlert, | ||||
|   mdiUndo, | ||||
| } from "@mdi/js"; | ||||
| import { HassEntity } from "home-assistant-js-websocket"; | ||||
| import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; | ||||
| import { | ||||
|   CSSResultGroup, | ||||
|   LitElement, | ||||
| @@ -37,16 +37,18 @@ import type { | ||||
|   RowClickedEvent, | ||||
|   SelectionChangedEvent, | ||||
| } from "../../../components/data-table/ha-data-table"; | ||||
| import "../../../components/data-table/ha-data-table-labels"; | ||||
| import "../../../components/ha-alert"; | ||||
| import "../../../components/ha-button-menu"; | ||||
| import "../../../components/ha-check-list-item"; | ||||
| import "../../../components/ha-filter-devices"; | ||||
| import "../../../components/ha-filter-floor-areas"; | ||||
| import "../../../components/ha-filter-integrations"; | ||||
| import "../../../components/ha-filter-states"; | ||||
| import "../../../components/ha-filter-labels"; | ||||
| import "../../../components/ha-icon"; | ||||
| import "../../../components/ha-icon-button"; | ||||
| import "../../../components/ha-svg-icon"; | ||||
| import "../../../components/ha-alert"; | ||||
| import { ConfigEntry, getConfigEntries } from "../../../data/config_entries"; | ||||
| import { fullEntitiesContext } from "../../../data/context"; | ||||
| import { UNAVAILABLE } from "../../../data/entity"; | ||||
| @@ -57,6 +59,10 @@ import { | ||||
|   updateEntityRegistryEntry, | ||||
| } from "../../../data/entity_registry"; | ||||
| import { entryIcon } from "../../../data/icons"; | ||||
| import { | ||||
|   LabelRegistryEntry, | ||||
|   subscribeLabelRegistry, | ||||
| } from "../../../data/label_registry"; | ||||
| import { | ||||
|   showAlertDialog, | ||||
|   showConfirmationDialog, | ||||
| @@ -65,6 +71,7 @@ import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info | ||||
| import "../../../layouts/hass-loading-screen"; | ||||
| import "../../../layouts/hass-tabs-subpage-data-table"; | ||||
| import type { HaTabsSubpageDataTable } from "../../../layouts/hass-tabs-subpage-data-table"; | ||||
| import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; | ||||
| import { haStyle } from "../../../resources/styles"; | ||||
| import type { HomeAssistant, Route } from "../../../types"; | ||||
| import { configSections } from "../ha-panel-config"; | ||||
| @@ -86,10 +93,11 @@ export interface EntityRow extends StateEntity { | ||||
|   status: string | undefined; | ||||
|   area?: string; | ||||
|   localized_platform: string; | ||||
|   label_entries: LabelRegistryEntry[]; | ||||
| } | ||||
|  | ||||
| @customElement("ha-config-entities") | ||||
| export class HaConfigEntities extends LitElement { | ||||
| export class HaConfigEntities extends SubscribeMixin(LitElement) { | ||||
|   @property({ attribute: false }) public hass!: HomeAssistant; | ||||
|  | ||||
|   @property({ type: Boolean }) public isWide = false; | ||||
| @@ -119,6 +127,9 @@ export class HaConfigEntities extends LitElement { | ||||
|  | ||||
|   @state() private _expandedFilter?: string; | ||||
|  | ||||
|   @state() | ||||
|   _labels!: LabelRegistryEntry[]; | ||||
|  | ||||
|   @query("hass-tabs-subpage-data-table", true) | ||||
|   private _dataTable!: HaTabsSubpageDataTable; | ||||
|  | ||||
| @@ -202,14 +213,21 @@ export class HaConfigEntities extends LitElement { | ||||
|         filterable: true, | ||||
|         direction: "asc", | ||||
|         grows: true, | ||||
|         template: narrow | ||||
|           ? (entry) => html` | ||||
|               ${entry.name}<br /> | ||||
|               <div class="secondary"> | ||||
|         template: (entry) => html` | ||||
|           <div style="font-size: 14px;">${entry.name}</div> | ||||
|           ${narrow | ||||
|             ? html`<div class="secondary"> | ||||
|                 ${entry.entity_id} | ${entry.localized_platform} | ||||
|               </div> | ||||
|             ` | ||||
|           : undefined, | ||||
|               </div>` | ||||
|             : nothing} | ||||
|           ${entry.label_entries.length | ||||
|             ? html` | ||||
|                 <ha-data-table-labels | ||||
|                   .labels=${entry.label_entries} | ||||
|                 ></ha-data-table-labels> | ||||
|               ` | ||||
|             : nothing} | ||||
|         `, | ||||
|       }, | ||||
|       entity_id: { | ||||
|         title: localize("ui.panel.config.entities.picker.headers.entity_id"), | ||||
| @@ -301,6 +319,13 @@ export class HaConfigEntities extends LitElement { | ||||
|               ` | ||||
|             : "—", | ||||
|       }, | ||||
|       labels: { | ||||
|         title: "", | ||||
|         hidden: true, | ||||
|         filterable: true, | ||||
|         template: (entry) => | ||||
|           entry.label_entries.map((lbl) => lbl.name).join(" "), | ||||
|       }, | ||||
|     }) | ||||
|   ); | ||||
|  | ||||
| @@ -315,7 +340,8 @@ export class HaConfigEntities extends LitElement { | ||||
|         string, | ||||
|         { value: string[] | undefined; items: Set<string> | undefined } | ||||
|       >, | ||||
|       entries?: ConfigEntry[] | ||||
|       entries?: ConfigEntry[], | ||||
|       labelReg?: LabelRegistryEntry[] | ||||
|     ) => { | ||||
|       const result: EntityRow[] = []; | ||||
|  | ||||
| @@ -337,12 +363,12 @@ export class HaConfigEntities extends LitElement { | ||||
|       let filteredConfigEntry: ConfigEntry | undefined; | ||||
|       const filteredDomains = new Set<string>(); | ||||
|  | ||||
|       Object.entries(filters).forEach(([key, flter]) => { | ||||
|         if (key === "config_entry" && flter.value?.length) { | ||||
|       Object.entries(filters).forEach(([key, filter]) => { | ||||
|         if (key === "config_entry" && filter.value?.length) { | ||||
|           filteredEntities = filteredEntities.filter( | ||||
|             (entity) => | ||||
|               entity.config_entry_id && | ||||
|               flter.value?.includes(entity.config_entry_id) | ||||
|               filter.value?.includes(entity.config_entry_id) | ||||
|           ); | ||||
|  | ||||
|           if (!entries) { | ||||
| @@ -351,7 +377,7 @@ export class HaConfigEntities extends LitElement { | ||||
|           } | ||||
|  | ||||
|           const configEntries = entries.filter( | ||||
|             (entry) => entry.entry_id && flter.value?.includes(entry.entry_id) | ||||
|             (entry) => entry.entry_id && filter.value?.includes(entry.entry_id) | ||||
|           ); | ||||
|  | ||||
|           configEntries.forEach((configEntry) => { | ||||
| @@ -360,23 +386,27 @@ export class HaConfigEntities extends LitElement { | ||||
|           if (configEntries.length === 1) { | ||||
|             filteredConfigEntry = configEntries[0]; | ||||
|           } | ||||
|         } else if (key === "ha-filter-integrations" && flter.value?.length) { | ||||
|         } else if (key === "ha-filter-integrations" && filter.value?.length) { | ||||
|           if (!entries) { | ||||
|             this._loadConfigEntries(); | ||||
|             return; | ||||
|           } | ||||
|           const entryIds = entries | ||||
|             .filter((entry) => flter.value!.includes(entry.domain)) | ||||
|             .filter((entry) => filter.value!.includes(entry.domain)) | ||||
|             .map((entry) => entry.entry_id); | ||||
|           filteredEntities = filteredEntities.filter( | ||||
|             (entity) => | ||||
|               entity.config_entry_id && | ||||
|               entryIds.includes(entity.config_entry_id) | ||||
|           ); | ||||
|           flter.value!.forEach((domain) => filteredDomains.add(domain)); | ||||
|         } else if (flter.items) { | ||||
|           filter.value!.forEach((domain) => filteredDomains.add(domain)); | ||||
|         } else if (key === "ha-filter-labels" && filter.value?.length) { | ||||
|           filteredEntities = filteredEntities.filter((entity) => | ||||
|             flter.items!.has(entity.entity_id) | ||||
|             entity.labels.some((lbl) => filter.value!.includes(lbl)) | ||||
|           ); | ||||
|         } else if (filter.items) { | ||||
|           filteredEntities = filteredEntities.filter((entity) => | ||||
|             filter.items!.has(entity.entity_id) | ||||
|           ); | ||||
|         } | ||||
|       }); | ||||
| @@ -404,6 +434,11 @@ export class HaConfigEntities extends LitElement { | ||||
|           continue; | ||||
|         } | ||||
|  | ||||
|         const labels = labelReg && entry?.labels; | ||||
|         const labelsEntries = (labels || []).map( | ||||
|           (lbl) => labelReg!.find((label) => label.label_id === lbl)! | ||||
|         ); | ||||
|  | ||||
|         result.push({ | ||||
|           ...entry, | ||||
|           entity, | ||||
| @@ -431,6 +466,7 @@ export class HaConfigEntities extends LitElement { | ||||
|                     : localize( | ||||
|                         "ui.panel.config.entities.picker.status.available" | ||||
|                       ), | ||||
|           label_entries: labelsEntries, | ||||
|         }); | ||||
|       } | ||||
|  | ||||
| @@ -438,6 +474,14 @@ export class HaConfigEntities extends LitElement { | ||||
|     } | ||||
|   ); | ||||
|  | ||||
|   protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] { | ||||
|     return [ | ||||
|       subscribeLabelRegistry(this.hass.connection, (labels) => { | ||||
|         this._labels = labels; | ||||
|       }), | ||||
|     ]; | ||||
|   } | ||||
|  | ||||
|   protected render() { | ||||
|     if (!this.hass || this._entities === undefined) { | ||||
|       return html` <hass-loading-screen></hass-loading-screen> `; | ||||
| @@ -451,7 +495,8 @@ export class HaConfigEntities extends LitElement { | ||||
|         this.hass.areas, | ||||
|         this._stateEntities, | ||||
|         this._filters, | ||||
|         this._entries | ||||
|         this._entries, | ||||
|         this._labels | ||||
|       ); | ||||
|  | ||||
|     const includeAddDeviceFab = | ||||
| @@ -482,16 +527,17 @@ export class HaConfigEntities extends LitElement { | ||||
|         .filters=${Object.values(this._filters).filter( | ||||
|           (filter) => filter.value?.length | ||||
|         ).length} | ||||
|         .selected=${this._selectedEntities.length} | ||||
|         .filter=${this._filter} | ||||
|         selectable | ||||
|         clickable | ||||
|         .selected=${this._selectedEntities.length} | ||||
|         @selection-changed=${this._handleSelectionChanged} | ||||
|         clickable | ||||
|         @clear-filter=${this._clearFilter} | ||||
|         @search-changed=${this._handleSearchChange} | ||||
|         @row-click=${this._openEditEntry} | ||||
|         id="entity_id" | ||||
|         .hasFab=${includeAddDeviceFab} | ||||
|         class=${this.narrow ? "narrow" : ""} | ||||
|       > | ||||
|         <ha-integration-overflow-menu | ||||
|           .hass=${this.hass} | ||||
| @@ -633,6 +679,15 @@ export class HaConfigEntities extends LitElement { | ||||
|           .narrow=${this.narrow} | ||||
|           @expanded-changed=${this._filterExpanded} | ||||
|         ></ha-filter-states> | ||||
|         <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> | ||||
|         ${includeAddDeviceFab | ||||
|           ? html`<ha-fab | ||||
|               .label=${this.hass.localize("ui.panel.config.devices.add_device")} | ||||
| @@ -703,6 +758,23 @@ export class HaConfigEntities extends 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() { | ||||
| @@ -918,7 +990,8 @@ export class HaConfigEntities extends LitElement { | ||||
|         this.hass.areas, | ||||
|         this._stateEntities, | ||||
|         this._filters, | ||||
|         this._entries | ||||
|         this._entries, | ||||
|         this._labels | ||||
|       ); | ||||
|     if ( | ||||
|       filteredDomains.size === 1 && | ||||
| @@ -940,6 +1013,12 @@ export class HaConfigEntities extends LitElement { | ||||
|     return [ | ||||
|       haStyle, | ||||
|       css` | ||||
|         hass-tabs-subpage-data-table { | ||||
|           --data-table-row-height: 60px; | ||||
|         } | ||||
|         hass-tabs-subpage-data-table.narrow { | ||||
|           --data-table-row-height: 72px; | ||||
|         } | ||||
|         hass-loading-screen { | ||||
|           --app-header-background-color: var(--sidebar-background-color); | ||||
|           --app-header-text-color: var(--sidebar-text-color); | ||||
|   | ||||
| @@ -1,8 +1,17 @@ | ||||
| import "@lrnwebcomponents/simple-tooltip/simple-tooltip"; | ||||
| import { mdiAlertCircle, mdiPencilOff, mdiPlus } from "@mdi/js"; | ||||
| 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 { consume } from "@lit-labs/context"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { computeStateDomain } from "../../../common/entity/compute_state_domain"; | ||||
| import { navigate } from "../../../common/navigate"; | ||||
| @@ -15,6 +24,7 @@ import { | ||||
|   DataTableColumnContainer, | ||||
|   RowClickedEvent, | ||||
| } from "../../../components/data-table/ha-data-table"; | ||||
| import "../../../components/data-table/ha-data-table-labels"; | ||||
| import "../../../components/ha-fab"; | ||||
| import "../../../components/ha-icon"; | ||||
| import "../../../components/ha-state-icon"; | ||||
| @@ -44,6 +54,13 @@ import { configSections } from "../ha-panel-config"; | ||||
| import "../integrations/ha-integration-overflow-menu"; | ||||
| import { isHelperDomain } from "./const"; | ||||
| import { showHelperDetailDialog } from "./show-dialog-helper-detail"; | ||||
| import { | ||||
|   LabelRegistryEntry, | ||||
|   subscribeLabelRegistry, | ||||
| } from "../../../data/label_registry"; | ||||
| import { fullEntitiesContext } from "../../../data/context"; | ||||
| import "../../../components/ha-filter-labels"; | ||||
| import { haStyle } from "../../../resources/styles"; | ||||
|  | ||||
| type HelperItem = { | ||||
|   id: string; | ||||
| @@ -54,6 +71,7 @@ type HelperItem = { | ||||
|   type: string; | ||||
|   configEntry?: ConfigEntry; | ||||
|   entity?: HassEntity; | ||||
|   label_entries: LabelRegistryEntry[]; | ||||
| }; | ||||
|  | ||||
| // 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 _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() { | ||||
|     return [ | ||||
|       subscribeConfigEntries( | ||||
| @@ -117,6 +153,9 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { | ||||
|       subscribeEntityRegistry(this.hass.connection!, (entries) => { | ||||
|         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, | ||||
|           direction: "asc", | ||||
|           template: (helper) => html` | ||||
|             ${helper.name} | ||||
|             <div style="font-size: 14px;">${helper.name}</div> | ||||
|             ${narrow | ||||
|               ? html`<div class="secondary">${helper.entity_id}</div> ` | ||||
|               : ""} | ||||
|               : nothing} | ||||
|             ${helper.label_entries.length | ||||
|               ? html` | ||||
|                   <ha-data-table-labels | ||||
|                     .labels=${helper.label_entries} | ||||
|                   ></ha-data-table-labels> | ||||
|                 ` | ||||
|               : nothing} | ||||
|           `, | ||||
|         }, | ||||
|       }; | ||||
| @@ -201,8 +247,15 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { | ||||
|       localize: LocalizeFunc, | ||||
|       stateItems: HassEntity[], | ||||
|       entityEntries: Record<string, EntityRegistryEntry>, | ||||
|       configEntries: Record<string, ConfigEntry> | ||||
|       configEntries: Record<string, ConfigEntry>, | ||||
|       entityReg: EntityRegistryEntry[], | ||||
|       labelReg?: LabelRegistryEntry[], | ||||
|       filteredStateItems?: string[] | null | ||||
|     ): HelperItem[] => { | ||||
|       if (filteredStateItems === null) { | ||||
|         return []; | ||||
|       } | ||||
|  | ||||
|       const configEntriesCopy = { ...configEntries }; | ||||
|  | ||||
|       const states = stateItems.map((entityState) => { | ||||
| @@ -241,14 +294,29 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { | ||||
|         entity: undefined, | ||||
|       })); | ||||
|  | ||||
|       return [...states, ...entries].map((item) => ({ | ||||
|         ...item, | ||||
|         localized_type: item.configEntry | ||||
|           ? domainToName(localize, item.type) | ||||
|           : localize( | ||||
|               `ui.panel.config.helpers.types.${item.type}` as LocalizeKeys | ||||
|             ) || item.type, | ||||
|       })); | ||||
|       return [...states, ...entries] | ||||
|         .filter((item) => | ||||
|           filteredStateItems | ||||
|             ? filteredStateItems?.includes(item.entity_id) | ||||
|             : true | ||||
|         ) | ||||
|         .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" | ||||
|         .route=${this.route} | ||||
|         .tabs=${configSections.devices} | ||||
|         hasFilters | ||||
|         .filters=${Object.values(this._filters).filter( | ||||
|           (filter) => filter.value?.length | ||||
|         ).length} | ||||
|         .columns=${this._columns(this.narrow, this.hass.localize)} | ||||
|         .data=${this._getItems( | ||||
|           this.hass.localize, | ||||
|           this._stateItems, | ||||
|           this._entityEntries, | ||||
|           this._configEntries | ||||
|           this._configEntries, | ||||
|           this._entityReg, | ||||
|           this._labels, | ||||
|           this._filteredStateItems | ||||
|         )} | ||||
|         .activeFilters=${this._activeFilters} | ||||
|         @clear-filter=${this._clearFilter} | ||||
|         @row-click=${this._openEditDialog} | ||||
|         hasFab | ||||
|         clickable | ||||
|         .noDataText=${this.hass.localize( | ||||
|           "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 | ||||
|           .hass=${this.hass} | ||||
|           slot="toolbar-icon" | ||||
| @@ -293,7 +381,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { | ||||
|             "ui.panel.config.helpers.picker.create_helper" | ||||
|           )} | ||||
|           extended | ||||
|           @click=${this._createHelpler} | ||||
|           @click=${this._createHelper} | ||||
|         > | ||||
|           <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> | ||||
|         </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) { | ||||
|     super.firstUpdated(changedProps); | ||||
|     if (this.route.path === "/add") { | ||||
| @@ -418,9 +563,23 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private _createHelpler() { | ||||
|   private _createHelper() { | ||||
|     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 { | ||||
|   | ||||
| @@ -49,11 +49,19 @@ class DialogLabelDetail | ||||
|       this._icon = ""; | ||||
|       this._color = ""; | ||||
|     } | ||||
|     document.body.addEventListener("keydown", this._handleKeyPress); | ||||
|   } | ||||
|  | ||||
|   private _handleKeyPress = (ev: KeyboardEvent) => { | ||||
|     if (ev.key === "Escape") { | ||||
|       ev.stopPropagation(); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   public closeDialog(): void { | ||||
|     this._params = undefined; | ||||
|     fireEvent(this, "dialog-closed", { dialog: this.localName }); | ||||
|     document.body.removeEventListener("keydown", this._handleKeyPress); | ||||
|   } | ||||
|  | ||||
|   protected render() { | ||||
|   | ||||
| @@ -1,4 +1,11 @@ | ||||
| import { mdiHelpCircle, mdiPlus } from "@mdi/js"; | ||||
| import { | ||||
|   mdiDelete, | ||||
|   mdiDevices, | ||||
|   mdiHelpCircle, | ||||
|   mdiPlus, | ||||
|   mdiRobot, | ||||
|   mdiShape, | ||||
| } from "@mdi/js"; | ||||
| import { LitElement, PropertyValues, html, nothing } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| @@ -11,6 +18,7 @@ import { | ||||
| import "../../../components/ha-fab"; | ||||
| import "../../../components/ha-icon-button"; | ||||
| import "../../../components/ha-relative-time"; | ||||
| import "../../../components/ha-icon-overflow-menu"; | ||||
| import { | ||||
|   LabelRegistryEntry, | ||||
|   LabelRegistryEntryMutableParams, | ||||
| @@ -27,6 +35,7 @@ import "../../../layouts/hass-tabs-subpage-data-table"; | ||||
| import { HomeAssistant, Route } from "../../../types"; | ||||
| import { configSections } from "../ha-panel-config"; | ||||
| import { showLabelDetailDialog } from "./show-dialog-label-detail"; | ||||
| import { navigate } from "../../../common/navigate"; | ||||
|  | ||||
| @customElement("ha-config-labels") | ||||
| export class HaConfigLabels extends LitElement { | ||||
| @@ -71,6 +80,41 @@ export class HaConfigLabels extends LitElement { | ||||
|         filterable: true, | ||||
|         grows: true, | ||||
|       }, | ||||
|       actions: { | ||||
|         title: "", | ||||
|         width: "64px", | ||||
|         type: "overflow-menu", | ||||
|         template: (label) => html` | ||||
|           <ha-icon-overflow-menu | ||||
|             .hass=${this.hass} | ||||
|             narrow | ||||
|             .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"), | ||||
|                 path: mdiDelete, | ||||
|                 action: () => this._removeLabel(label), | ||||
|                 warning: true, | ||||
|               }, | ||||
|             ]} | ||||
|           > | ||||
|           </ha-icon-overflow-menu> | ||||
|         `, | ||||
|       }, | ||||
|     }; | ||||
|     return columns; | ||||
|   }); | ||||
| @@ -189,6 +233,7 @@ export class HaConfigLabels extends LitElement { | ||||
|         }), | ||||
|         dismissText: this.hass!.localize("ui.common.cancel"), | ||||
|         confirmText: this.hass!.localize("ui.common.remove"), | ||||
|         destructive: true, | ||||
|       })) | ||||
|     ) { | ||||
|       return false; | ||||
| @@ -203,6 +248,20 @@ export class HaConfigLabels extends LitElement { | ||||
|       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 { | ||||
|   | ||||
| @@ -16,6 +16,7 @@ import { UnsubscribeFunc } from "home-assistant-js-websocket"; | ||||
| import { | ||||
|   CSSResultGroup, | ||||
|   LitElement, | ||||
|   PropertyValues, | ||||
|   TemplateResult, | ||||
|   css, | ||||
|   html, | ||||
| @@ -95,6 +96,8 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) { | ||||
|  | ||||
|   @property({ attribute: false }) public scenes!: SceneEntity[]; | ||||
|  | ||||
|   @state() private _searchParms = new URLSearchParams(window.location.search); | ||||
|  | ||||
|   @state() private _activeFilters?: string[]; | ||||
|  | ||||
|   @state() private _filteredScenes?: string[] | null; | ||||
| @@ -297,6 +300,13 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) { | ||||
|     } | ||||
|   ); | ||||
|  | ||||
|   protected updated(changedProps: PropertyValues) { | ||||
|     super.updated(changedProps); | ||||
|     if (changedProps.has("_entityReg")) { | ||||
|       this._applyFilters(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] { | ||||
|     return [ | ||||
|       subscribeCategoryRegistry(this.hass.connection, "scene", (categories) => { | ||||
| @@ -522,6 +532,27 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) { | ||||
|     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>) { | ||||
|     const scene = this.scenes.find((a) => a.entity_id === ev.detail.id); | ||||
|  | ||||
|   | ||||
| @@ -15,6 +15,7 @@ import { UnsubscribeFunc } from "home-assistant-js-websocket"; | ||||
| import { | ||||
|   CSSResultGroup, | ||||
|   LitElement, | ||||
|   PropertyValues, | ||||
|   TemplateResult, | ||||
|   css, | ||||
|   html, | ||||
| @@ -560,10 +561,35 @@ class HaScriptPicker extends SubscribeMixin(LitElement) { | ||||
|     this._filteredScripts = items ? [...items] : undefined; | ||||
|   } | ||||
|  | ||||
|   protected updated(changedProps: PropertyValues) { | ||||
|     super.updated(changedProps); | ||||
|     if (changedProps.has("_entityReg")) { | ||||
|       this._applyFilters(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   firstUpdated() { | ||||
|     if (this._searchParms.has("blueprint")) { | ||||
|       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() { | ||||
|   | ||||
| @@ -35,6 +35,9 @@ export abstract class HuiStackCard<T extends StackCardConfig = StackCardConfig> | ||||
|  | ||||
|   @state() protected _config?: T; | ||||
|  | ||||
|   @property({ type: Boolean, reflect: true }) | ||||
|   public isPanel = false; | ||||
|  | ||||
|   public getCardSize(): number | Promise<number> { | ||||
|     return 1; | ||||
|   } | ||||
| @@ -98,10 +101,10 @@ export abstract class HuiStackCard<T extends StackCardConfig = StackCardConfig> | ||||
|         display: block; | ||||
|         padding: 24px 16px 16px; | ||||
|       } | ||||
|       :host { | ||||
|         --ha-card-border-radius: inherit !important; | ||||
|         --ha-card-border-width: inherit !important; | ||||
|         --ha-card-box-shadow: inherit !important; | ||||
|       :host([ispanel]) #root { | ||||
|         --ha-card-border-radius: var(--restore-card-border-radius); | ||||
|         --ha-card-border-width: var(--restore-card-border-width); | ||||
|         --ha-card-box-shadow: var(--restore-card-border-shadow); | ||||
|       } | ||||
|     `; | ||||
|   } | ||||
|   | ||||
| @@ -130,6 +130,9 @@ export class PanelView extends LitElement implements LovelaceViewElement { | ||||
|       :host { | ||||
|         display: block; | ||||
|         height: 100%; | ||||
|         --restore-card-border-radius: var(--ha-card-border-radius, 12px); | ||||
|         --restore-card-border-width: var(--ha-card-border-width, 1px); | ||||
|         --restore-card-box-shadow: var(--ha-card-box-shadow, none); | ||||
|       } | ||||
|  | ||||
|       * { | ||||
|   | ||||
| @@ -32,6 +32,7 @@ const mainStyles = css` | ||||
|     --accent-color: ${unsafeCSS(DEFAULT_ACCENT_COLOR)}; | ||||
|     --divider-color: rgba(0, 0, 0, 0.12); | ||||
|     --outline-color: rgba(0, 0, 0, 0.12); | ||||
|     --outline-hover-color: rgba(0, 0, 0, 0.24); | ||||
|  | ||||
|     --scrollbar-thumb-color: rgb(194, 194, 194); | ||||
|  | ||||
|   | ||||
| @@ -15,6 +15,7 @@ export const darkStyles = { | ||||
|   "switch-unchecked-track-color": "#9b9b9b", | ||||
|   "divider-color": "rgba(225, 225, 225, .12)", | ||||
|   "outline-color": "rgba(225, 225, 225, .12)", | ||||
|   "outline-hover-color": "rgba(225, 225, 225, .24)", | ||||
|   "mdc-ripple-color": "#AAAAAA", | ||||
|   "mdc-linear-progress-buffer-color": "rgba(255, 255, 255, 0.1)", | ||||
|  | ||||
| @@ -142,7 +143,10 @@ export const derivedStyles = { | ||||
|   "mdc-select-disabled-ink-color": "var(--input-disabled-ink-color)", | ||||
|   "mdc-select-dropdown-icon-color": "var(--input-dropdown-icon-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)", | ||||
|   // Vaadin | ||||
|   "material-body-text-color": "var(--primary-text-color)", | ||||
|   | ||||
| @@ -501,11 +501,18 @@ | ||||
|       }, | ||||
|       "subpage-data-table": { | ||||
|         "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}", | ||||
|         "group_by": "Group by {groupColumn}", | ||||
|         "dont_group_by": "Don't group", | ||||
|         "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": "Integration" | ||||
| @@ -564,6 +571,7 @@ | ||||
|         "add_new_sugestion": "Add new category ''{name}''", | ||||
|         "add_new": "Add new category…", | ||||
|         "no_categories": "You don't have any categories", | ||||
|         "no_match": "No matching categories found", | ||||
|         "add_dialog": { | ||||
|           "title": "Add new category", | ||||
|           "text": "Enter the name of the new category.", | ||||
| @@ -592,13 +600,7 @@ | ||||
|         "no_areas": "You don't have any areas", | ||||
|         "no_match": "No matching areas found", | ||||
|         "unassigned_areas": "Unassigned areas", | ||||
|         "add_dialog": { | ||||
|           "title": "Add new area", | ||||
|           "text": "Enter the name of the new area.", | ||||
|           "name": "Name", | ||||
|           "add": "Add", | ||||
|           "failed_create_area": "Failed to create area." | ||||
|         } | ||||
|         "failed_create_area": "Failed to create area." | ||||
|       }, | ||||
|       "floor-picker": { | ||||
|         "clear": "Clear", | ||||
| @@ -608,13 +610,7 @@ | ||||
|         "add_new": "Add new floor…", | ||||
|         "no_floors": "You don't have any floors", | ||||
|         "no_match": "No matching floors found", | ||||
|         "add_dialog": { | ||||
|           "title": "Add new floor", | ||||
|           "text": "Enter the name of the new floor.", | ||||
|           "name": "Name", | ||||
|           "add": "Add", | ||||
|           "failed_create_floor": "Failed to create floor." | ||||
|         } | ||||
|         "failed_create_floor": "Failed to create floor." | ||||
|       }, | ||||
|       "area-filter": { | ||||
|         "title": "Areas", | ||||
| @@ -1118,6 +1114,7 @@ | ||||
|         "edit": "Edit entity", | ||||
|         "details": "Details", | ||||
|         "back_to_info": "Back to info", | ||||
|         "info": "Information", | ||||
|         "related": "Related", | ||||
|         "history": "History", | ||||
|         "logbook": "Logbook", | ||||
| @@ -1959,7 +1956,11 @@ | ||||
|         "labels": { | ||||
|           "caption": "Labels", | ||||
|           "description": "Group devices and entities", | ||||
|           "headers": { "name": "Name", "icon": "Icon", "color": "Color" }, | ||||
|           "headers": { | ||||
|             "name": "Name", | ||||
|             "icon": "Icon", | ||||
|             "color": "Color" | ||||
|           }, | ||||
|           "add_label": "Add label", | ||||
|           "no_labels": "You don't have any labels", | ||||
|           "introduction": "Labels can help you organize your areas, devices and entities. They can be used to filter in the UI, or use them as a target in automations.", | ||||
| @@ -2665,6 +2666,7 @@ | ||||
|             "edit_automation": "Edit automation", | ||||
|             "dev_automation": "Debug automation", | ||||
|             "show_info_automation": "Show info about automation", | ||||
|             "show_settings": "Show settings", | ||||
|             "delete": "[%key:ui::common::delete%]", | ||||
|             "delete_confirm_title": "Delete automation?", | ||||
|             "delete_confirm_text": "{name} will be permanently deleted.", | ||||
| @@ -2685,6 +2687,14 @@ | ||||
|               "state": "State", | ||||
|               "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_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''." | ||||
| @@ -5388,7 +5398,6 @@ | ||||
|             "type": "View type", | ||||
|             "type_warning_sections": "You can not change your view to use the 'sections' view type because migration is not supported yet. Start from scratch with a new view if you want to experiment with the 'sections' view.", | ||||
|             "type_warning_others": "You can not change your view to an other type because migration is not supported yet. Start from scratch with a new view if you want to use another view type.", | ||||
|  | ||||
|             "types": { | ||||
|               "masonry": "Masonry (default)", | ||||
|               "sidebar": "Sidebar", | ||||
|   | ||||
							
								
								
									
										114
									
								
								yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										114
									
								
								yarn.lock
									
									
									
									
									
								
							| @@ -1526,14 +1526,14 @@ __metadata: | ||||
|   languageName: node | ||||
|   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": | ||||
|   version: 6.26.0 | ||||
|   resolution: "@codemirror/view@npm:6.26.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.1 | ||||
|   resolution: "@codemirror/view@npm:6.26.1" | ||||
|   dependencies: | ||||
|     "@codemirror/state": "npm:^6.4.0" | ||||
|     style-mod: "npm:^4.1.0" | ||||
|     w3c-keyname: "npm:^2.2.4" | ||||
|   checksum: 10/d4ef249044cbc293a7267c83e08671a68646fd7bbe1efb8d205c01385f157c93918eabeaedb62a4cc10598ab63818ac749cec4f6355fe0404d9d4beb7857c31f | ||||
|   checksum: 10/6d2b19b2439c36b2712d3560eeb0c198ad2ee442ad22641c2b4bce94077812cffbb52ca12328219d3b9663b2dd0ffc63481432a2550839e5c7a7a53704e82a9a | ||||
|   languageName: node | ||||
|   linkType: hard | ||||
|  | ||||
| @@ -4543,15 +4543,15 @@ __metadata: | ||||
|   languageName: node | ||||
|   linkType: hard | ||||
|  | ||||
| "@typescript-eslint/eslint-plugin@npm:7.3.1": | ||||
|   version: 7.3.1 | ||||
|   resolution: "@typescript-eslint/eslint-plugin@npm:7.3.1" | ||||
| "@typescript-eslint/eslint-plugin@npm:7.4.0": | ||||
|   version: 7.4.0 | ||||
|   resolution: "@typescript-eslint/eslint-plugin@npm:7.4.0" | ||||
|   dependencies: | ||||
|     "@eslint-community/regexpp": "npm:^4.5.1" | ||||
|     "@typescript-eslint/scope-manager": "npm:7.3.1" | ||||
|     "@typescript-eslint/type-utils": "npm:7.3.1" | ||||
|     "@typescript-eslint/utils": "npm:7.3.1" | ||||
|     "@typescript-eslint/visitor-keys": "npm:7.3.1" | ||||
|     "@typescript-eslint/scope-manager": "npm:7.4.0" | ||||
|     "@typescript-eslint/type-utils": "npm:7.4.0" | ||||
|     "@typescript-eslint/utils": "npm:7.4.0" | ||||
|     "@typescript-eslint/visitor-keys": "npm:7.4.0" | ||||
|     debug: "npm:^4.3.4" | ||||
|     graphemer: "npm:^1.4.0" | ||||
|     ignore: "npm:^5.2.4" | ||||
| @@ -4564,44 +4564,44 @@ __metadata: | ||||
|   peerDependenciesMeta: | ||||
|     typescript: | ||||
|       optional: true | ||||
|   checksum: 10/8ed276113a714d93ab3ababb1179e4785bd9378e6d97726519ea1d2ac502a94475e0be988c2ec427dcfc1e6950329d58da6e64131ee87028fce63493461cc51a | ||||
|   checksum: 10/9bd8852c7e4e9608c3fded94f7c60506cc7d2b6d8a8c1cad6d48969a7363751b20282874e55ccdf180635cf204cb10b3e1e5c3d1cff34d4fcd07762be3fc138e | ||||
|   languageName: node | ||||
|   linkType: hard | ||||
|  | ||||
| "@typescript-eslint/parser@npm:7.3.1": | ||||
|   version: 7.3.1 | ||||
|   resolution: "@typescript-eslint/parser@npm:7.3.1" | ||||
| "@typescript-eslint/parser@npm:7.4.0": | ||||
|   version: 7.4.0 | ||||
|   resolution: "@typescript-eslint/parser@npm:7.4.0" | ||||
|   dependencies: | ||||
|     "@typescript-eslint/scope-manager": "npm:7.3.1" | ||||
|     "@typescript-eslint/types": "npm:7.3.1" | ||||
|     "@typescript-eslint/typescript-estree": "npm:7.3.1" | ||||
|     "@typescript-eslint/visitor-keys": "npm:7.3.1" | ||||
|     "@typescript-eslint/scope-manager": "npm:7.4.0" | ||||
|     "@typescript-eslint/types": "npm:7.4.0" | ||||
|     "@typescript-eslint/typescript-estree": "npm:7.4.0" | ||||
|     "@typescript-eslint/visitor-keys": "npm:7.4.0" | ||||
|     debug: "npm:^4.3.4" | ||||
|   peerDependencies: | ||||
|     eslint: ^8.56.0 | ||||
|   peerDependenciesMeta: | ||||
|     typescript: | ||||
|       optional: true | ||||
|   checksum: 10/018326010fec1dcefd75809ccac5102a475bf1e052d824b898d707e7c0bf3e51e101164b410d1b2a513628985c96eb412538644d2005e26b99a22db6eb9402df | ||||
|   checksum: 10/142a9e1187d305ed43b4fef659c36fa4e28359467198c986f0955c70b4067c9799f4c85d9881fbf099c55dfb265e30666e28b3ef290520e242b45ca7cb8e4ca9 | ||||
|   languageName: node | ||||
|   linkType: hard | ||||
|  | ||||
| "@typescript-eslint/scope-manager@npm:7.3.1": | ||||
|   version: 7.3.1 | ||||
|   resolution: "@typescript-eslint/scope-manager@npm:7.3.1" | ||||
| "@typescript-eslint/scope-manager@npm:7.4.0": | ||||
|   version: 7.4.0 | ||||
|   resolution: "@typescript-eslint/scope-manager@npm:7.4.0" | ||||
|   dependencies: | ||||
|     "@typescript-eslint/types": "npm:7.3.1" | ||||
|     "@typescript-eslint/visitor-keys": "npm:7.3.1" | ||||
|   checksum: 10/7384d1f46d7f3678a1135a1ac0bd8b6dfa2f01e93b19e2510c7082766cf6983a1bf80b4ccf498651199a81d9f2bdb65101fd7a19226a723260514204d0c30b34 | ||||
|     "@typescript-eslint/types": "npm:7.4.0" | ||||
|     "@typescript-eslint/visitor-keys": "npm:7.4.0" | ||||
|   checksum: 10/8cf9292444f9731017a707cac34bef5ae0eb33b5cd42ed07fcd046e981d97889d9201d48e02f470f2315123f53771435e10b1dc81642af28a11df5352a8e8be2 | ||||
|   languageName: node | ||||
|   linkType: hard | ||||
|  | ||||
| "@typescript-eslint/type-utils@npm:7.3.1": | ||||
|   version: 7.3.1 | ||||
|   resolution: "@typescript-eslint/type-utils@npm:7.3.1" | ||||
| "@typescript-eslint/type-utils@npm:7.4.0": | ||||
|   version: 7.4.0 | ||||
|   resolution: "@typescript-eslint/type-utils@npm:7.4.0" | ||||
|   dependencies: | ||||
|     "@typescript-eslint/typescript-estree": "npm:7.3.1" | ||||
|     "@typescript-eslint/utils": "npm:7.3.1" | ||||
|     "@typescript-eslint/typescript-estree": "npm:7.4.0" | ||||
|     "@typescript-eslint/utils": "npm:7.4.0" | ||||
|     debug: "npm:^4.3.4" | ||||
|     ts-api-utils: "npm:^1.0.1" | ||||
|   peerDependencies: | ||||
| @@ -4609,23 +4609,23 @@ __metadata: | ||||
|   peerDependenciesMeta: | ||||
|     typescript: | ||||
|       optional: true | ||||
|   checksum: 10/fae9003a76a8f2a2a4bb88dc0f82c0a1ca0688633183fac391920e7124a12807aac84bb287a21f61e99523c15223d1c08e7680685ebf21d07429604cba6c420b | ||||
|   checksum: 10/a8bd0929d8237679b2b8a7817f070a4b9658ee976882fba8ff37e4a70dd33f87793e1b157771104111fe8054eaa8ad437a010b6aa465072fbdb932647125db2d | ||||
|   languageName: node | ||||
|   linkType: hard | ||||
|  | ||||
| "@typescript-eslint/types@npm:7.3.1": | ||||
|   version: 7.3.1 | ||||
|   resolution: "@typescript-eslint/types@npm:7.3.1" | ||||
|   checksum: 10/c9c8eae1cf937cececd99a253bd65eb71b40206e79cf917ad9c3b3ab80cc7ce5fefb2804f9fd2a70e7438951f0d1e63df3031fc61e3a08dfef5fde208a12e0ed | ||||
| "@typescript-eslint/types@npm:7.4.0": | ||||
|   version: 7.4.0 | ||||
|   resolution: "@typescript-eslint/types@npm:7.4.0" | ||||
|   checksum: 10/2782c5bf65cd3dfa9cd32bc3023676bbca22144987c3f6c6b67fd96c73d4a60b85a57458c49fd11b9971ac6531824bb3ae0664491e7a6de25d80c523c9be92b7 | ||||
|   languageName: node | ||||
|   linkType: hard | ||||
|  | ||||
| "@typescript-eslint/typescript-estree@npm:7.3.1": | ||||
|   version: 7.3.1 | ||||
|   resolution: "@typescript-eslint/typescript-estree@npm:7.3.1" | ||||
| "@typescript-eslint/typescript-estree@npm:7.4.0": | ||||
|   version: 7.4.0 | ||||
|   resolution: "@typescript-eslint/typescript-estree@npm:7.4.0" | ||||
|   dependencies: | ||||
|     "@typescript-eslint/types": "npm:7.3.1" | ||||
|     "@typescript-eslint/visitor-keys": "npm:7.3.1" | ||||
|     "@typescript-eslint/types": "npm:7.4.0" | ||||
|     "@typescript-eslint/visitor-keys": "npm:7.4.0" | ||||
|     debug: "npm:^4.3.4" | ||||
|     globby: "npm:^11.1.0" | ||||
|     is-glob: "npm:^4.0.3" | ||||
| @@ -4635,34 +4635,34 @@ __metadata: | ||||
|   peerDependenciesMeta: | ||||
|     typescript: | ||||
|       optional: true | ||||
|   checksum: 10/363ad9864b56394b4000dff7c2b77d0ea52042c3c20e3b86c0f3c66044915632d9890255527c6f3a5ef056886dec72e38fbcfce49d4ad092c160440f54128230 | ||||
|   checksum: 10/162ec9d7582f45588342e1be36fdb60e41f50bbdfbc3035c91b517ff5d45244f776921c88d88e543e1c7d0f1e6ada5474a8316b78f1b0e6d2233b101bc45b166 | ||||
|   languageName: node | ||||
|   linkType: hard | ||||
|  | ||||
| "@typescript-eslint/utils@npm:7.3.1": | ||||
|   version: 7.3.1 | ||||
|   resolution: "@typescript-eslint/utils@npm:7.3.1" | ||||
| "@typescript-eslint/utils@npm:7.4.0": | ||||
|   version: 7.4.0 | ||||
|   resolution: "@typescript-eslint/utils@npm:7.4.0" | ||||
|   dependencies: | ||||
|     "@eslint-community/eslint-utils": "npm:^4.4.0" | ||||
|     "@types/json-schema": "npm:^7.0.12" | ||||
|     "@types/semver": "npm:^7.5.0" | ||||
|     "@typescript-eslint/scope-manager": "npm:7.3.1" | ||||
|     "@typescript-eslint/types": "npm:7.3.1" | ||||
|     "@typescript-eslint/typescript-estree": "npm:7.3.1" | ||||
|     "@typescript-eslint/scope-manager": "npm:7.4.0" | ||||
|     "@typescript-eslint/types": "npm:7.4.0" | ||||
|     "@typescript-eslint/typescript-estree": "npm:7.4.0" | ||||
|     semver: "npm:^7.5.4" | ||||
|   peerDependencies: | ||||
|     eslint: ^8.56.0 | ||||
|   checksum: 10/234d9d65fe5d0f4a31345bd8f5a6f2879a578b3a531a14c2b3edaa7fb587c71d26249f86c41857382c0405384dc104955c02b588b3cee6fc2734f1ae40aef07b | ||||
|   checksum: 10/ffed27e770c486cd000ff892d9049b0afe8b9d6318452a5355b78a37436cbb414bceacae413a2ac813f3e584684825d5e0baa2e6376b7ad6013a108ac91bc19d | ||||
|   languageName: node | ||||
|   linkType: hard | ||||
|  | ||||
| "@typescript-eslint/visitor-keys@npm:7.3.1": | ||||
|   version: 7.3.1 | ||||
|   resolution: "@typescript-eslint/visitor-keys@npm:7.3.1" | ||||
| "@typescript-eslint/visitor-keys@npm:7.4.0": | ||||
|   version: 7.4.0 | ||||
|   resolution: "@typescript-eslint/visitor-keys@npm:7.4.0" | ||||
|   dependencies: | ||||
|     "@typescript-eslint/types": "npm:7.3.1" | ||||
|     "@typescript-eslint/types": "npm:7.4.0" | ||||
|     eslint-visitor-keys: "npm:^3.4.1" | ||||
|   checksum: 10/163a93597c1d696920a19b3c1627d02368bdd52059f811c0fadd680c38034bb6418ebefe99d8ce26e0dd44ae184f18fab186af775de1a8771256be1a7905c174 | ||||
|   checksum: 10/70dc99f2ad116c6e2d9e55af249e4453e06bba2ceea515adef2d2e86e97e557865bb1b1d467667462443eb0d624baba36f7442fd1082f3874339bbc381c26e93 | ||||
|   languageName: node | ||||
|   linkType: hard | ||||
|  | ||||
| @@ -9604,7 +9604,7 @@ __metadata: | ||||
|     "@codemirror/legacy-modes": "npm:6.3.3" | ||||
|     "@codemirror/search": "npm:6.5.6" | ||||
|     "@codemirror/state": "npm:6.4.1" | ||||
|     "@codemirror/view": "npm:6.26.0" | ||||
|     "@codemirror/view": "npm:6.26.1" | ||||
|     "@egjs/hammerjs": "npm:2.0.17" | ||||
|     "@formatjs/intl-datetimeformat": "npm:6.12.3" | ||||
|     "@formatjs/intl-displaynames": "npm:6.6.6" | ||||
| @@ -9688,8 +9688,8 @@ __metadata: | ||||
|     "@types/tar": "npm:6.1.11" | ||||
|     "@types/ua-parser-js": "npm:0.7.39" | ||||
|     "@types/webspeechapi": "npm:0.0.29" | ||||
|     "@typescript-eslint/eslint-plugin": "npm:7.3.1" | ||||
|     "@typescript-eslint/parser": "npm:7.3.1" | ||||
|     "@typescript-eslint/eslint-plugin": "npm:7.4.0" | ||||
|     "@typescript-eslint/parser": "npm:7.4.0" | ||||
|     "@vaadin/combo-box": "npm:24.3.10" | ||||
|     "@vaadin/vaadin-themable-mixin": "npm:24.3.10" | ||||
|     "@vibrant/color": "npm:3.2.1-alpha.1" | ||||
|   | ||||
		Reference in New Issue
	
	Block a user