From 8f050516ece5f2aed09ba496b873f97db301d219 Mon Sep 17 00:00:00 2001 From: Simon Lamon <32477463+silamon@users.noreply.github.com> Date: Thu, 11 Apr 2024 11:10:02 +0200 Subject: [PATCH 1/5] Fix missing argument in voice assistant expose search label (#20491) --- .../voice-assistants/ha-config-voice-assistants-expose.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/panels/config/voice-assistants/ha-config-voice-assistants-expose.ts b/src/panels/config/voice-assistants/ha-config-voice-assistants-expose.ts index 72820db82e..c64ea12cef 100644 --- a/src/panels/config/voice-assistants/ha-config-voice-assistants-expose.ts +++ b/src/panels/config/voice-assistants/ha-config-voice-assistants-expose.ts @@ -496,7 +496,10 @@ export class VoiceAssistantsExpose extends LitElement { )} .data=${filteredEntities} .searchLabel=${this.hass.localize( - "ui.panel.config.entities.picker.search" + "ui.panel.config.entities.picker.search", + { + number: filteredEntities.length, + } )} .filter=${this._filter} selectable From c96968e4766497a35c4d7c095576e1959d0db398 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 12 Apr 2024 11:10:23 +0200 Subject: [PATCH 2/5] Fix issues with application credentials (#20495) --- .../ha-config-application-credentials.ts | 99 ++++++++----------- 1 file changed, 40 insertions(+), 59 deletions(-) diff --git a/src/panels/config/application_credentials/ha-config-application-credentials.ts b/src/panels/config/application_credentials/ha-config-application-credentials.ts index c606e4f158..1431491f3b 100644 --- a/src/panels/config/application_credentials/ha-config-application-credentials.ts +++ b/src/panels/config/application_credentials/ha-config-application-credentials.ts @@ -1,14 +1,6 @@ import { mdiDelete, mdiPlus } from "@mdi/js"; -import { - css, - CSSResultGroup, - html, - LitElement, - PropertyValues, - nothing, -} from "lit"; +import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit"; import { customElement, property, query, state } from "lit/decorators"; -import { classMap } from "lit/directives/class-map"; import memoizeOne from "memoize-one"; import type { HASSDomEvent } from "../../../common/dom/fire_event"; import { LocalizeFunc } from "../../../common/translations/localize"; @@ -59,27 +51,24 @@ export class HaConfigApplicationCredentials extends LitElement { title: localize( "ui.panel.config.application_credentials.picker.headers.name" ), + sortable: true, direction: "asc", grows: true, - template: (entry) => html`${entry.name}`, }, client_id: { title: localize( "ui.panel.config.application_credentials.picker.headers.client_id" ), width: "30%", - direction: "asc", hidden: narrow, - template: (entry) => html`${entry.client_id}`, }, - application: { + localizedDomain: { title: localize( "ui.panel.config.application_credentials.picker.headers.application" ), sortable: true, width: "30%", direction: "asc", - template: (entry) => html`${domainToName(localize, entry.domain)}`, }, }; @@ -87,6 +76,14 @@ export class HaConfigApplicationCredentials extends LitElement { } ); + private _getApplicationCredentials = memoizeOne( + (applicationCredentials: ApplicationCredential[], localize: LocalizeFunc) => + applicationCredentials.map((credential) => ({ + ...credential, + localizedDomain: domainToName(localize, credential.domain), + })) + ); + protected firstUpdated(changedProperties: PropertyValues) { super.firstUpdated(changedProperties); this._loadTranslations(); @@ -102,56 +99,40 @@ export class HaConfigApplicationCredentials extends LitElement { backPath="/config" .tabs=${configSections.devices} .columns=${this._columns(this.narrow, this.hass.localize)} - .data=${this._applicationCredentials} + .data=${this._getApplicationCredentials( + this._applicationCredentials, + this.hass.localize + )} hasFab selectable + .selected=${this._selected.length} @selection-changed=${this._handleSelectionChanged} > - ${this._selected.length - ? html` -
-

- ${this.hass.localize( - "ui.panel.config.application_credentials.picker.selected", - { number: this._selected.length } +

+ ${!this.narrow + ? html` + ${this.hass.localize( + "ui.panel.config.application_credentials.picker.remove_selected.button" + )} + ` + : html` + + -
- ${!this.narrow - ? html` - ${this.hass.localize( - "ui.panel.config.application_credentials.picker.remove_selected.button" - )} - ` - : html` - - - - `} -
-
- ` - : nothing} + > + + `} +
Date: Fri, 12 Apr 2024 11:15:15 +0200 Subject: [PATCH 3/5] Handle errors in multi select (#20494) --- .../util/promise-all-settled-results.ts | 9 +++ .../config/automation/ha-automation-picker.ts | 65 +++++++++++++++++-- .../devices/ha-config-devices-dashboard.ts | 20 +++++- .../config/entities/ha-config-entities.ts | 55 +++++++++++++--- .../config/helpers/ha-config-helpers.ts | 34 +++++++++- src/panels/config/scene/ha-scene-dashboard.ts | 35 +++++++++- src/panels/config/script/ha-script-picker.ts | 35 +++++++++- src/translations/en.json | 1 + 8 files changed, 235 insertions(+), 19 deletions(-) create mode 100644 src/common/util/promise-all-settled-results.ts diff --git a/src/common/util/promise-all-settled-results.ts b/src/common/util/promise-all-settled-results.ts new file mode 100644 index 0000000000..459febb453 --- /dev/null +++ b/src/common/util/promise-all-settled-results.ts @@ -0,0 +1,9 @@ +export const hasRejectedItems = (results: PromiseSettledResult[]) => + results.some((result) => result.status === "rejected"); + +export const rejectedItems = ( + results: PromiseSettledResult[] +): PromiseRejectedResult[] => + results.filter( + (result) => result.status === "rejected" + ) as PromiseRejectedResult[]; diff --git a/src/panels/config/automation/ha-automation-picker.ts b/src/panels/config/automation/ha-automation-picker.ts index df56a6317a..b48a6dc20f 100644 --- a/src/panels/config/automation/ha-automation-picker.ts +++ b/src/panels/config/automation/ha-automation-picker.ts @@ -105,6 +105,10 @@ import { showCategoryRegistryDetailDialog } from "../category/show-dialog-catego import { configSections } from "../ha-panel-config"; import { showLabelDetailDialog } from "../labels/show-dialog-label-detail"; import { showNewAutomationDialog } from "./show-dialog-new-automation"; +import { + hasRejectedItems, + rejectedItems, +} from "../../../common/util/promise-all-settled-results"; type AutomationItem = AutomationEntity & { name: string; @@ -196,6 +200,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { labels: (labels || []).map( (lbl) => labelReg!.find((label) => label.label_id === lbl)! ), + selectable: entityRegEntry !== undefined, }; }); } @@ -1112,7 +1117,20 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { }) ); }); - await Promise.all(promises); + const result = await Promise.allSettled(promises); + if (hasRejectedItems(result)) { + const rejected = rejectedItems(result); + showAlertDialog(this, { + title: this.hass.localize("ui.panel.config.common.multiselect.failed", { + number: rejected.length, + }), + text: html`
+${rejected
+            .map((r) => r.reason.message || r.reason.code || r.reason)
+            .join("\r\n")}
`, + }); + } } private async _handleBulkLabel(ev) { @@ -1135,7 +1153,20 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { }) ); }); - await Promise.all(promises); + const result = await Promise.allSettled(promises); + if (hasRejectedItems(result)) { + const rejected = rejectedItems(result); + showAlertDialog(this, { + title: this.hass.localize("ui.panel.config.common.multiselect.failed", { + number: rejected.length, + }), + text: html`
+${rejected
+            .map((r) => r.reason.message || r.reason.code || r.reason)
+            .join("\r\n")}
`, + }); + } } private async _handleBulkEnable() { @@ -1143,7 +1174,20 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { this._selected.forEach((entityId) => { promises.push(turnOnOffEntity(this.hass, entityId, true)); }); - await Promise.all(promises); + const result = await Promise.allSettled(promises); + if (hasRejectedItems(result)) { + const rejected = rejectedItems(result); + showAlertDialog(this, { + title: this.hass.localize("ui.panel.config.common.multiselect.failed", { + number: rejected.length, + }), + text: html`
+${rejected
+            .map((r) => r.reason.message || r.reason.code || r.reason)
+            .join("\r\n")}
`, + }); + } } private async _handleBulkDisable() { @@ -1151,7 +1195,20 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { this._selected.forEach((entityId) => { promises.push(turnOnOffEntity(this.hass, entityId, false)); }); - await Promise.all(promises); + const result = await Promise.allSettled(promises); + if (hasRejectedItems(result)) { + const rejected = rejectedItems(result); + showAlertDialog(this, { + title: this.hass.localize("ui.panel.config.common.multiselect.failed", { + number: rejected.length, + }), + text: html`
+${rejected
+            .map((r) => r.reason.message || r.reason.code || r.reason)
+            .join("\r\n")}
`, + }); + } } private async _bulkCreateCategory() { diff --git a/src/panels/config/devices/ha-config-devices-dashboard.ts b/src/panels/config/devices/ha-config-devices-dashboard.ts index 68c402ee5c..e834e453fc 100644 --- a/src/panels/config/devices/ha-config-devices-dashboard.ts +++ b/src/panels/config/devices/ha-config-devices-dashboard.ts @@ -69,6 +69,11 @@ import { configSections } from "../ha-panel-config"; import "../integrations/ha-integration-overflow-menu"; import { showAddIntegrationDialog } from "../integrations/show-add-integration-dialog"; import { showLabelDetailDialog } from "../labels/show-dialog-label-detail"; +import { + hasRejectedItems, + rejectedItems, +} from "../../../common/util/promise-all-settled-results"; +import { showAlertDialog } from "../../lovelace/custom-card-helpers"; interface DeviceRowData extends DeviceRegistryEntry { device?: DeviceRowData; @@ -824,7 +829,20 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) { }) ); }); - await Promise.all(promises); + const result = await Promise.allSettled(promises); + if (hasRejectedItems(result)) { + const rejected = rejectedItems(result); + showAlertDialog(this, { + title: this.hass.localize("ui.panel.config.common.multiselect.failed", { + number: rejected.length, + }), + text: html`
+${rejected
+            .map((r) => r.reason.message || r.reason.code || r.reason)
+            .join("\r\n")}
`, + }); + } } private _bulkCreateLabel() { diff --git a/src/panels/config/entities/ha-config-entities.ts b/src/panels/config/entities/ha-config-entities.ts index e98bc7b037..50aa67a862 100644 --- a/src/panels/config/entities/ha-config-entities.ts +++ b/src/panels/config/entities/ha-config-entities.ts @@ -90,6 +90,10 @@ import { EntitySources, fetchEntitySourcesWithCache, } from "../../../data/entity_sources"; +import { + hasRejectedItems, + rejectedItems, +} from "../../../common/util/promise-all-settled-results"; export interface StateEntity extends Omit { @@ -957,19 +961,41 @@ ${ confirm: async () => { let require_restart = false; let reload_delay = 0; - await Promise.all( + const result = await Promise.allSettled( this._selected.map(async (entity) => { - const result = await updateEntityRegistryEntry(this.hass, entity, { - disabled_by: null, - }); - if (result.require_restart) { + const updateResult = await updateEntityRegistryEntry( + this.hass, + entity, + { + disabled_by: null, + } + ); + if (updateResult.require_restart) { require_restart = true; } - if (result.reload_delay) { - reload_delay = Math.max(reload_delay, result.reload_delay); + if (updateResult.reload_delay) { + reload_delay = Math.max(reload_delay, updateResult.reload_delay); } }) ); + + if (hasRejectedItems(result)) { + const rejected = rejectedItems(result); + showAlertDialog(this, { + title: this.hass.localize( + "ui.panel.config.common.multiselect.failed", + { + number: rejected.length, + } + ), + text: html`
+    ${rejected
+                .map((r) => r.reason.message || r.reason.code || r.reason)
+                .join("\r\n")}
`, + }); + } + this._clearSelection(); // If restart is required by any entity, show a dialog. // Otherwise, show a dialog explaining that some patience is needed @@ -1068,7 +1094,20 @@ ${ }) ); }); - await Promise.all(promises); + const result = await Promise.allSettled(promises); + if (hasRejectedItems(result)) { + const rejected = rejectedItems(result); + showAlertDialog(this, { + title: this.hass.localize("ui.panel.config.common.multiselect.failed", { + number: rejected.length, + }), + text: html`
+${rejected
+            .map((r) => r.reason.message || r.reason.code || r.reason)
+            .join("\r\n")}
`, + }); + } } private _bulkCreateLabel() { diff --git a/src/panels/config/helpers/ha-config-helpers.ts b/src/panels/config/helpers/ha-config-helpers.ts index e44659e34b..14149c7d19 100644 --- a/src/panels/config/helpers/ha-config-helpers.ts +++ b/src/panels/config/helpers/ha-config-helpers.ts @@ -32,6 +32,10 @@ import { LocalizeKeys, } from "../../../common/translations/localize"; import { extractSearchParam } from "../../../common/url/search-params"; +import { + hasRejectedItems, + rejectedItems, +} from "../../../common/util/promise-all-settled-results"; import { DataTableColumnContainer, RowClickedEvent, @@ -801,7 +805,20 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { }) ); }); - await Promise.all(promises); + const result = await Promise.allSettled(promises); + if (hasRejectedItems(result)) { + const rejected = rejectedItems(result); + showAlertDialog(this, { + title: this.hass.localize("ui.panel.config.common.multiselect.failed", { + number: rejected.length, + }), + text: html`
+${rejected
+            .map((r) => r.reason.message || r.reason.code || r.reason)
+            .join("\r\n")}
`, + }); + } } private async _handleBulkLabel(ev) { @@ -824,7 +841,20 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { }) ); }); - await Promise.all(promises); + const result = await Promise.allSettled(promises); + if (hasRejectedItems(result)) { + const rejected = rejectedItems(result); + showAlertDialog(this, { + title: this.hass.localize("ui.panel.config.common.multiselect.failed", { + number: rejected.length, + }), + text: html`
+${rejected
+            .map((r) => r.reason.message || r.reason.code || r.reason)
+            .join("\r\n")}
`, + }); + } } private _handleSelectionChanged( diff --git a/src/panels/config/scene/ha-scene-dashboard.ts b/src/panels/config/scene/ha-scene-dashboard.ts index e357bb00fe..fbbe1cc334 100644 --- a/src/panels/config/scene/ha-scene-dashboard.ts +++ b/src/panels/config/scene/ha-scene-dashboard.ts @@ -95,6 +95,10 @@ import { showAssignCategoryDialog } from "../category/show-dialog-assign-categor import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail"; import { configSections } from "../ha-panel-config"; import { showLabelDetailDialog } from "../labels/show-dialog-label-detail"; +import { + hasRejectedItems, + rejectedItems, +} from "../../../common/util/promise-all-settled-results"; type SceneItem = SceneEntity & { name: string; @@ -178,6 +182,7 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) { labels: (labels || []).map( (lbl) => labelReg!.find((label) => label.label_id === lbl)! ), + selectable: entityRegEntry !== undefined, }; }); } @@ -798,7 +803,20 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) { }) ); }); - await Promise.all(promises); + const result = await Promise.allSettled(promises); + if (hasRejectedItems(result)) { + const rejected = rejectedItems(result); + showAlertDialog(this, { + title: this.hass.localize("ui.panel.config.common.multiselect.failed", { + number: rejected.length, + }), + text: html`
+${rejected
+            .map((r) => r.reason.message || r.reason.code || r.reason)
+            .join("\r\n")}
`, + }); + } } private async _handleBulkLabel(ev) { @@ -821,7 +839,20 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) { }) ); }); - await Promise.all(promises); + const result = await Promise.allSettled(promises); + if (hasRejectedItems(result)) { + const rejected = rejectedItems(result); + showAlertDialog(this, { + title: this.hass.localize("ui.panel.config.common.multiselect.failed", { + number: rejected.length, + }), + text: html`
+${rejected
+            .map((r) => r.reason.message || r.reason.code || r.reason)
+            .join("\r\n")}
`, + }); + } } private _editCategory(scene: any) { diff --git a/src/panels/config/script/ha-script-picker.ts b/src/panels/config/script/ha-script-picker.ts index 944c7dc844..b6ae00c012 100644 --- a/src/panels/config/script/ha-script-picker.ts +++ b/src/panels/config/script/ha-script-picker.ts @@ -97,6 +97,10 @@ import { showAssignCategoryDialog } from "../category/show-dialog-assign-categor import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail"; import { configSections } from "../ha-panel-config"; import { showLabelDetailDialog } from "../labels/show-dialog-label-detail"; +import { + hasRejectedItems, + rejectedItems, +} from "../../../common/util/promise-all-settled-results"; type ScriptItem = ScriptEntity & { name: string; @@ -185,6 +189,7 @@ class HaScriptPicker extends SubscribeMixin(LitElement) { labels: (labels || []).map( (lbl) => labelReg!.find((label) => label.label_id === lbl)! ), + selectable: entityRegEntry !== undefined, }; }); } @@ -867,7 +872,20 @@ class HaScriptPicker extends SubscribeMixin(LitElement) { }) ); }); - await Promise.all(promises); + const result = await Promise.allSettled(promises); + if (hasRejectedItems(result)) { + const rejected = rejectedItems(result); + showAlertDialog(this, { + title: this.hass.localize("ui.panel.config.common.multiselect.failed", { + number: rejected.length, + }), + text: html`
+${rejected
+            .map((r) => r.reason.message || r.reason.code || r.reason)
+            .join("\r\n")}
`, + }); + } } private async _handleBulkLabel(ev) { @@ -890,7 +908,20 @@ class HaScriptPicker extends SubscribeMixin(LitElement) { }) ); }); - await Promise.all(promises); + const result = await Promise.allSettled(promises); + if (hasRejectedItems(result)) { + const rejected = rejectedItems(result); + showAlertDialog(this, { + title: this.hass.localize("ui.panel.config.common.multiselect.failed", { + number: rejected.length, + }), + text: html`
+${rejected
+            .map((r) => r.reason.message || r.reason.code || r.reason)
+            .join("\r\n")}
`, + }); + } } private _handleRowClicked(ev: HASSDomEvent) { diff --git a/src/translations/en.json b/src/translations/en.json index bf3e8b9053..21e30b9e0b 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1867,6 +1867,7 @@ "editor": { "confirm_unsaved": "You have unsaved changes. Are you sure you want to leave?" }, + "multiselect": { "failed": "Failed to update {number} items." }, "learn_more": "Learn more" }, "updates": { From 2e505cfb1fba7414220520a02dcc257fd36a8df2 Mon Sep 17 00:00:00 2001 From: Simon Lamon <32477463+silamon@users.noreply.github.com> Date: Fri, 12 Apr 2024 11:17:51 +0200 Subject: [PATCH 4/5] Add spacing between icon and name in entity button bar (#20492) * Fix width between icon and name * Remove no-text --- .../lovelace/components/hui-buttons-base.ts | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/src/panels/lovelace/components/hui-buttons-base.ts b/src/panels/lovelace/components/hui-buttons-base.ts index 4fa798a8e5..2b1f9a1152 100644 --- a/src/panels/lovelace/components/hui-buttons-base.ts +++ b/src/panels/lovelace/components/hui-buttons-base.ts @@ -52,7 +52,6 @@ export class HuiButtonsBase extends LitElement { .stateObj=${stateObj} .overrideIcon=${entityConf.icon} .overrideImage=${entityConf.image} - class=${name ? "" : "no-text"} .stateColor=${true} slot="icon" > @@ -92,27 +91,8 @@ export class HuiButtonsBase extends LitElement { color: var(--secondary-text-color); align-items: center; justify-content: center; - width: 24px; - height: 24px; - margin-left: -4px; - margin-inline-start: -4px; - margin-inline-end: initial; margin-top: -2px; } - state-badge.no-text { - width: 26px; - height: 26px; - margin-left: -3px; - margin-inline-start: -3px; - margin-inline-end: initial; - margin-top: -3px; - } - ha-assist-chip state-badge { - margin-right: -4px; - margin-inline-end: -4px; - margin-inline-start: initial; - --mdc-icon-size: 18px; - } @media all and (max-width: 450px), all and (max-height: 500px) { .ha-scrollbar { flex-wrap: nowrap; From 1914de7ddf5f5c30be8dd1b85e77b4dd51b48be0 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 12 Apr 2024 11:21:05 +0200 Subject: [PATCH 5/5] Bumped version to 20240404.2 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1ac865e1d9..42670dd060 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "home-assistant-frontend" -version = "20240404.1" +version = "20240404.2" license = {text = "Apache-2.0"} description = "The Home Assistant frontend" readme = "README.md"