From bb20ab8c2cad1084e6ff5874b3c5f3687794e339 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 3 Apr 2024 09:54:49 +0200 Subject: [PATCH 01/17] Fix removing labels (#20354) --- src/components/ha-labels-picker.ts | 33 ++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/src/components/ha-labels-picker.ts b/src/components/ha-labels-picker.ts index 2cc592e26e..4d59f33045 100644 --- a/src/components/ha-labels-picker.ts +++ b/src/components/ha-labels-picker.ts @@ -2,8 +2,10 @@ import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; import { LitElement, TemplateResult, css, html, nothing } from "lit"; import { customElement, property, query, state } from "lit/decorators"; import { repeat } from "lit/directives/repeat"; +import memoizeOne from "memoize-one"; import { computeCssColor } from "../common/color/compute-color"; import { fireEvent } from "../common/dom/fire_event"; +import { stringCompare } from "../common/string/compare"; import { LabelRegistryEntry, subscribeLabelRegistry, @@ -17,7 +19,6 @@ 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) { @@ -102,25 +103,35 @@ export class HaLabelsPicker extends SubscribeMixin(LitElement) { ]; } + private _sortedLabels = memoizeOne( + ( + value: string[] | undefined, + labels: { [id: string]: LabelRegistryEntry } | undefined, + language: string + ) => + value + ?.map((id) => labels?.[id]) + .sort((a, b) => stringCompare(a?.name || "", b?.name || "", language)) + ); + protected render(): TemplateResult { - const labels = this.value - ?.map((id) => this._labels?.[id]) - .sort((a, b) => - stringCompare(a?.name || "", b?.name || "", this.hass.locale.language) - ); + const labels = this._sortedLabels( + this.value, + this._labels, + this.hass.locale.language + ); return html` ${labels?.length ? html` ${repeat( labels, (label) => label?.label_id, - (label, idx) => { + (label) => { const color = label?.color ? computeCssColor(label.color) : undefined; return html` id !== label.label_id)); } private _openDetail(ev) { - const label = ev.target.item; + const label = ev.currentTarget.item; showLabelDetailDialog(this, { entry: label, updateEntry: async (values) => { From 6c1f328d71befe30bc59363184ec4fe1d69a48a7 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 3 Apr 2024 10:12:25 +0200 Subject: [PATCH 02/17] Take lang into account when sorting groups (#20355) * Take lang into account when sorting groups * make sure empty values are at the bottom --- src/components/data-table/ha-data-table.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/components/data-table/ha-data-table.ts b/src/components/data-table/ha-data-table.ts index e0f7c8894e..2c95735e82 100644 --- a/src/components/data-table/ha-data-table.ts +++ b/src/components/data-table/ha-data-table.ts @@ -33,6 +33,7 @@ import "../ha-svg-icon"; import "../search-input"; import { filterData, sortData } from "./sort-filter"; import { groupBy } from "../../common/util/group-by"; +import { stringCompare } from "../../common/string/compare"; declare global { // for fire event @@ -529,7 +530,13 @@ export class HaDataTable extends LitElement { const sorted: { [key: string]: DataTableRowData[]; } = Object.keys(grouped) - .sort() + .sort((a, b) => + stringCompare( + ["", "-", "—"].includes(a) ? "zzz" : a, + ["", "-", "—"].includes(b) ? "zzz" : b, + this.hass.locale.language + ) + ) .reduce((obj, key) => { obj[key] = grouped[key]; return obj; From 95caf8c7dfd6387e87fc32bfdffb703f3d9df5a0 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 3 Apr 2024 10:12:36 +0200 Subject: [PATCH 03/17] make subpage data table full height (#20358) --- src/layouts/hass-tabs-subpage-data-table.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/layouts/hass-tabs-subpage-data-table.ts b/src/layouts/hass-tabs-subpage-data-table.ts index 57036a9ab4..ecff75108b 100644 --- a/src/layouts/hass-tabs-subpage-data-table.ts +++ b/src/layouts/hass-tabs-subpage-data-table.ts @@ -577,6 +577,7 @@ export class HaTabsSubpageDataTable extends LitElement { return css` :host { display: block; + height: 100%; } ha-data-table { From 30d18050d1279acc73b6e90eba4e2e6f54cce518 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 3 Apr 2024 10:24:10 +0200 Subject: [PATCH 04/17] Add arrow to areas under floors (#20344) --- src/components/ha-area-floor-picker.ts | 13 +++++++++--- src/components/ha-filter-floor-areas.ts | 27 ++++++++++++++++++++----- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/src/components/ha-area-floor-picker.ts b/src/components/ha-area-floor-picker.ts index 711b550561..d19756d6eb 100644 --- a/src/components/ha-area-floor-picker.ts +++ b/src/components/ha-area-floor-picker.ts @@ -1,7 +1,7 @@ -import { mdiTextureBox } from "@mdi/js"; +import { mdiSubdirectoryArrowRight, mdiTextureBox } from "@mdi/js"; import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit"; import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; -import { LitElement, PropertyValues, TemplateResult, html } from "lit"; +import { LitElement, PropertyValues, TemplateResult, html, nothing } from "lit"; import { customElement, property, query, state } from "lit/decorators"; import memoizeOne from "memoize-one"; import { fireEvent } from "../common/dom/fire_event"; @@ -49,9 +49,16 @@ const rowRenderer: ComboBoxLitRenderer = (item) => html` + ${item.type === "area" && item.hasFloor + ? html`` + : nothing} ${item.type === "floor" ? html`` : item.icon diff --git a/src/components/ha-filter-floor-areas.ts b/src/components/ha-filter-floor-areas.ts index ba983b7349..d52db44f02 100644 --- a/src/components/ha-filter-floor-areas.ts +++ b/src/components/ha-filter-floor-areas.ts @@ -1,7 +1,11 @@ import "@material/mwc-menu/mwc-menu-surface"; -import { mdiFilterVariantRemove, mdiTextureBox } from "@mdi/js"; +import { + mdiFilterVariantRemove, + mdiSubdirectoryArrowRight, + mdiTextureBox, +} from "@mdi/js"; import { UnsubscribeFunc } from "home-assistant-js-websocket"; -import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; +import { CSSResultGroup, LitElement, css, html, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { repeat } from "lit/directives/repeat"; import memoizeOne from "memoize-one"; @@ -11,7 +15,7 @@ import { getFloorAreaLookup, subscribeFloorRegistry, } from "../data/floor_registry"; -import { findRelated, RelatedResult } from "../data/search"; +import { RelatedResult, findRelated } from "../data/search"; import { SubscribeMixin } from "../mixins/subscribe-mixin"; import { haStyleScrollbar } from "../resources/styles"; import type { HomeAssistant } from "../types"; @@ -112,6 +116,13 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) { class=${area.floor_id ? "floor" : ""} @request-selected=${this._handleItemClick} > + ${area.floor_id + ? html`` + : nothing} ${area.icon ? html`` : html` Date: Wed, 3 Apr 2024 10:28:33 +0200 Subject: [PATCH 05/17] Fix elements above filter dialog (#20359) --- src/layouts/hass-tabs-subpage-data-table.ts | 68 +++++++++++---------- 1 file changed, 35 insertions(+), 33 deletions(-) diff --git a/src/layouts/hass-tabs-subpage-data-table.ts b/src/layouts/hass-tabs-subpage-data-table.ts index ecff75108b..8ce1166172 100644 --- a/src/layouts/hass-tabs-subpage-data-table.ts +++ b/src/layouts/hass-tabs-subpage-data-table.ts @@ -349,39 +349,7 @@ export class HaTabsSubpageDataTable extends LitElement { : nothing} ${this.showFilters ? !showPane - ? html` - - - ${localize( - "ui.components.subpage-data-table.filters" - )} - ${this.filters - ? html`` - : nothing} - -
-
` + ? nothing : html`
+ ${this.showFilters && !showPane + ? html` + + + ${localize("ui.components.subpage-data-table.filters")} + ${this.filters + ? html`` + : nothing} + +
+
` + : nothing} `; } @@ -768,6 +769,7 @@ export class HaTabsSubpageDataTable extends LitElement { } ha-dialog { + --dialog-z-index: 100; --mdc-dialog-min-width: calc( 100vw - env(safe-area-inset-right) - env(safe-area-inset-left) ); From 567ee8000db1dab0975acf41d0ee6577f7c57ef6 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 3 Apr 2024 10:30:08 +0200 Subject: [PATCH 06/17] Fix toggles in automation datatable on firefox (#20360) --- src/panels/config/automation/ha-automation-picker.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/panels/config/automation/ha-automation-picker.ts b/src/panels/config/automation/ha-automation-picker.ts index 24232a69f8..0fb14ce87a 100644 --- a/src/panels/config/automation/ha-automation-picker.ts +++ b/src/panels/config/automation/ha-automation-picker.ts @@ -283,6 +283,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { sortable: true, groupable: true, title: "", + type: "overflow", label: this.hass.localize("ui.panel.config.automation.picker.state"), template: (automation) => html` Date: Wed, 3 Apr 2024 11:04:24 +0200 Subject: [PATCH 07/17] Prevent line break in selection menu (#20361) --- src/layouts/hass-tabs-subpage-data-table.ts | 23 ++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/layouts/hass-tabs-subpage-data-table.ts b/src/layouts/hass-tabs-subpage-data-table.ts index 8ce1166172..9c053db1d6 100644 --- a/src/layouts/hass-tabs-subpage-data-table.ts +++ b/src/layouts/hass-tabs-subpage-data-table.ts @@ -321,19 +321,28 @@ export class HaTabsSubpageDataTable extends LitElement { .path=${mdiMenuDown} >
- ${localize("ui.components.subpage-data-table.select_all")} + +
+ ${localize("ui.components.subpage-data-table.select_all")} +
- ${localize("ui.components.subpage-data-table.select_none")} + +
+ ${localize( + "ui.components.subpage-data-table.select_none" + )} +
${localize( - "ui.components.subpage-data-table.close_select_mode" - )} + > +
+ ${localize( + "ui.components.subpage-data-table.close_select_mode" + )} +

From 82a3b9d80f0b1c0976df2492a0bc7c711d01864c Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 3 Apr 2024 11:27:30 +0200 Subject: [PATCH 08/17] Use tree for nested floor instead of icon (#20363) --- src/components/ha-area-floor-picker.ts | 76 ++++++++++++--------- src/components/ha-filter-floor-areas.ts | 87 +++++++++++++++---------- src/components/ha-tree-indicator.ts | 36 ++++++++++ 3 files changed, 136 insertions(+), 63 deletions(-) create mode 100644 src/components/ha-tree-indicator.ts diff --git a/src/components/ha-area-floor-picker.ts b/src/components/ha-area-floor-picker.ts index d19756d6eb..12a032bdec 100644 --- a/src/components/ha-area-floor-picker.ts +++ b/src/components/ha-area-floor-picker.ts @@ -1,8 +1,9 @@ -import { mdiSubdirectoryArrowRight, mdiTextureBox } from "@mdi/js"; +import { mdiTextureBox } from "@mdi/js"; import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit"; import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; import { LitElement, PropertyValues, TemplateResult, html, nothing } from "lit"; import { customElement, property, query, state } from "lit/decorators"; +import { styleMap } from "lit/directives/style-map"; import memoizeOne from "memoize-one"; import { fireEvent } from "../common/dom/fire_event"; import { computeDomain } from "../common/entity/compute_domain"; @@ -11,6 +12,7 @@ import { ScorableTextItem, fuzzyFilterSort, } from "../common/string/filter/sequence-matching"; +import { computeRTL } from "../common/util/compute_rtl"; import { AreaRegistryEntry } from "../data/area_registry"; import { DeviceEntityDisplayLookup, @@ -32,6 +34,7 @@ import "./ha-floor-icon"; import "./ha-icon-button"; import "./ha-list-item"; import "./ha-svg-icon"; +import "./ha-tree-indicator"; type ScorableAreaFloorEntry = ScorableTextItem & FloorAreaEntry; @@ -41,35 +44,11 @@ interface FloorAreaEntry { icon: string | null; strings: string[]; type: "floor" | "area"; - hasFloor?: boolean; level: number | null; + hasFloor?: boolean; + lastArea?: boolean; } -const rowRenderer: ComboBoxLitRenderer = (item) => - html` - ${item.type === "area" && item.hasFloor - ? html`` - : nothing} - ${item.type === "floor" - ? html`` - : item.icon - ? html`` - : html``} - ${item.name} - `; - @customElement("ha-area-floor-picker") export class HaAreaFloorPicker extends SubscribeMixin(LitElement) { @property({ attribute: false }) public hass!: HomeAssistant; @@ -158,6 +137,44 @@ export class HaAreaFloorPicker extends SubscribeMixin(LitElement) { await this.comboBox?.focus(); } + private _rowRenderer: ComboBoxLitRenderer = (item) => { + const rtl = computeRTL(this.hass); + return html` + + ${item.type === "area" && item.hasFloor + ? html`` + : nothing} + ${item.type === "floor" + ? html`` + : item.icon + ? html`` + : html``} + ${item.name} + + `; + }; + private _getAreas = memoizeOne( ( floors: FloorRegistryEntry[], @@ -371,7 +388,7 @@ export class HaAreaFloorPicker extends SubscribeMixin(LitElement) { }); } output.push( - ...floorAreas.map((area) => ({ + ...floorAreas.map((area, index, array) => ({ id: area.area_id, type: "area" as const, name: area.name, @@ -379,6 +396,7 @@ export class HaAreaFloorPicker extends SubscribeMixin(LitElement) { strings: [area.area_id, ...area.aliases, area.name], hasFloor: true, level: null, + lastArea: index === array.length - 1, })) ); }); @@ -452,7 +470,7 @@ export class HaAreaFloorPicker extends SubscribeMixin(LitElement) { .placeholder=${this.placeholder ? this.hass.areas[this.placeholder]?.name : undefined} - .renderer=${rowRenderer} + .renderer=${this._rowRenderer} @filter-changed=${this._filterChanged} @opened-changed=${this._openedChanged} @value-changed=${this._areaChanged} diff --git a/src/components/ha-filter-floor-areas.ts b/src/components/ha-filter-floor-areas.ts index d52db44f02..8810c9d6d2 100644 --- a/src/components/ha-filter-floor-areas.ts +++ b/src/components/ha-filter-floor-areas.ts @@ -1,15 +1,13 @@ import "@material/mwc-menu/mwc-menu-surface"; -import { - mdiFilterVariantRemove, - mdiSubdirectoryArrowRight, - mdiTextureBox, -} from "@mdi/js"; +import { mdiFilterVariantRemove, mdiTextureBox } 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 { classMap } from "lit/directives/class-map"; import { repeat } from "lit/directives/repeat"; import memoizeOne from "memoize-one"; import { fireEvent } from "../common/dom/fire_event"; +import { computeRTL } from "../common/util/compute_rtl"; import { FloorRegistryEntry, getFloorAreaLookup, @@ -23,6 +21,7 @@ import "./ha-check-list-item"; import "./ha-floor-icon"; import "./ha-icon"; import "./ha-svg-icon"; +import "./ha-tree-indicator"; @customElement("ha-filter-floor-areas") export class HaFilterFloorAreas extends SubscribeMixin(LitElement) { @@ -90,8 +89,10 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) { ${repeat( floor.areas, - (area) => area.area_id, - (area) => this._renderArea(area) + (area, index) => + `${area.area_id}${index === floor.areas.length - 1 ? "___last" : ""}`, + (area, index) => + this._renderArea(area, index === floor.areas.length - 1) )} ` )} @@ -107,30 +108,37 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) { `; } - private _renderArea(area) { - return html` - ${area.floor_id - ? html`` - : nothing} - ${area.icon - ? html`` - : html``} - ${area.name} - `; + private _renderArea(area, last: boolean = false) { + const hasFloor = !!area.floor_id; + return html` + + ${hasFloor + ? html` + + ` + : nothing} + ${area.icon + ? html`` + : html``} + ${area.name} + + `; } private _handleItemClick(ev) { @@ -305,9 +313,20 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) { --mdc-list-item-graphic-margin: 16px; } .floor { - padding-left: 38px; - padding-inline-start: 38px; - --mdc-list-item-graphic-margin: 32px; + padding-left: 48px; + padding-inline-start: 48px; + padding-inline-end: 16px; + } + ha-tree-indicator { + width: 56px; + position: absolute; + top: 0px; + left: 0px; + } + .rtl ha-tree-indicator { + right: 0px; + left: initial; + transform: scaleX(-1); } .subdir { margin-inline-end: 8px; diff --git a/src/components/ha-tree-indicator.ts b/src/components/ha-tree-indicator.ts new file mode 100644 index 0000000000..5912d7650c --- /dev/null +++ b/src/components/ha-tree-indicator.ts @@ -0,0 +1,36 @@ +import { LitElement, TemplateResult, css, html } from "lit"; +import { customElement, property } from "lit/decorators"; + +@customElement("ha-tree-indicator") +export class HaTreeIndicator extends LitElement { + @property({ type: Boolean, reflect: true }) + public end?: boolean = false; + + protected render(): TemplateResult { + return html` + + + + + `; + } + + static styles = css` + :host { + display: block; + width: 48px; + height: 48px; + } + line { + stroke: var(--divider-color); + stroke-width: 2; + stroke-dasharray: 2; + } + `; +} + +declare global { + interface HTMLElementTagNameMap { + "ha-tree-indicator": HaTreeIndicator; + } +} From 5315545a4d4a65906ebe5c73efb326796080719d Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 3 Apr 2024 12:03:35 +0200 Subject: [PATCH 09/17] Add search to integration filter (#20367) --- src/components/ha-filter-integrations.ts | 40 +++++++++++++++++++----- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/src/components/ha-filter-integrations.ts b/src/components/ha-filter-integrations.ts index 2f8b6f3cf0..cab9726f44 100644 --- a/src/components/ha-filter-integrations.ts +++ b/src/components/ha-filter-integrations.ts @@ -13,6 +13,7 @@ import { import { haStyleScrollbar } from "../resources/styles"; import type { HomeAssistant } from "../types"; import "./ha-domain-icon"; +import "./search-input-outlined"; @customElement("ha-filter-integrations") export class HaFilterIntegrations extends LitElement { @@ -28,6 +29,8 @@ export class HaFilterIntegrations extends LitElement { @state() private _shouldRender = false; + @state() private _filter?: string; + protected render() { return html` ${this._manifests && this._shouldRender - ? html` + ? html` + ${repeat( - this._integrations(this._manifests, this.value), + this._integrations(this._manifests, this._filter, this.value), (i) => i.domain, (integration) => html`` )} - - ` + ` : nothing} `; @@ -103,12 +110,17 @@ export class HaFilterIntegrations extends LitElement { } private _integrations = memoizeOne( - (manifest: IntegrationManifest[], _value) => + (manifest: IntegrationManifest[], filter: string | undefined, _value) => manifest .filter( (mnfst) => - !mnfst.integration_type || - !["entity", "system", "hardware"].includes(mnfst.integration_type) + (!mnfst.integration_type || + !["entity", "system", "hardware"].includes( + mnfst.integration_type + )) && + (!filter || + mnfst.name.toLowerCase().includes(filter) || + mnfst.domain.toLowerCase().includes(filter)) ) .sort((a, b) => stringCompare( @@ -122,7 +134,11 @@ export class HaFilterIntegrations extends LitElement { private async _integrationsSelected( ev: CustomEvent>> ) { - const integrations = this._integrations(this._manifests!, this.value); + const integrations = this._integrations( + this._manifests!, + this._filter, + this.value + ); if (!ev.detail.index.size) { fireEvent(this, "data-table-filter-changed", { @@ -156,6 +172,10 @@ export class HaFilterIntegrations extends LitElement { }); } + private _handleSearchChange(ev: CustomEvent) { + this._filter = ev.detail.value.toLowerCase(); + } + static get styles(): CSSResultGroup { return [ haStyleScrollbar, @@ -195,6 +215,10 @@ export class HaFilterIntegrations extends LitElement { padding: 0px 2px; color: var(--text-primary-color); } + search-input-outlined { + display: block; + padding: 0 8px; + } `, ]; } From bfdc9a3d8670ef050368ff82ef9ed102c5ae94ce Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 3 Apr 2024 13:04:47 +0200 Subject: [PATCH 10/17] Add area to automation, scene, script tables (#20366) * Add area to automation, scene, script tables * typing --- .../config/automation/ha-automation-picker.ts | 84 +++++---- src/panels/config/scene/ha-scene-dashboard.ts | 166 ++++++++++-------- src/panels/config/script/ha-script-picker.ts | 139 ++++++++------- src/translations/en.json | 9 +- 4 files changed, 218 insertions(+), 180 deletions(-) diff --git a/src/panels/config/automation/ha-automation-picker.ts b/src/panels/config/automation/ha-automation-picker.ts index 0fb14ce87a..20b9763750 100644 --- a/src/panels/config/automation/ha-automation-picker.ts +++ b/src/panels/config/automation/ha-automation-picker.ts @@ -103,6 +103,7 @@ import { showNewAutomationDialog } from "./show-dialog-new-automation"; type AutomationItem = AutomationEntity & { name: string; + area: string | undefined; last_triggered?: string | undefined; formatted_state: string; category: string | undefined; @@ -152,6 +153,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { ( automations: AutomationEntity[], entityReg: EntityRegistryEntry[], + areas: HomeAssistant["areas"], categoryReg?: CategoryRegistryEntry[], labelReg?: LabelRegistryEntry[], filteredAutomations?: string[] | null @@ -174,6 +176,9 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { return { ...automation, name: computeStateName(automation), + area: entityRegEntry?.area_id + ? areas[entityRegEntry?.area_id]?.name + : undefined, last_triggered: automation.attributes.last_triggered || undefined, formatted_state: this.hass.formatEntityState(automation), category: category @@ -242,6 +247,13 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { `; }, }, + area: { + title: localize("ui.panel.config.automation.picker.headers.area"), + hidden: true, + groupable: true, + filterable: true, + sortable: true, + }, category: { title: localize("ui.panel.config.automation.picker.headers.category"), hidden: true, @@ -256,34 +268,32 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { template: (automation) => automation.labels.map((lbl) => lbl.name).join(" "), }, - }; - columns.last_triggered = { - sortable: true, - width: "130px", - title: localize("ui.card.automation.last_triggered"), - hidden: narrow, - template: (automation) => { - if (!automation.last_triggered) { - return this.hass.localize("ui.components.relative_time.never"); - } - const date = new Date(automation.last_triggered); - const now = new Date(); - const dayDifference = differenceInDays(now, date); - return html` - ${dayDifference > 3 - ? formatShortDateTime(date, locale, this.hass.config) - : relativeTime(date, locale)} - `; + last_triggered: { + sortable: true, + width: "130px", + title: localize("ui.card.automation.last_triggered"), + hidden: narrow, + template: (automation) => { + if (!automation.last_triggered) { + return this.hass.localize("ui.components.relative_time.never"); + } + const date = new Date(automation.last_triggered); + const now = new Date(); + const dayDifference = differenceInDays(now, date); + return html` + ${dayDifference > 3 + ? formatShortDateTime(date, locale, this.hass.config) + : relativeTime(date, locale)} + `; + }, }, - }; - - if (!this.narrow) { - columns.formatted_state = { + formatted_state: { width: "82px", sortable: true, groupable: true, title: "", type: "overflow", + hidden: narrow, label: this.hass.localize("ui.panel.config.automation.picker.state"), template: (automation) => html` `, - }; - } - - columns.actions = { - title: "", - width: "64px", - type: "icon-button", - template: (automation) => html` - - `, + }, + actions: { + title: "", + width: "64px", + type: "icon-button", + template: (automation) => html` + + `, + }, }; return columns; } @@ -401,6 +410,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { .data=${this._automations( this.automations, this._entityReg, + this.hass.areas, this._categories, this._labels, this._filteredAutomations diff --git a/src/panels/config/scene/ha-scene-dashboard.ts b/src/panels/config/scene/ha-scene-dashboard.ts index cb8bebaaf1..11a0d92dcc 100644 --- a/src/panels/config/scene/ha-scene-dashboard.ts +++ b/src/panels/config/scene/ha-scene-dashboard.ts @@ -91,6 +91,7 @@ import { computeCssColor } from "../../../common/color/compute-color"; type SceneItem = SceneEntity & { name: string; + area: string | undefined; category: string | undefined; labels: LabelRegistryEntry[]; }; @@ -136,6 +137,7 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) { ( scenes: SceneEntity[], entityReg: EntityRegistryEntry[], + areas: HomeAssistant["areas"], categoryReg?: CategoryRegistryEntry[], labelReg?: LabelRegistryEntry[], filteredScenes?: string[] | null @@ -156,6 +158,9 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) { return { ...scene, name: computeStateName(scene), + area: entityRegEntry?.area_id + ? areas[entityRegEntry?.area_id]?.name + : undefined, category: category ? categoryReg?.find((cat) => cat.category_id === category)?.name : undefined, @@ -198,6 +203,13 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) { : nothing} `, }, + area: { + title: localize("ui.panel.config.scene.picker.headers.area"), + hidden: true, + groupable: true, + filterable: true, + sortable: true, + }, category: { title: localize("ui.panel.config.scene.picker.headers.category"), hidden: true, @@ -211,14 +223,13 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) { filterable: true, template: (scene) => scene.labels.map((lbl) => lbl.name).join(" "), }, - }; - if (!narrow) { - columns.state = { + state: { title: localize( "ui.panel.config.scene.picker.headers.last_activated" ), sortable: true, width: "30%", + hidden: narrow, template: (scene) => { const lastActivated = scene.state; if (!lastActivated || isUnavailableState(lastActivated)) { @@ -233,80 +244,80 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) { : relativeTime(date, this.hass.locale)} `; }, - }; - } - columns.only_editable = { - title: "", - width: "56px", - template: (scene) => - !scene.attributes.id - ? html` - - ${this.hass.localize( - "ui.panel.config.scene.picker.only_editable" - )} - - - ` - : "", - }; - columns.actions = { - title: "", - width: "64px", - type: "overflow-menu", - template: (scene) => html` - this._showInfo(scene), - }, - { - path: mdiPlay, - label: this.hass.localize( - "ui.panel.config.scene.picker.activate" - ), - action: () => this._activateScene(scene), - }, - { - path: mdiTag, - label: this.hass.localize( - `ui.panel.config.scene.picker.${scene.category ? "edit_category" : "assign_category"}` - ), - action: () => this._editCategory(scene), - }, - { - divider: true, - }, - { - path: mdiContentDuplicate, - label: this.hass.localize( - "ui.panel.config.scene.picker.duplicate" - ), - action: () => this._duplicate(scene), - disabled: !scene.attributes.id, - }, - { - label: this.hass.localize( - "ui.panel.config.scene.picker.delete" - ), - path: mdiDelete, - action: () => this._deleteConfirm(scene), - warning: scene.attributes.id, - disabled: !scene.attributes.id, - }, - ]} - > - - `, + }, + only_editable: { + title: "", + width: "56px", + template: (scene) => + !scene.attributes.id + ? html` + + ${this.hass.localize( + "ui.panel.config.scene.picker.only_editable" + )} + + + ` + : "", + }, + actions: { + title: "", + width: "64px", + type: "overflow-menu", + template: (scene) => html` + this._showInfo(scene), + }, + { + path: mdiPlay, + label: this.hass.localize( + "ui.panel.config.scene.picker.activate" + ), + action: () => this._activateScene(scene), + }, + { + path: mdiTag, + label: this.hass.localize( + `ui.panel.config.scene.picker.${scene.category ? "edit_category" : "assign_category"}` + ), + action: () => this._editCategory(scene), + }, + { + divider: true, + }, + { + path: mdiContentDuplicate, + label: this.hass.localize( + "ui.panel.config.scene.picker.duplicate" + ), + action: () => this._duplicate(scene), + disabled: !scene.attributes.id, + }, + { + label: this.hass.localize( + "ui.panel.config.scene.picker.delete" + ), + path: mdiDelete, + action: () => this._deleteConfirm(scene), + warning: scene.attributes.id, + disabled: !scene.attributes.id, + }, + ]} + > + + `, + }, }; return columns; @@ -386,6 +397,7 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) { .data=${this._scenes( this.scenes, this._entityReg, + this.hass.areas, this._categories, this._labels, this._filteredScenes diff --git a/src/panels/config/script/ha-script-picker.ts b/src/panels/config/script/ha-script-picker.ts index 15441b45da..5e1e764b47 100644 --- a/src/panels/config/script/ha-script-picker.ts +++ b/src/panels/config/script/ha-script-picker.ts @@ -93,6 +93,7 @@ import { computeCssColor } from "../../../common/color/compute-color"; type ScriptItem = ScriptEntity & { name: string; + area: string | undefined; category: string | undefined; labels: LabelRegistryEntry[]; }; @@ -140,6 +141,7 @@ class HaScriptPicker extends SubscribeMixin(LitElement) { ( scripts: ScriptEntity[], entityReg: EntityRegistryEntry[], + areas: HomeAssistant["areas"], categoryReg?: CategoryRegistryEntry[], labelReg?: LabelRegistryEntry[], filteredScripts?: string[] | null @@ -162,6 +164,9 @@ class HaScriptPicker extends SubscribeMixin(LitElement) { return { ...script, name: computeStateName(script), + area: entityRegEntry?.area_id + ? areas[entityRegEntry?.area_id]?.name + : undefined, last_triggered: script.attributes.last_triggered || undefined, category: category ? categoryReg?.find((cat) => cat.category_id === category)?.name @@ -227,6 +232,13 @@ class HaScriptPicker extends SubscribeMixin(LitElement) { `; }, }, + area: { + title: localize("ui.panel.config.script.picker.headers.area"), + hidden: true, + groupable: true, + filterable: true, + sortable: true, + }, category: { title: localize("ui.panel.config.script.picker.headers.category"), hidden: true, @@ -240,9 +252,8 @@ class HaScriptPicker extends SubscribeMixin(LitElement) { filterable: true, template: (script) => script.labels.map((lbl) => lbl.name).join(" "), }, - }; - if (!narrow) { - columns.last_triggered = { + last_triggered: { + hidden: narrow, sortable: true, width: "40%", title: localize("ui.card.automation.last_triggered"), @@ -262,66 +273,67 @@ class HaScriptPicker extends SubscribeMixin(LitElement) { : this.hass.localize("ui.components.relative_time.never")} `; }, - }; - } - - columns.actions = { - title: "", - width: "64px", - type: "overflow-menu", - template: (script) => html` - this._showInfo(script), - }, - { - path: mdiTag, - label: this.hass.localize( - `ui.panel.config.script.picker.${script.category ? "edit_category" : "assign_category"}` - ), - action: () => this._editCategory(script), - }, - { - path: mdiPlay, - label: this.hass.localize("ui.panel.config.script.picker.run"), - action: () => this._runScript(script), - }, - { - path: mdiTransitConnection, - label: this.hass.localize( - "ui.panel.config.script.picker.show_trace" - ), - action: () => this._showTrace(script), - }, - { - divider: true, - }, - { - path: mdiContentDuplicate, - label: this.hass.localize( - "ui.panel.config.script.picker.duplicate" - ), - action: () => this._duplicate(script), - }, - { - label: this.hass.localize( - "ui.panel.config.script.picker.delete" - ), - path: mdiDelete, - action: () => this._deleteConfirm(script), - warning: true, - }, - ]} - > - - `, + }, + actions: { + title: "", + width: "64px", + type: "overflow-menu", + template: (script) => html` + this._showInfo(script), + }, + { + path: mdiTag, + label: this.hass.localize( + `ui.panel.config.script.picker.${script.category ? "edit_category" : "assign_category"}` + ), + action: () => this._editCategory(script), + }, + { + path: mdiPlay, + label: this.hass.localize( + "ui.panel.config.script.picker.run" + ), + action: () => this._runScript(script), + }, + { + path: mdiTransitConnection, + label: this.hass.localize( + "ui.panel.config.script.picker.show_trace" + ), + action: () => this._showTrace(script), + }, + { + divider: true, + }, + { + path: mdiContentDuplicate, + label: this.hass.localize( + "ui.panel.config.script.picker.duplicate" + ), + action: () => this._duplicate(script), + }, + { + label: this.hass.localize( + "ui.panel.config.script.picker.delete" + ), + path: mdiDelete, + action: () => this._deleteConfirm(script), + warning: true, + }, + ]} + > + + `, + }, }; return columns; @@ -401,6 +413,7 @@ class HaScriptPicker extends SubscribeMixin(LitElement) { .data=${this._scripts( this.scripts, this._entityReg, + this.hass.areas, this._categories, this._labels, this._filteredScripts diff --git a/src/translations/en.json b/src/translations/en.json index f41492dce9..90ccb308cd 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2686,7 +2686,8 @@ "trigger": "Trigger", "actions": "Actions", "state": "State", - "category": "Category" + "category": "Category", + "area": "Area" }, "bulk_action": "Action", "bulk_actions": { @@ -3560,7 +3561,8 @@ "headers": { "name": "Name", "state": "State", - "category": "Category" + "category": "Category", + "area": "Area" }, "edit_category": "[%key:ui::panel::config::automation::picker::edit_category%]", "assign_category": "[%key:ui::panel::config::automation::picker::assign_category%]", @@ -3669,7 +3671,8 @@ "state": "State", "name": "Name", "last_activated": "Last activated", - "category": "Category" + "category": "Category", + "area": "Area" }, "edit_category": "[%key:ui::panel::config::automation::picker::edit_category%]", "assign_category": "[%key:ui::panel::config::automation::picker::assign_category%]", From 578d3c42601a1f9542dc7f68b00ff927706a3286 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 3 Apr 2024 13:10:51 +0200 Subject: [PATCH 11/17] Set input and button background color to white for toolbar (#20369) --- src/components/chips/ha-assist-chip.ts | 4 ++-- src/components/ha-outlined-text-field.ts | 4 ++++ src/components/search-input-outlined.ts | 1 + src/layouts/hass-tabs-subpage-data-table.ts | 3 ++- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/components/chips/ha-assist-chip.ts b/src/components/chips/ha-assist-chip.ts index 5a7b0d1fd5..a284818fd4 100644 --- a/src/components/chips/ha-assist-chip.ts +++ b/src/components/chips/ha-assist-chip.ts @@ -45,8 +45,8 @@ export class HaAssistChip extends MdAssistChip { margin-inline-start: var(--_icon-label-space); } ::before { - background: var(--ha-assist-chip-container-color); - opacity: var(--ha-assist-chip-container-opacity); + background: var(--ha-assist-chip-container-color, transparent); + opacity: var(--ha-assist-chip-container-opacity, 1); } :where(.active)::before { background: var(--ha-assist-chip-active-container-color); diff --git a/src/components/ha-outlined-text-field.ts b/src/components/ha-outlined-text-field.ts index 3118c85049..02ed120d6e 100644 --- a/src/components/ha-outlined-text-field.ts +++ b/src/components/ha-outlined-text-field.ts @@ -27,6 +27,10 @@ export class HaOutlinedTextField extends MdOutlinedTextField { --md-outlined-field-focus-outline-width: 1px; --mdc-icon-size: var(--md-input-chip-icon-size, 18px); } + md-outlined-field { + background: var(--ha-outlined-text-field-container-color, transparent); + opacity: var(--ha-outlined-text-field-container-opacity, 1); + } .input { font-family: Roboto, sans-serif; } diff --git a/src/components/search-input-outlined.ts b/src/components/search-input-outlined.ts index 5e40df35c9..f0ce00c673 100644 --- a/src/components/search-input-outlined.ts +++ b/src/components/search-input-outlined.ts @@ -97,6 +97,7 @@ class SearchInputOutlined extends LitElement { ha-outlined-text-field { display: block; width: 100%; + --ha-outlined-text-field-container-color: var(--card-background-color); } ha-svg-icon, ha-icon-button { diff --git a/src/layouts/hass-tabs-subpage-data-table.ts b/src/layouts/hass-tabs-subpage-data-table.ts index 9c053db1d6..e5cf082d3c 100644 --- a/src/layouts/hass-tabs-subpage-data-table.ts +++ b/src/layouts/hass-tabs-subpage-data-table.ts @@ -743,7 +743,7 @@ export class HaTabsSubpageDataTable extends LitElement { padding: 8px 12px; box-sizing: border-box; font-size: 14px; - --ha-assist-chip-container-color: var(--primary-background-color); + --ha-assist-chip-container-color: var(--card-background-color); } .selection-controls { @@ -770,6 +770,7 @@ export class HaTabsSubpageDataTable extends LitElement { ha-assist-chip { --ha-assist-chip-container-shape: 10px; + --ha-assist-chip-container-color: var(--card-background-color); } .select-mode-chip { From ccde9cceee631c8c9c6df603b078a869fdb6f9cd Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 3 Apr 2024 13:22:40 +0200 Subject: [PATCH 12/17] Add category and filters to helpers (#20346) * Add category and filters to helpers * Add support for adding label and category in multi select * remove labels multi --- src/data/search.ts | 1 + .../config/helpers/ha-config-helpers.ts | 519 +++++++++++++++--- src/translations/en.json | 3 +- 3 files changed, 460 insertions(+), 63 deletions(-) diff --git a/src/data/search.ts b/src/data/search.ts index ec21d03d58..5011f9a4c1 100644 --- a/src/data/search.ts +++ b/src/data/search.ts @@ -28,6 +28,7 @@ export type ItemType = | "entity" | "floor" | "group" + | "label" | "scene" | "script" | "automation_blueprint" diff --git a/src/panels/config/helpers/ha-config-helpers.ts b/src/panels/config/helpers/ha-config-helpers.ts index 125663863f..809f03a23e 100644 --- a/src/panels/config/helpers/ha-config-helpers.ts +++ b/src/panels/config/helpers/ha-config-helpers.ts @@ -1,5 +1,15 @@ +import { consume } from "@lit-labs/context"; import "@lrnwebcomponents/simple-tooltip/simple-tooltip"; -import { mdiAlertCircle, mdiPencilOff, mdiPlus } from "@mdi/js"; +import { + mdiAlertCircle, + mdiChevronRight, + mdiCog, + mdiDotsVertical, + mdiMenuDown, + mdiPencilOff, + mdiPlus, + mdiTag, +} from "@mdi/js"; import { HassEntity } from "home-assistant-js-websocket"; import { CSSResultGroup, @@ -11,8 +21,9 @@ import { nothing, } from "lit"; import { customElement, property, state } from "lit/decorators"; -import { consume } from "@lit-labs/context"; import memoizeOne from "memoize-one"; +import { computeCssColor } from "../../../common/color/compute-color"; +import { HASSDomEvent } from "../../../common/dom/fire_event"; import { computeStateDomain } from "../../../common/entity/compute_state_domain"; import { navigate } from "../../../common/navigate"; import { @@ -23,22 +34,42 @@ import { extractSearchParam } from "../../../common/url/search-params"; import { DataTableColumnContainer, RowClickedEvent, + SelectionChangedEvent, } from "../../../components/data-table/ha-data-table"; import "../../../components/data-table/ha-data-table-labels"; import "../../../components/ha-fab"; +import "../../../components/ha-filter-categories"; +import "../../../components/ha-filter-devices"; +import "../../../components/ha-filter-entities"; +import "../../../components/ha-filter-floor-areas"; +import "../../../components/ha-filter-labels"; import "../../../components/ha-icon"; +import "../../../components/ha-icon-overflow-menu"; import "../../../components/ha-state-icon"; import "../../../components/ha-svg-icon"; +import { + CategoryRegistryEntry, + createCategoryRegistryEntry, + subscribeCategoryRegistry, +} from "../../../data/category_registry"; import { ConfigEntry, subscribeConfigEntries, } from "../../../data/config_entries"; import { getConfigFlowHandlers } from "../../../data/config_flow"; +import { fullEntitiesContext } from "../../../data/context"; import { EntityRegistryEntry, + UpdateEntityRegistryEntryResult, subscribeEntityRegistry, + updateEntityRegistryEntry, } from "../../../data/entity_registry"; import { domainToName } from "../../../data/integration"; +import { + LabelRegistryEntry, + createLabelRegistryEntry, + subscribeLabelRegistry, +} from "../../../data/label_registry"; import { showConfigFlowDialog } from "../../../dialogs/config-flow/show-dialog-config-flow"; import { showOptionsFlowDialog } from "../../../dialogs/config-flow/show-dialog-options-flow"; import { @@ -49,18 +80,15 @@ import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info import "../../../layouts/hass-loading-screen"; import "../../../layouts/hass-tabs-subpage-data-table"; import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; +import { haStyle } from "../../../resources/styles"; import { HomeAssistant, Route } from "../../../types"; +import { showAssignCategoryDialog } from "../category/show-dialog-assign-category"; import { 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"; +import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail"; +import { showLabelDetailDialog } from "../labels/show-dialog-label-detail"; type HelperItem = { id: string; @@ -71,6 +99,7 @@ type HelperItem = { type: string; configEntry?: ConfigEntry; entity?: HassEntity; + category: string | undefined; label_entries: LabelRegistryEntry[]; }; @@ -111,6 +140,8 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { @state() private _configEntries?: Record; + @state() private _selected: string[] = []; + @state() private _activeFilters?: string[]; @state() private _filters: Record< @@ -120,6 +151,9 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { @state() private _expandedFilter?: string; + @state() + _categories!: CategoryRegistryEntry[]; + @state() _labels!: LabelRegistryEntry[]; @@ -156,65 +190,86 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { subscribeLabelRegistry(this.hass.connection, (labels) => { this._labels = labels; }), + subscribeCategoryRegistry( + this.hass.connection, + "helpers", + (categories) => { + this._categories = categories; + } + ), ]; } private _columns = memoizeOne( - (narrow: boolean, localize: LocalizeFunc): DataTableColumnContainer => { - const columns: DataTableColumnContainer = { - icon: { - title: "", - label: localize("ui.panel.config.helpers.picker.headers.icon"), - type: "icon", - template: (helper) => - helper.entity - ? html`` - : html``, - }, - name: { - title: localize("ui.panel.config.helpers.picker.headers.name"), - main: true, - sortable: true, - filterable: true, - grows: true, - direction: "asc", - template: (helper) => html` -

${helper.name}
- ${narrow - ? html`
${helper.entity_id}
` - : nothing} - ${helper.label_entries.length - ? html` - - ` - : nothing} - `, - }, - }; - if (!narrow) { - columns.entity_id = { - title: localize("ui.panel.config.helpers.picker.headers.entity_id"), - sortable: true, - filterable: true, - width: "25%", - }; - } - columns.localized_type = { + ( + narrow: boolean, + localize: LocalizeFunc + ): DataTableColumnContainer => ({ + icon: { + title: "", + label: localize("ui.panel.config.helpers.picker.headers.icon"), + type: "icon", + template: (helper) => + helper.entity + ? html`` + : html``, + }, + name: { + title: localize("ui.panel.config.helpers.picker.headers.name"), + main: true, + sortable: true, + filterable: true, + grows: true, + direction: "asc", + template: (helper) => html` +
${helper.name}
+ ${narrow + ? html`
${helper.entity_id}
` + : nothing} + ${helper.label_entries.length + ? html` + + ` + : nothing} + `, + }, + entity_id: { + title: localize("ui.panel.config.helpers.picker.headers.entity_id"), + hidden: this.narrow, + sortable: true, + filterable: true, + width: "25%", + }, + category: { + title: localize("ui.panel.config.helpers.picker.headers.category"), + hidden: true, + groupable: true, + filterable: true, + sortable: true, + }, + labels: { + title: "", + hidden: true, + filterable: true, + template: (helper) => + helper.label_entries.map((lbl) => lbl.name).join(" "), + }, + localized_type: { title: localize("ui.panel.config.helpers.picker.headers.type"), sortable: true, width: "25%", filterable: true, groupable: true, - }; - columns.editable = { + }, + editable: { title: "", label: this.hass.localize( "ui.panel.config.helpers.picker.headers.editable" @@ -237,9 +292,36 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { ` : ""} `, - }; - return columns; - } + }, + actions: { + title: "", + width: "64px", + type: "overflow-menu", + template: (helper) => html` + this._openSettings(helper), + }, + { + path: mdiTag, + label: this.hass.localize( + `ui.panel.config.automation.picker.${helper.category ? "edit_category" : "assign_category"}` + ), + action: () => this._editCategory(helper), + }, + ]} + > + + `, + }, + }) ); private _getItems = memoizeOne( @@ -249,6 +331,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { entityEntries: Record, configEntries: Record, entityReg: EntityRegistryEntry[], + categoryReg?: CategoryRegistryEntry[], labelReg?: LabelRegistryEntry[], filteredStateItems?: string[] | null ): HelperItem[] => { @@ -305,6 +388,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { (reg) => reg.entity_id === item.entity_id ); const labels = labelReg && entityRegEntry?.labels; + const category = entityRegEntry?.categories.helpers; return { ...item, localized_type: item.configEntry @@ -315,6 +399,9 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { label_entries: (labels || []).map( (lbl) => labelReg!.find((label) => label.label_id === lbl)! ), + category: category + ? categoryReg?.find((cat) => cat.category_id === category)?.name + : undefined, }; }); } @@ -330,6 +417,66 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { return html` `; } + const categoryItems = html`${this._categories?.map( + (category) => + html` + ${category.icon + ? html`` + : html``} +
${category.name}
+
` + )} + +
+ ${this.hass.localize( + "ui.panel.config.automation.picker.bulk_actions.no_category" + )} +
+
+ + +
+ ${this.hass.localize("ui.panel.config.category.editor.add")} +
+
`; + const labelItems = html`${this._labels?.map((label) => { + const color = label.color ? computeCssColor(label.color) : undefined; + const selected = this._selected.every((entityId) => + this.hass.entities[entityId]?.labels.includes(label.label_id) + ); + const partial = + !selected && + this._selected.some((entityId) => + this.hass.entities[entityId]?.labels.includes(label.label_id) + ); + return html` + + + ${label.icon + ? html`` + : nothing} + ${label.name} + + `; + })} + +
+ ${this.hass.localize("ui.panel.config.labels.add_label")} +
+
`; + return html` filter.value?.length @@ -348,9 +498,11 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { this._entityEntries, this._configEntries, this._entityReg, + this._categories, this._labels, this._filteredStateItems )} + initialGroupColumn="category" .activeFilters=${this._activeFilters} @clear-filter=${this._clearFilter} @row-click=${this._openEditDialog} @@ -361,6 +513,26 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { )} class=${this.narrow ? "narrow" : ""} > + + + + + ${!this.narrow + ? html` + + + + ${categoryItems} + + ${this.hass.dockedSidebar === "docked" + ? nothing + : html` + + + + ${labelItems} + `}` + : nothing} + ${this.narrow || this.hass.dockedSidebar === "docked" + ? html` + + ${ + this.narrow + ? html` + + ` + : html`` + } + + ${ + this.narrow + ? html` + +
+ ${this.hass.localize( + "ui.panel.config.automation.picker.bulk_actions.move_category" + )} +
+ +
+ ${categoryItems} +
` + : nothing + } + ${ + this.narrow || this.hass.dockedSidebar === "docked" + ? html` + +
+ ${this.hass.localize( + "ui.panel.config.automation.picker.bulk_actions.add_label" + )} +
+ +
+ ${labelItems} +
` + : nothing + } +
` + : nothing} labelItems!.has(x))); } + if (key === "ha-filter-categories" && filter.value?.length) { + const categoryItems: Set = new Set(); + this._stateItems + .filter( + (stateItem) => + filter.value![0] === + this._entityReg.find( + (reg) => reg.entity_id === stateItem.entity_id + )?.categories.helpers + ) + .forEach((stateItem) => categoryItems.add(stateItem.entity_id)); + if (!items) { + items = categoryItems; + continue; + } + items = + "intersection" in items + ? // @ts-ignore + items.intersection(categoryItems) + : new Set([...items].filter((x) => categoryItems!.has(x))); + } } this._filteredStateItems = items ? [...items] : undefined; } @@ -446,6 +747,65 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { this._applyFilters(); } + private _editCategory(helper: any) { + const entityReg = this._entityReg.find( + (reg) => reg.entity_id === helper.entity_id + ); + if (!entityReg) { + showAlertDialog(this, { + title: this.hass.localize( + "ui.panel.config.automation.picker.no_category_support" + ), + text: this.hass.localize( + "ui.panel.config.automation.picker.no_category_entity_reg" + ), + }); + return; + } + showAssignCategoryDialog(this, { + scope: "helpers", + entityReg, + }); + } + + private async _handleBulkCategory(ev) { + const category = ev.currentTarget.value; + const promises: Promise[] = []; + this._selected.forEach((entityId) => { + promises.push( + updateEntityRegistryEntry(this.hass, entityId, { + categories: { helpers: category }, + }) + ); + }); + await Promise.all(promises); + } + + private async _handleBulkLabel(ev) { + const label = ev.currentTarget.value; + const action = ev.currentTarget.action; + const promises: Promise[] = []; + this._selected.forEach((entityId) => { + promises.push( + updateEntityRegistryEntry(this.hass, entityId, { + labels: + action === "add" + ? this.hass.entities[entityId].labels.concat(label) + : this.hass.entities[entityId].labels.filter( + (lbl) => lbl !== label + ), + }) + ); + }); + await Promise.all(promises); + } + + private _handleSelectionChanged( + ev: HASSDomEvent + ): void { + this._selected = ev.detail.value; + } + protected firstUpdated(changedProps: PropertyValues) { super.firstUpdated(changedProps); if (this.route.path === "/add") { @@ -563,10 +923,35 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { } } + private _openSettings(helper: HelperItem) { + if (helper.entity) { + showMoreInfoDialog(this, { + entityId: helper.entity_id, + view: "settings", + }); + } else { + showOptionsFlowDialog(this, helper.configEntry!); + } + } + private _createHelper() { showHelperDetailDialog(this, {}); } + private _createCategory() { + showCategoryRegistryDetailDialog(this, { + scope: "helpers", + createEntry: (values) => + createCategoryRegistryEntry(this.hass, "helpers", values), + }); + } + + private _createLabel() { + showLabelDetailDialog(this, { + createEntry: (values) => createLabelRegistryEntry(this.hass, values), + }); + } + static get styles(): CSSResultGroup { return [ haStyle, @@ -577,6 +962,16 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { hass-tabs-subpage-data-table.narrow { --data-table-row-height: 72px; } + ha-assist-chip { + --ha-assist-chip-container-shape: 10px; + } + ha-button-menu-new ha-assist-chip { + --md-assist-chip-trailing-space: 8px; + } + ha-label { + --ha-label-background-color: var(--color, var(--grey-color)); + --ha-label-background-opacity: 0.5; + } `, ]; } diff --git a/src/translations/en.json b/src/translations/en.json index 90ccb308cd..ead12d761d 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2263,7 +2263,8 @@ "name": "Name", "entity_id": "Entity ID", "type": "Type", - "editable": "Editable" + "editable": "Editable", + "category": "Category" }, "create_helper": "Create helper", "no_helpers": "Looks like you don't have any helpers yet!" From e25d4f17aa6e12aa0663be9de09b24f1b4a9c486 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 3 Apr 2024 13:23:00 +0200 Subject: [PATCH 13/17] Add create category and label to multi select (#20365) * Add create category and label to multi select * move out of map --- .../config/automation/ha-automation-picker.ts | 53 +++++++++++++---- .../devices/ha-config-devices-dashboard.ts | 39 ++++++++---- .../config/entities/ha-config-entities.ts | 37 ++++++++---- src/panels/config/scene/ha-scene-dashboard.ts | 59 ++++++++++++++----- src/panels/config/script/ha-script-picker.ts | 56 +++++++++++++----- 5 files changed, 178 insertions(+), 66 deletions(-) diff --git a/src/panels/config/automation/ha-automation-picker.ts b/src/panels/config/automation/ha-automation-picker.ts index 20b9763750..62900a2798 100644 --- a/src/panels/config/automation/ha-automation-picker.ts +++ b/src/panels/config/automation/ha-automation-picker.ts @@ -73,6 +73,7 @@ import { } from "../../../data/automation"; import { CategoryRegistryEntry, + createCategoryRegistryEntry, subscribeCategoryRegistry, } from "../../../data/category_registry"; import { fullEntitiesContext } from "../../../data/context"; @@ -84,6 +85,7 @@ import { } from "../../../data/entity_registry"; import { LabelRegistryEntry, + createLabelRegistryEntry, subscribeLabelRegistry, } from "../../../data/label_registry"; import { findRelated } from "../../../data/search"; @@ -98,7 +100,9 @@ import { HomeAssistant, Route, ServiceCallResponse } from "../../../types"; import { documentationUrl } from "../../../util/documentation-url"; import { turnOnOffEntity } from "../../lovelace/common/entity/turn-on-off-entity"; import { showAssignCategoryDialog } from "../category/show-dialog-assign-category"; +import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail"; import { configSections } from "../ha-panel-config"; +import { showLabelDetailDialog } from "../labels/show-dialog-label-detail"; import { showNewAutomationDialog } from "./show-dialog-new-automation"; type AutomationItem = AutomationEntity & { @@ -367,21 +371,32 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { "ui.panel.config.automation.picker.bulk_actions.no_category" )}
+ + + +
+ ${this.hass.localize("ui.panel.config.category.editor.add")} +
`; const labelItems = html` ${this._labels?.map((label) => { - const color = label.color ? computeCssColor(label.color) : undefined; - return html` - - ${label.icon - ? html`` - : nothing} - ${label.name} - + const color = label.color ? computeCssColor(label.color) : undefined; + return html` + + ${label.icon + ? html`` + : nothing} + ${label.name} + + `; + })} + +
+ ${this.hass.localize("ui.panel.config.labels.add_label")} +
`; - })}`; return html` + createCategoryRegistryEntry(this.hass, "automation", values), + }); + } + + private _createLabel() { + showLabelDetailDialog(this, { + createEntry: (values) => createLabelRegistryEntry(this.hass, values), + }); + } + static get styles(): CSSResultGroup { return [ haStyle, diff --git a/src/panels/config/devices/ha-config-devices-dashboard.ts b/src/panels/config/devices/ha-config-devices-dashboard.ts index 76e086cba9..e47b567e3e 100644 --- a/src/panels/config/devices/ha-config-devices-dashboard.ts +++ b/src/panels/config/devices/ha-config-devices-dashboard.ts @@ -57,6 +57,7 @@ import { import { IntegrationManifest } from "../../../data/integration"; import { LabelRegistryEntry, + createLabelRegistryEntry, subscribeLabelRegistry, } from "../../../data/label_registry"; import "../../../layouts/hass-tabs-subpage-data-table"; @@ -67,6 +68,7 @@ import { brandsUrl } from "../../../util/brands-url"; import { configSections } from "../ha-panel-config"; import "../integrations/ha-integration-overflow-menu"; import { showAddIntegrationDialog } from "../integrations/show-add-integration-dialog"; +import { showLabelDetailDialog } from "../labels/show-dialog-label-detail"; interface DeviceRowData extends DeviceRegistryEntry { device?: DeviceRowData; @@ -542,20 +544,25 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) { this._labels ); - const labelItems = html` ${this._labels?.map((label) => { - const color = label.color ? computeCssColor(label.color) : undefined; - return html` - - ${label.icon - ? html`` - : nothing} - ${label.name} - + const labelItems = html`${this._labels?.map((label) => { + const color = label.color ? computeCssColor(label.color) : undefined; + return html` + + ${label.icon + ? html`` + : nothing} + ${label.name} + + `; + })} + +
+ ${this.hass.localize("ui.panel.config.labels.add_label")} +
`; - })}`; return html` createLabelRegistryEntry(this.hass, values), + }); + } + static get styles(): CSSResultGroup { return [ css` diff --git a/src/panels/config/entities/ha-config-entities.ts b/src/panels/config/entities/ha-config-entities.ts index 80adf624d5..b4ae8573b9 100644 --- a/src/panels/config/entities/ha-config-entities.ts +++ b/src/panels/config/entities/ha-config-entities.ts @@ -70,6 +70,7 @@ import { import { entryIcon } from "../../../data/icons"; import { LabelRegistryEntry, + createLabelRegistryEntry, subscribeLabelRegistry, } from "../../../data/label_registry"; import { @@ -86,6 +87,7 @@ import type { HomeAssistant, Route } from "../../../types"; import { configSections } from "../ha-panel-config"; import "../integrations/ha-integration-overflow-menu"; import { showAddIntegrationDialog } from "../integrations/show-add-integration-dialog"; +import { showLabelDetailDialog } from "../labels/show-dialog-label-detail"; export interface StateEntity extends Omit { @@ -515,19 +517,24 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { ); const labelItems = html` ${this._labels?.map((label) => { - const color = label.color ? computeCssColor(label.color) : undefined; - return html` - - ${label.icon - ? html`` - : nothing} - ${label.name} - + const color = label.color ? computeCssColor(label.color) : undefined; + return html` + + ${label.icon + ? html`` + : nothing} + ${label.name} + + `; + })} + +
+ ${this.hass.localize("ui.panel.config.labels.add_label")} +
`; - })}`; return html` createLabelRegistryEntry(this.hass, values), + }); + } + static get styles(): CSSResultGroup { return [ haStyle, diff --git a/src/panels/config/scene/ha-scene-dashboard.ts b/src/panels/config/scene/ha-scene-dashboard.ts index 11a0d92dcc..e88f76d1e8 100644 --- a/src/panels/config/scene/ha-scene-dashboard.ts +++ b/src/panels/config/scene/ha-scene-dashboard.ts @@ -27,6 +27,7 @@ import { } from "lit"; import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; +import { computeCssColor } from "../../../common/color/compute-color"; import { formatShortDateTime } from "../../../common/datetime/format_date_time"; import { relativeTime } from "../../../common/datetime/relative_time"; import { HASSDomEvent, fireEvent } from "../../../common/dom/fire_event"; @@ -48,12 +49,13 @@ 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-state-icon"; -import "../../../components/ha-svg-icon"; import "../../../components/ha-menu-item"; +import "../../../components/ha-state-icon"; import "../../../components/ha-sub-menu"; +import "../../../components/ha-svg-icon"; import { CategoryRegistryEntry, + createCategoryRegistryEntry, subscribeCategoryRegistry, } from "../../../data/category_registry"; import { fullEntitiesContext } from "../../../data/context"; @@ -66,6 +68,7 @@ import { import { forwardHaptic } from "../../../data/haptics"; import { LabelRegistryEntry, + createLabelRegistryEntry, subscribeLabelRegistry, } from "../../../data/label_registry"; import { @@ -86,8 +89,9 @@ import { HomeAssistant, Route } from "../../../types"; import { documentationUrl } from "../../../util/documentation-url"; import { showToast } from "../../../util/toast"; import { showAssignCategoryDialog } from "../category/show-dialog-assign-category"; +import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail"; import { configSections } from "../ha-panel-config"; -import { computeCssColor } from "../../../common/color/compute-color"; +import { showLabelDetailDialog } from "../labels/show-dialog-label-detail"; type SceneItem = SceneEntity & { name: string; @@ -361,21 +365,32 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) { "ui.panel.config.automation.picker.bulk_actions.no_category" )}
+ + + +
+ ${this.hass.localize("ui.panel.config.category.editor.add")} +
`; const labelItems = html` ${this._labels?.map((label) => { - const color = label.color ? computeCssColor(label.color) : undefined; - return html` - - ${label.icon - ? html`` - : nothing} - ${label.name} - + const color = label.color ? computeCssColor(label.color) : undefined; + return html` + + ${label.icon + ? html`` + : nothing} + ${label.name} + + `; + })} + +
+ ${this.hass.localize("ui.panel.config.labels.add_label")} +
`; - })}`; return html` + createCategoryRegistryEntry(this.hass, "scene", values), + }); + } + + private _createLabel() { + showLabelDetailDialog(this, { + createEntry: (values) => createLabelRegistryEntry(this.hass, values), + }); + } + static get styles(): CSSResultGroup { return [ haStyle, diff --git a/src/panels/config/script/ha-script-picker.ts b/src/panels/config/script/ha-script-picker.ts index 5e1e764b47..c4be1b4e00 100644 --- a/src/panels/config/script/ha-script-picker.ts +++ b/src/panels/config/script/ha-script-picker.ts @@ -27,6 +27,7 @@ import { import { customElement, property, state } from "lit/decorators"; import { styleMap } from "lit/directives/style-map"; import memoizeOne from "memoize-one"; +import { computeCssColor } from "../../../common/color/compute-color"; import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { formatShortDateTime } from "../../../common/datetime/format_date_time"; import { relativeTime } from "../../../common/datetime/relative_time"; @@ -49,11 +50,12 @@ 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-svg-icon"; import "../../../components/ha-menu-item"; import "../../../components/ha-sub-menu"; +import "../../../components/ha-svg-icon"; import { CategoryRegistryEntry, + createCategoryRegistryEntry, subscribeCategoryRegistry, } from "../../../data/category_registry"; import { fullEntitiesContext } from "../../../data/context"; @@ -65,6 +67,7 @@ import { } from "../../../data/entity_registry"; import { LabelRegistryEntry, + createLabelRegistryEntry, subscribeLabelRegistry, } from "../../../data/label_registry"; import { @@ -88,8 +91,9 @@ import { documentationUrl } from "../../../util/documentation-url"; import { showToast } from "../../../util/toast"; import { showNewAutomationDialog } from "../automation/show-dialog-new-automation"; import { showAssignCategoryDialog } from "../category/show-dialog-assign-category"; +import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail"; import { configSections } from "../ha-panel-config"; -import { computeCssColor } from "../../../common/color/compute-color"; +import { showLabelDetailDialog } from "../labels/show-dialog-label-detail"; type ScriptItem = ScriptEntity & { name: string; @@ -373,22 +377,32 @@ class HaScriptPicker extends SubscribeMixin(LitElement) { ${this.hass.localize( "ui.panel.config.automation.picker.bulk_actions.no_category" )} +
+ +
+ ${this.hass.localize("ui.panel.config.category.editor.add")}
`; const labelItems = html` ${this._labels?.map((label) => { - const color = label.color ? computeCssColor(label.color) : undefined; - return html` - - ${label.icon - ? html`` - : nothing} - ${label.name} - + const color = label.color ? computeCssColor(label.color) : undefined; + return html` + + ${label.icon + ? html`` + : nothing} + ${label.name} + + `; + })} + +
+ ${this.hass.localize("ui.panel.config.labels.add_label")} +
`; - })}`; return html` + createCategoryRegistryEntry(this.hass, "script", values), + }); + } + + private _createLabel() { + showLabelDetailDialog(this, { + createEntry: (values) => createLabelRegistryEntry(this.hass, values), + }); + } + static get styles(): CSSResultGroup { return [ haStyle, From eb79a1e7d7e7b4fdeb33741428b5341814d47312 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 3 Apr 2024 14:17:21 +0200 Subject: [PATCH 14/17] Allow to remove labels in multi select (#20368) * Allow to remove labels in multi select * reducedTouchTarget * fix devices * Update ha-config-devices-dashboard.ts --- .../config/automation/ha-automation-picker.ts | 35 +++++++++-- .../devices/ha-config-devices-dashboard.ts | 33 ++++++++-- .../config/entities/ha-config-entities.ts | 63 ++++++++++++------- .../config/helpers/ha-config-helpers.ts | 1 + src/panels/config/scene/ha-scene-dashboard.ts | 31 +++++++-- src/panels/config/script/ha-script-picker.ts | 33 ++++++++-- 6 files changed, 155 insertions(+), 41 deletions(-) diff --git a/src/panels/config/automation/ha-automation-picker.ts b/src/panels/config/automation/ha-automation-picker.ts index 62900a2798..28cd56dfc5 100644 --- a/src/panels/config/automation/ha-automation-picker.ts +++ b/src/panels/config/automation/ha-automation-picker.ts @@ -378,25 +378,42 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { ${this.hass.localize("ui.panel.config.category.editor.add")}
`; - const labelItems = html` ${this._labels?.map((label) => { + const labelItems = html`${this._labels?.map((label) => { const color = label.color ? computeCssColor(label.color) : undefined; + const selected = this._selected.every((entityId) => + this.hass.entities[entityId]?.labels.includes(label.label_id) + ); + const partial = + !selected && + this._selected.some((entityId) => + this.hass.entities[entityId]?.labels.includes(label.label_id) + ); return html` + ${label.icon ? html`` : nothing} ${label.name} - `; - })} + `; + })} +
${this.hass.localize("ui.panel.config.labels.add_label")} -
-
`; + `; return html` [] = []; this._selected.forEach((entityId) => { promises.push( updateEntityRegistryEntry(this.hass, entityId, { - labels: this.hass.entities[entityId].labels.concat(label), + labels: + action === "add" + ? this.hass.entities[entityId].labels.concat(label) + : this.hass.entities[entityId].labels.filter( + (lbl) => lbl !== label + ), }) ); }); diff --git a/src/panels/config/devices/ha-config-devices-dashboard.ts b/src/panels/config/devices/ha-config-devices-dashboard.ts index e47b567e3e..ba9630e2fa 100644 --- a/src/panels/config/devices/ha-config-devices-dashboard.ts +++ b/src/panels/config/devices/ha-config-devices-dashboard.ts @@ -546,23 +546,40 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) { const labelItems = html`${this._labels?.map((label) => { const color = label.color ? computeCssColor(label.color) : undefined; + const selected = this._selected.every((deviceId) => + this.hass.devices[deviceId]?.labels.includes(label.label_id) + ); + const partial = + !selected && + this._selected.some((deviceId) => + this.hass.devices[deviceId]?.labels.includes(label.label_id) + ); return html` + ${label.icon ? html`` : nothing} ${label.name} - `; - })} + `; + })} +
${this.hass.localize("ui.panel.config.labels.add_label")} -
-
`; + `; return html` [] = []; this._selected.forEach((deviceId) => { promises.push( updateDeviceRegistryEntry(this.hass, deviceId, { - labels: this.hass.devices[deviceId].labels.concat(label), + labels: + action === "add" + ? this.hass.devices[deviceId].labels.concat(label) + : this.hass.devices[deviceId].labels.filter( + (lbl) => lbl !== label + ), }) ); }); diff --git a/src/panels/config/entities/ha-config-entities.ts b/src/panels/config/entities/ha-config-entities.ts index b4ae8573b9..ef9289b099 100644 --- a/src/panels/config/entities/ha-config-entities.ts +++ b/src/panels/config/entities/ha-config-entities.ts @@ -134,7 +134,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { { value: string[] | undefined; items: Set | undefined } > = {}; - @state() private _selectedEntities: string[] = []; + @state() private _selected: string[] = []; @state() private _expandedFilter?: string; @@ -518,10 +518,26 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { const labelItems = html` ${this._labels?.map((label) => { const color = label.color ? computeCssColor(label.color) : undefined; + const selected = this._selected.every((entityId) => + this.hass.entities[entityId]?.labels.includes(label.label_id) + ); + const partial = + !selected && + this._selected.some((entityId) => + this.hass.entities[entityId]?.labels.includes(label.label_id) + ); return html` + ${label.icon ? html`` @@ -529,12 +545,13 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { ${label.name} `; - })} + })} +
${this.hass.localize("ui.panel.config.labels.add_label")} -
-
`; + `; return html` ): void { - this._selectedEntities = ev.detail.value; + this._selected = ev.detail.value; } private async _enableSelected() { showConfirmationDialog(this, { title: this.hass.localize( "ui.panel.config.entities.picker.enable_selected.confirm_title", - { number: this._selectedEntities.length } + { number: this._selected.length } ), text: this.hass.localize( "ui.panel.config.entities.picker.enable_selected.confirm_text" @@ -921,7 +938,7 @@ ${ let require_restart = false; let reload_delay = 0; await Promise.all( - this._selectedEntities.map(async (entity) => { + this._selected.map(async (entity) => { const result = await updateEntityRegistryEntry(this.hass, entity, { disabled_by: null, }); @@ -958,7 +975,7 @@ ${ showConfirmationDialog(this, { title: this.hass.localize( "ui.panel.config.entities.picker.disable_selected.confirm_title", - { number: this._selectedEntities.length } + { number: this._selected.length } ), text: this.hass.localize( "ui.panel.config.entities.picker.disable_selected.confirm_text" @@ -966,7 +983,7 @@ ${ confirmText: this.hass.localize("ui.common.disable"), dismissText: this.hass.localize("ui.common.cancel"), confirm: () => { - this._selectedEntities.forEach((entity) => + this._selected.forEach((entity) => updateEntityRegistryEntry(this.hass, entity, { disabled_by: "user", }) @@ -980,7 +997,7 @@ ${ showConfirmationDialog(this, { title: this.hass.localize( "ui.panel.config.entities.picker.hide_selected.confirm_title", - { number: this._selectedEntities.length } + { number: this._selected.length } ), text: this.hass.localize( "ui.panel.config.entities.picker.hide_selected.confirm_text" @@ -988,7 +1005,7 @@ ${ confirmText: this.hass.localize("ui.common.hide"), dismissText: this.hass.localize("ui.common.cancel"), confirm: () => { - this._selectedEntities.forEach((entity) => + this._selected.forEach((entity) => updateEntityRegistryEntry(this.hass, entity, { hidden_by: "user", }) @@ -999,7 +1016,7 @@ ${ } private _unhideSelected() { - this._selectedEntities.forEach((entity) => + this._selected.forEach((entity) => updateEntityRegistryEntry(this.hass, entity, { hidden_by: null, }) @@ -1009,11 +1026,17 @@ ${ private async _handleBulkLabel(ev) { const label = ev.currentTarget.value; + const action = ev.currentTarget.action; const promises: Promise[] = []; - this._selectedEntities.forEach((entityId) => { + this._selected.forEach((entityId) => { promises.push( updateEntityRegistryEntry(this.hass, entityId, { - labels: this.hass.entities[entityId].labels.concat(label), + labels: + action === "add" + ? this.hass.entities[entityId].labels.concat(label) + : this.hass.entities[entityId].labels.filter( + (lbl) => lbl !== label + ), }) ); }); @@ -1021,21 +1044,19 @@ ${ } private _removeSelected() { - const removeableEntities = this._selectedEntities.filter((entity) => { + const removeableEntities = this._selected.filter((entity) => { const stateObj = this.hass.states[entity]; return stateObj?.attributes.restored; }); showConfirmationDialog(this, { title: this.hass.localize( `ui.panel.config.entities.picker.remove_selected.confirm_${ - removeableEntities.length !== this._selectedEntities.length - ? "partly_" - : "" + removeableEntities.length !== this._selected.length ? "partly_" : "" }title`, { number: removeableEntities.length } ), text: - removeableEntities.length === this._selectedEntities.length + removeableEntities.length === this._selected.length ? this.hass.localize( "ui.panel.config.entities.picker.remove_selected.confirm_text" ) @@ -1043,7 +1064,7 @@ ${ "ui.panel.config.entities.picker.remove_selected.confirm_partly_text", { removable: removeableEntities.length, - selected: this._selectedEntities.length, + selected: this._selected.length, } ), confirmText: this.hass.localize("ui.common.remove"), diff --git a/src/panels/config/helpers/ha-config-helpers.ts b/src/panels/config/helpers/ha-config-helpers.ts index 809f03a23e..a63b0c2a01 100644 --- a/src/panels/config/helpers/ha-config-helpers.ts +++ b/src/panels/config/helpers/ha-config-helpers.ts @@ -456,6 +456,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { .value=${label.label_id} .action=${selected ? "remove" : "add"} @click=${this._handleBulkLabel} + keep-open > `; const labelItems = html` ${this._labels?.map((label) => { const color = label.color ? computeCssColor(label.color) : undefined; + const selected = this._selected.every((entityId) => + this.hass.entities[entityId]?.labels.includes(label.label_id) + ); + const partial = + !selected && + this._selected.some((entityId) => + this.hass.entities[entityId]?.labels.includes(label.label_id) + ); return html` + ${label.icon ? html`` @@ -385,12 +401,13 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) { ${label.name} `; - })} + })} +
${this.hass.localize("ui.panel.config.labels.add_label")} -
-
`; + `; return html` [] = []; this._selected.forEach((entityId) => { promises.push( updateEntityRegistryEntry(this.hass, entityId, { - labels: this.hass.entities[entityId].labels.concat(label), + labels: + action === "add" + ? this.hass.entities[entityId].labels.concat(label) + : this.hass.entities[entityId].labels.filter( + (lbl) => lbl !== label + ), }) ); }); diff --git a/src/panels/config/script/ha-script-picker.ts b/src/panels/config/script/ha-script-picker.ts index c4be1b4e00..4d0242e934 100644 --- a/src/panels/config/script/ha-script-picker.ts +++ b/src/panels/config/script/ha-script-picker.ts @@ -384,12 +384,28 @@ class HaScriptPicker extends SubscribeMixin(LitElement) { ${this.hass.localize("ui.panel.config.category.editor.add")} `; - const labelItems = html` ${this._labels?.map((label) => { + const labelItems = html`${this._labels?.map((label) => { const color = label.color ? computeCssColor(label.color) : undefined; + const selected = this._selected.every((entityId) => + this.hass.entities[entityId]?.labels.includes(label.label_id) + ); + const partial = + !selected && + this._selected.some((entityId) => + this.hass.entities[entityId]?.labels.includes(label.label_id) + ); return html` + ${label.icon ? html`` @@ -397,12 +413,13 @@ class HaScriptPicker extends SubscribeMixin(LitElement) { ${label.name} `; - })} + })} +
${this.hass.localize("ui.panel.config.labels.add_label")} -
-
`; + `; return html` [] = []; this._selected.forEach((entityId) => { promises.push( updateEntityRegistryEntry(this.hass, entityId, { - labels: this.hass.entities[entityId].labels.concat(label), + labels: + action === "add" + ? this.hass.entities[entityId].labels.concat(label) + : this.hass.entities[entityId].labels.filter( + (lbl) => lbl !== label + ), }) ); }); From 034fd9b4dfde9160bf0079f773b5b999c332175f Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 3 Apr 2024 14:17:32 +0200 Subject: [PATCH 15/17] Manage areas from floor dialog (#20347) * manage areas from floor dialog * Finish * fix exclude --- src/components/ha-floor-picker.ts | 12 +- .../areas/dialog-floor-registry-detail.ts | 141 +++++++++++++++++- .../config/areas/ha-config-areas-dashboard.ts | 29 +++- .../show-dialog-floor-registry-detail.ts | 9 +- src/translations/en.json | 5 +- 5 files changed, 179 insertions(+), 17 deletions(-) diff --git a/src/components/ha-floor-picker.ts b/src/components/ha-floor-picker.ts index f8e29dc7ef..c810292677 100644 --- a/src/components/ha-floor-picker.ts +++ b/src/components/ha-floor-picker.ts @@ -10,7 +10,10 @@ import { ScorableTextItem, fuzzyFilterSort, } from "../common/string/filter/sequence-matching"; -import { AreaRegistryEntry } from "../data/area_registry"; +import { + AreaRegistryEntry, + updateAreaRegistryEntry, +} from "../data/area_registry"; import { DeviceEntityDisplayLookup, DeviceRegistryEntry, @@ -441,9 +444,14 @@ export class HaFloorPicker extends SubscribeMixin(LitElement) { showFloorRegistryDetailDialog(this, { suggestedName: newValue === ADD_NEW_SUGGESTION_ID ? this._suggestion : "", - createEntry: async (values) => { + createEntry: async (values, addedAreas) => { try { const floor = await createFloorRegistryEntry(this.hass, values); + addedAreas.forEach((areaId) => { + updateAreaRegistryEntry(this.hass, areaId, { + floor_id: floor.floor_id, + }); + }); const floors = [...this._floors!, floor]; this.comboBox.filteredItems = this._getFloors( floors, diff --git a/src/panels/config/areas/dialog-floor-registry-detail.ts b/src/panels/config/areas/dialog-floor-registry-detail.ts index 12e666873f..d59a324f16 100644 --- a/src/panels/config/areas/dialog-floor-registry-detail.ts +++ b/src/panels/config/areas/dialog-floor-registry-detail.ts @@ -1,8 +1,13 @@ import "@material/mwc-button"; import "@material/mwc-list/mwc-list"; +import { mdiTextureBox } from "@mdi/js"; import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { property, state } from "lit/decorators"; +import { repeat } from "lit/directives/repeat"; +import memoizeOne from "memoize-one"; import { fireEvent } from "../../../common/dom/fire_event"; +import "../../../components/chips/ha-chip-set"; +import "../../../components/chips/ha-input-chip"; import "../../../components/ha-alert"; import "../../../components/ha-aliases-editor"; import { createCloseHeading } from "../../../components/ha-dialog"; @@ -11,10 +16,15 @@ import "../../../components/ha-picture-upload"; import "../../../components/ha-settings-row"; import "../../../components/ha-svg-icon"; import "../../../components/ha-textfield"; -import { FloorRegistryEntryMutableParams } from "../../../data/floor_registry"; -import { haStyleDialog } from "../../../resources/styles"; +import { + FloorRegistryEntry, + FloorRegistryEntryMutableParams, +} from "../../../data/floor_registry"; +import { haStyle, haStyleDialog } from "../../../resources/styles"; import { HomeAssistant } from "../../../types"; import { FloorRegistryDetailDialogParams } from "./show-dialog-floor-registry-detail"; +import { showAreaRegistryDetailDialog } from "./show-dialog-area-registry-detail"; +import { updateAreaRegistryEntry } from "../../../data/area_registry"; class DialogFloorDetail extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; @@ -33,9 +43,11 @@ class DialogFloorDetail extends LitElement { @state() private _submitting?: boolean; - public async showDialog( - params: FloorRegistryDetailDialogParams - ): Promise { + @state() private _addedAreas = new Set(); + + @state() private _removedAreas = new Set(); + + public showDialog(params: FloorRegistryDetailDialogParams): void { this._params = params; this._error = undefined; this._name = this._params.entry @@ -44,16 +56,40 @@ class DialogFloorDetail extends LitElement { this._aliases = this._params.entry?.aliases || []; this._icon = this._params.entry?.icon || null; this._level = this._params.entry?.level ?? null; - await this.updateComplete; + this._addedAreas.clear(); + this._removedAreas.clear(); } public closeDialog(): void { this._error = ""; this._params = undefined; + this._addedAreas.clear(); + this._removedAreas.clear(); fireEvent(this, "dialog-closed", { dialog: this.localName }); } + private _floorAreas = memoizeOne( + ( + entry: FloorRegistryEntry | undefined, + areas: HomeAssistant["areas"], + added: Set, + removed: Set + ) => + Object.values(areas).filter( + (area) => + (area.floor_id === entry?.floor_id || added.has(area.area_id)) && + !removed.has(area.area_id) + ) + ); + protected render() { + const areas = this._floorAreas( + this._params?.entry, + this.hass.areas, + this._addedAreas, + this._removedAreas + ); + if (!this._params) { return nothing; } @@ -125,6 +161,52 @@ class DialogFloorDetail extends LitElement { : nothing} +

+ ${this.hass.localize( + "ui.panel.config.floors.editor.areas_section" + )} +

+ +

+ ${this.hass.localize( + "ui.panel.config.floors.editor.areas_description" + )} +

+ ${areas.length + ? html` + ${repeat( + areas, + (area) => area.area_id, + (area) => + html` + ${area.icon + ? html`` + : html``} + ` + )} + ` + : nothing} + a.area_id)} + .label=${this.hass.localize( + "ui.panel.config.floors.editor.add_area" + )} + > +

${this.hass.localize( "ui.panel.config.floors.editor.aliases_section" @@ -159,6 +241,41 @@ class DialogFloorDetail extends LitElement { `; } + private _openArea(ev) { + const area = ev.target.area; + showAreaRegistryDetailDialog(this, { + entry: area, + updateEntry: (values) => + updateAreaRegistryEntry(this.hass!, area.area_id, values), + }); + } + + private _removeArea(ev) { + const areaId = ev.target.area.area_id; + if (this._addedAreas.has(areaId)) { + this._addedAreas.delete(areaId); + this._addedAreas = new Set(this._addedAreas); + return; + } + this._removedAreas.add(areaId); + this._removedAreas = new Set(this._removedAreas); + } + + private _addArea(ev) { + const areaId = ev.detail.value; + if (!areaId) { + return; + } + ev.target.value = ""; + if (this._removedAreas.has(areaId)) { + this._removedAreas.delete(areaId); + this._removedAreas = new Set(this._removedAreas); + return; + } + this._addedAreas.add(areaId); + this._addedAreas = new Set(this._addedAreas); + } + private _isNameValid() { return this._name.trim() !== ""; } @@ -189,9 +306,13 @@ class DialogFloorDetail extends LitElement { aliases: this._aliases, }; if (create) { - await this._params!.createEntry!(values); + await this._params!.createEntry!(values, this._addedAreas); } else { - await this._params!.updateEntry!(values); + await this._params!.updateEntry!( + values, + this._addedAreas, + this._removedAreas + ); } this.closeDialog(); } catch (err: any) { @@ -209,6 +330,7 @@ class DialogFloorDetail extends LitElement { static get styles(): CSSResultGroup { return [ + haStyle, haStyleDialog, css` ha-textfield { @@ -218,6 +340,9 @@ class DialogFloorDetail extends LitElement { ha-floor-icon { color: var(--secondary-text-color); } + ha-chip-set { + margin-bottom: 8px; + } `, ]; } diff --git a/src/panels/config/areas/ha-config-areas-dashboard.ts b/src/panels/config/areas/ha-config-areas-dashboard.ts index 12584b4f22..1b00b463d8 100644 --- a/src/panels/config/areas/ha-config-areas-dashboard.ts +++ b/src/panels/config/areas/ha-config-areas-dashboard.ts @@ -414,10 +414,31 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) { private _openFloorDialog(entry?: FloorRegistryEntry) { showFloorRegistryDetailDialog(this, { entry, - createEntry: async (values) => - createFloorRegistryEntry(this.hass!, values), - updateEntry: async (values) => - updateFloorRegistryEntry(this.hass!, entry!.floor_id, values), + createEntry: async (values, addedAreas) => { + const floor = await createFloorRegistryEntry(this.hass!, values); + addedAreas.forEach((areaId) => { + updateAreaRegistryEntry(this.hass, areaId, { + floor_id: floor.floor_id, + }); + }); + }, + updateEntry: async (values, addedAreas, removedAreas) => { + const floor = await updateFloorRegistryEntry( + this.hass!, + entry!.floor_id, + values + ); + addedAreas.forEach((areaId) => { + updateAreaRegistryEntry(this.hass, areaId, { + floor_id: floor.floor_id, + }); + }); + removedAreas.forEach((areaId) => { + updateAreaRegistryEntry(this.hass, areaId, { + floor_id: null, + }); + }); + }, }); } diff --git a/src/panels/config/areas/show-dialog-floor-registry-detail.ts b/src/panels/config/areas/show-dialog-floor-registry-detail.ts index ecaa74786b..d321f60670 100644 --- a/src/panels/config/areas/show-dialog-floor-registry-detail.ts +++ b/src/panels/config/areas/show-dialog-floor-registry-detail.ts @@ -7,9 +7,14 @@ import { export interface FloorRegistryDetailDialogParams { entry?: FloorRegistryEntry; suggestedName?: string; - createEntry?: (values: FloorRegistryEntryMutableParams) => Promise; + createEntry?: ( + values: FloorRegistryEntryMutableParams, + addedAreas: Set + ) => Promise; updateEntry?: ( - updates: Partial + updates: Partial, + addedAreas: Set, + removedAreas: Set ) => Promise; } diff --git a/src/translations/en.json b/src/translations/en.json index ead12d761d..d4e2852c0a 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1927,7 +1927,10 @@ "aliases_section": "Aliases", "no_aliases": "No configured aliases", "configured_aliases": "{count} configured {count, plural,\n one {alias}\n other {aliases}\n}", - "aliases_description": "Aliases are alternative names used in voice assistants to refer to this floor." + "aliases_description": "Aliases are alternative names used in voice assistants to refer to this floor.", + "areas_section": "Areas", + "areas_description": "Specify the areas that are on this floor.", + "add_area": "Add area" } }, "category": { From 1a6d96cf3a3dc0556bb56a8ff0f57b054de61c7b Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 3 Apr 2024 14:18:07 +0200 Subject: [PATCH 16/17] Bumped version to 20240403.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c78a2c67f1..7aa35c5c66 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "home-assistant-frontend" -version = "20240402.2" +version = "20240403.0" license = {text = "Apache-2.0"} description = "The Home Assistant frontend" readme = "README.md" From 3b5b3f3bb66f6308ddc6facce825f0af31fdc37d Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 3 Apr 2024 14:40:48 +0200 Subject: [PATCH 17/17] Handle disabled entities in multi select label (#20371) --- src/panels/config/entities/ha-config-entities.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/panels/config/entities/ha-config-entities.ts b/src/panels/config/entities/ha-config-entities.ts index ef9289b099..910448ac3a 100644 --- a/src/panels/config/entities/ha-config-entities.ts +++ b/src/panels/config/entities/ha-config-entities.ts @@ -1029,14 +1029,18 @@ ${ const action = ev.currentTarget.action; const promises: Promise[] = []; this._selected.forEach((entityId) => { + const entityReg = + this.hass.entities[entityId] || + this._entities.find((entReg) => entReg.entity_id === entityId); + if (!entityReg) { + return; + } promises.push( updateEntityRegistryEntry(this.hass, entityId, { labels: action === "add" - ? this.hass.entities[entityId].labels.concat(label) - : this.hass.entities[entityId].labels.filter( - (lbl) => lbl !== label - ), + ? entityReg.labels.concat(label) + : entityReg.labels.filter((lbl) => lbl !== label), }) ); });