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": {