Add multiselect area to automation/scene/script (#20604)

This commit is contained in:
Bram Kragten 2024-04-24 12:10:43 +02:00 committed by GitHub
parent 7e9b01b56d
commit 87bcd3e471
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 356 additions and 12 deletions

View File

@ -15,6 +15,7 @@ import {
mdiPlus, mdiPlus,
mdiRobotHappy, mdiRobotHappy,
mdiTag, mdiTag,
mdiTextureBox,
mdiToggleSwitch, mdiToggleSwitch,
mdiToggleSwitchOffOutline, mdiToggleSwitchOffOutline,
mdiTransitConnection, mdiTransitConnection,
@ -69,6 +70,7 @@ import type { HaMenu } from "../../../components/ha-menu";
import "../../../components/ha-menu-item"; import "../../../components/ha-menu-item";
import "../../../components/ha-sub-menu"; import "../../../components/ha-sub-menu";
import "../../../components/ha-svg-icon"; import "../../../components/ha-svg-icon";
import { createAreaRegistryEntry } from "../../../data/area_registry";
import { import {
AutomationEntity, AutomationEntity,
deleteAutomation, deleteAutomation,
@ -106,6 +108,7 @@ import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route, ServiceCallResponse } from "../../../types"; import { HomeAssistant, Route, ServiceCallResponse } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url"; import { documentationUrl } from "../../../util/documentation-url";
import { turnOnOffEntity } from "../../lovelace/common/entity/turn-on-off-entity"; import { turnOnOffEntity } from "../../lovelace/common/entity/turn-on-off-entity";
import { showAreaRegistryDetailDialog } from "../areas/show-dialog-area-registry-detail";
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category"; import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail"; import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail";
import { configSections } from "../ha-panel-config"; import { configSections } from "../ha-panel-config";
@ -403,6 +406,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
${this.hass.localize("ui.panel.config.category.editor.add")} ${this.hass.localize("ui.panel.config.category.editor.add")}
</div> </div>
</ha-menu-item>`; </ha-menu-item>`;
const labelItems = html`${this._labels?.map((label) => { const labelItems = html`${this._labels?.map((label) => {
const color = label.color ? computeCssColor(label.color) : undefined; const color = label.color ? computeCssColor(label.color) : undefined;
const selected = this._selected.every((entityId) => const selected = this._selected.every((entityId) =>
@ -440,10 +444,45 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
</div></ha-menu-item </div></ha-menu-item
>`; >`;
const labelsInOverflow = const areaItems = html`${Object.values(this.hass.areas).map(
(this._sizeController.value && this._sizeController.value < 700) || (area) =>
html`<ha-menu-item
.value=${area.area_id}
@click=${this._handleBulkArea}
>
${area.icon
? html`<ha-icon slot="start" .icon=${area.icon}></ha-icon>`
: html`<ha-svg-icon
slot="start"
.path=${mdiTextureBox}
></ha-svg-icon>`}
<div slot="headline">${area.name}</div>
</ha-menu-item>`
)}
<ha-menu-item .value=${null} @click=${this._handleBulkArea}>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.no_area"
)}
</div>
</ha-menu-item>
<md-divider role="separator" tabindex="-1"></md-divider>
<ha-menu-item @click=${this._bulkCreateArea}>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.add_area"
)}
</div>
</ha-menu-item>`;
const areasInOverflow =
(this._sizeController.value && this._sizeController.value < 900) ||
(!this._sizeController.value && this.hass.dockedSidebar === "docked"); (!this._sizeController.value && this.hass.dockedSidebar === "docked");
const labelsInOverflow =
areasInOverflow &&
(!this._sizeController.value || this._sizeController.value < 700);
const automations = this._automations( const automations = this._automations(
this.automations, this.automations,
this._entityReg, this._entityReg,
@ -598,6 +637,22 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
></ha-svg-icon> ></ha-svg-icon>
</ha-assist-chip> </ha-assist-chip>
${labelItems} ${labelItems}
</ha-button-menu-new>`}
${areasInOverflow
? nothing
: html`<ha-button-menu-new slot="selection-bar">
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.move_area"
)}
>
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-assist-chip>
${areaItems}
</ha-button-menu-new>`}` </ha-button-menu-new>`}`
: nothing : nothing
} }
@ -662,6 +717,24 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
</ha-sub-menu>` </ha-sub-menu>`
: nothing : nothing
} }
${
this.narrow || areasInOverflow
? html`<ha-sub-menu>
<ha-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.move_area"
)}
</div>
<ha-svg-icon
slot="end"
.path=${mdiChevronRight}
></ha-svg-icon>
</ha-menu-item>
<ha-menu slot="menu">${areaItems}</ha-menu>
</ha-sub-menu>`
: nothing
}
<ha-menu-item @click=${this._handleBulkEnable}> <ha-menu-item @click=${this._handleBulkEnable}>
<ha-svg-icon slot="start" .path=${mdiToggleSwitch}></ha-svg-icon> <ha-svg-icon slot="start" .path=${mdiToggleSwitch}></ha-svg-icon>
<div slot="headline"> <div slot="headline">
@ -1191,6 +1264,46 @@ ${rejected
} }
} }
private async _handleBulkArea(ev) {
const area = ev.currentTarget.value;
this._bulkAddArea(area);
}
private async _bulkAddArea(area: string) {
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
this._selected.forEach((entityId) => {
promises.push(
updateEntityRegistryEntry(this.hass, entityId, {
area_id: area,
})
);
});
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`<pre>
${rejected
.map((r) => r.reason.message || r.reason.code || r.reason)
.join("\r\n")}</pre
>`,
});
}
}
private async _bulkCreateArea() {
showAreaRegistryDetailDialog(this, {
createEntry: async (values) => {
const area = await createAreaRegistryEntry(this.hass, values);
this._bulkAddArea(area.area_id);
return area;
},
});
}
private async _handleBulkEnable() { private async _handleBulkEnable() {
const promises: Promise<ServiceCallResponse>[] = []; const promises: Promise<ServiceCallResponse>[] = [];
this._selected.forEach((entityId) => { this._selected.forEach((entityId) => {

View File

@ -15,6 +15,7 @@ import {
mdiPlay, mdiPlay,
mdiPlus, mdiPlus,
mdiTag, mdiTag,
mdiTextureBox,
} from "@mdi/js"; } from "@mdi/js";
import { differenceInDays } from "date-fns"; import { differenceInDays } from "date-fns";
import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { UnsubscribeFunc } from "home-assistant-js-websocket";
@ -61,6 +62,7 @@ import "../../../components/ha-menu-item";
import "../../../components/ha-state-icon"; import "../../../components/ha-state-icon";
import "../../../components/ha-sub-menu"; import "../../../components/ha-sub-menu";
import "../../../components/ha-svg-icon"; import "../../../components/ha-svg-icon";
import { createAreaRegistryEntry } from "../../../data/area_registry";
import { import {
CategoryRegistryEntry, CategoryRegistryEntry,
createCategoryRegistryEntry, createCategoryRegistryEntry,
@ -97,6 +99,7 @@ import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route } from "../../../types"; import { HomeAssistant, Route } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url"; import { documentationUrl } from "../../../util/documentation-url";
import { showToast } from "../../../util/toast"; import { showToast } from "../../../util/toast";
import { showAreaRegistryDetailDialog } from "../areas/show-dialog-area-registry-detail";
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category"; import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail"; import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail";
import { configSections } from "../ha-panel-config"; import { configSections } from "../ha-panel-config";
@ -406,6 +409,7 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
${this.hass.localize("ui.panel.config.category.editor.add")} ${this.hass.localize("ui.panel.config.category.editor.add")}
</div> </div>
</ha-menu-item>`; </ha-menu-item>`;
const labelItems = html` ${this._labels?.map((label) => { const labelItems = html` ${this._labels?.map((label) => {
const color = label.color ? computeCssColor(label.color) : undefined; const color = label.color ? computeCssColor(label.color) : undefined;
const selected = this._selected.every((entityId) => const selected = this._selected.every((entityId) =>
@ -442,9 +446,46 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
${this.hass.localize("ui.panel.config.labels.add_label")} ${this.hass.localize("ui.panel.config.labels.add_label")}
</div></ha-menu-item </div></ha-menu-item
>`; >`;
const labelsInOverflow =
(this._sizeController.value && this._sizeController.value < 700) || const areaItems = html`${Object.values(this.hass.areas).map(
(area) =>
html`<ha-menu-item
.value=${area.area_id}
@click=${this._handleBulkArea}
>
${area.icon
? html`<ha-icon slot="start" .icon=${area.icon}></ha-icon>`
: html`<ha-svg-icon
slot="start"
.path=${mdiTextureBox}
></ha-svg-icon>`}
<div slot="headline">${area.name}</div>
</ha-menu-item>`
)}
<ha-menu-item .value=${null} @click=${this._handleBulkArea}>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.no_area"
)}
</div>
</ha-menu-item>
<md-divider role="separator" tabindex="-1"></md-divider>
<ha-menu-item @click=${this._bulkCreateArea}>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.add_area"
)}
</div>
</ha-menu-item>`;
const areasInOverflow =
(this._sizeController.value && this._sizeController.value < 900) ||
(!this._sizeController.value && this.hass.dockedSidebar === "docked"); (!this._sizeController.value && this.hass.dockedSidebar === "docked");
const labelsInOverflow =
areasInOverflow &&
(!this._sizeController.value || this._sizeController.value < 700);
const scenes = this._scenes( const scenes = this._scenes(
this.scenes, this.scenes,
this._entityReg, this._entityReg,
@ -453,6 +494,7 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
this._labels, this._labels,
this._filteredScenes this._filteredScenes
); );
return html` return html`
<hass-tabs-subpage-data-table <hass-tabs-subpage-data-table
.hass=${this.hass} .hass=${this.hass}
@ -582,9 +624,25 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
></ha-svg-icon> ></ha-svg-icon>
</ha-assist-chip> </ha-assist-chip>
${labelItems} ${labelItems}
</ha-button-menu-new>`}
${areasInOverflow
? nothing
: html`<ha-button-menu-new slot="selection-bar">
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.move_area"
)}
>
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-assist-chip>
${areaItems}
</ha-button-menu-new>`}` </ha-button-menu-new>`}`
: nothing} : nothing}
${this.narrow || labelsInOverflow ${this.narrow || areasInOverflow
? html` ? html`
<ha-button-menu-new has-overflow slot="selection-bar"> <ha-button-menu-new has-overflow slot="selection-bar">
${ ${
@ -630,8 +688,8 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
: nothing : nothing
} }
${ ${
this.narrow || this.hass.dockedSidebar === "docked" this.narrow || labelsInOverflow
? html` <ha-sub-menu> ? html`<ha-sub-menu>
<ha-menu-item slot="item"> <ha-menu-item slot="item">
<div slot="headline"> <div slot="headline">
${this.hass.localize( ${this.hass.localize(
@ -647,6 +705,24 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
</ha-sub-menu>` </ha-sub-menu>`
: nothing : nothing
} }
${
this.narrow || areasInOverflow
? html`<ha-sub-menu>
<ha-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.move_area"
)}
</div>
<ha-svg-icon
slot="end"
.path=${mdiChevronRight}
></ha-svg-icon>
</ha-menu-item>
<ha-menu slot="menu">${areaItems}</ha-menu>
</ha-sub-menu>`
: nothing
}
</ha-button-menu-new>` </ha-button-menu-new>`
: nothing} : nothing}
${!this.scenes.length ${!this.scenes.length
@ -875,6 +951,46 @@ ${rejected
} }
} }
private async _handleBulkArea(ev) {
const area = ev.currentTarget.value;
this._bulkAddArea(area);
}
private async _bulkAddArea(area: string) {
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
this._selected.forEach((entityId) => {
promises.push(
updateEntityRegistryEntry(this.hass, entityId, {
area_id: area,
})
);
});
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`<pre>
${rejected
.map((r) => r.reason.message || r.reason.code || r.reason)
.join("\r\n")}</pre
>`,
});
}
}
private async _bulkCreateArea() {
showAreaRegistryDetailDialog(this, {
createEntry: async (values) => {
const area = await createAreaRegistryEntry(this.hass, values);
this._bulkAddArea(area.area_id);
return area;
},
});
}
private _editCategory(scene: any) { private _editCategory(scene: any) {
const entityReg = this._entityReg.find( const entityReg = this._entityReg.find(
(reg) => reg.entity_id === scene.entity_id (reg) => reg.entity_id === scene.entity_id

View File

@ -13,6 +13,7 @@ import {
mdiPlus, mdiPlus,
mdiScriptText, mdiScriptText,
mdiTag, mdiTag,
mdiTextureBox,
mdiTransitConnection, mdiTransitConnection,
} from "@mdi/js"; } from "@mdi/js";
import { differenceInDays } from "date-fns"; import { differenceInDays } from "date-fns";
@ -61,6 +62,7 @@ import "../../../components/ha-icon-overflow-menu";
import "../../../components/ha-menu-item"; import "../../../components/ha-menu-item";
import "../../../components/ha-sub-menu"; import "../../../components/ha-sub-menu";
import "../../../components/ha-svg-icon"; import "../../../components/ha-svg-icon";
import { createAreaRegistryEntry } from "../../../data/area_registry";
import { import {
CategoryRegistryEntry, CategoryRegistryEntry,
createCategoryRegistryEntry, createCategoryRegistryEntry,
@ -98,6 +100,7 @@ import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route } from "../../../types"; import { HomeAssistant, Route } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url"; import { documentationUrl } from "../../../util/documentation-url";
import { showToast } from "../../../util/toast"; import { showToast } from "../../../util/toast";
import { showAreaRegistryDetailDialog } from "../areas/show-dialog-area-registry-detail";
import { showNewAutomationDialog } from "../automation/show-dialog-new-automation"; import { showNewAutomationDialog } from "../automation/show-dialog-new-automation";
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category"; import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail"; import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail";
@ -418,6 +421,7 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
${this.hass.localize("ui.panel.config.category.editor.add")} ${this.hass.localize("ui.panel.config.category.editor.add")}
</div> </div>
</ha-menu-item>`; </ha-menu-item>`;
const labelItems = html`${this._labels?.map((label) => { const labelItems = html`${this._labels?.map((label) => {
const color = label.color ? computeCssColor(label.color) : undefined; const color = label.color ? computeCssColor(label.color) : undefined;
const selected = this._selected.every((entityId) => const selected = this._selected.every((entityId) =>
@ -454,9 +458,46 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
${this.hass.localize("ui.panel.config.labels.add_label")} ${this.hass.localize("ui.panel.config.labels.add_label")}
</div></ha-menu-item </div></ha-menu-item
>`; >`;
const labelsInOverflow =
(this._sizeController.value && this._sizeController.value < 700) || const areaItems = html`${Object.values(this.hass.areas).map(
(area) =>
html`<ha-menu-item
.value=${area.area_id}
@click=${this._handleBulkArea}
>
${area.icon
? html`<ha-icon slot="start" .icon=${area.icon}></ha-icon>`
: html`<ha-svg-icon
slot="start"
.path=${mdiTextureBox}
></ha-svg-icon>`}
<div slot="headline">${area.name}</div>
</ha-menu-item>`
)}
<ha-menu-item .value=${null} @click=${this._handleBulkArea}>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.no_area"
)}
</div>
</ha-menu-item>
<md-divider role="separator" tabindex="-1"></md-divider>
<ha-menu-item @click=${this._bulkCreateArea}>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.add_area"
)}
</div>
</ha-menu-item>`;
const areasInOverflow =
(this._sizeController.value && this._sizeController.value < 900) ||
(!this._sizeController.value && this.hass.dockedSidebar === "docked"); (!this._sizeController.value && this.hass.dockedSidebar === "docked");
const labelsInOverflow =
areasInOverflow &&
(!this._sizeController.value || this._sizeController.value < 700);
const scripts = this._scripts( const scripts = this._scripts(
this.scripts, this.scripts,
this._entityReg, this._entityReg,
@ -608,9 +649,25 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
></ha-svg-icon> ></ha-svg-icon>
</ha-assist-chip> </ha-assist-chip>
${labelItems} ${labelItems}
</ha-button-menu-new>`}
${areasInOverflow
? nothing
: html`<ha-button-menu-new slot="selection-bar">
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.move_area"
)}
>
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-assist-chip>
${areaItems}
</ha-button-menu-new>`}` </ha-button-menu-new>`}`
: nothing} : nothing}
${this.narrow || labelsInOverflow ${this.narrow || areasInOverflow
? html` ? html`
<ha-button-menu-new has-overflow slot="selection-bar"> <ha-button-menu-new has-overflow slot="selection-bar">
${ ${
@ -656,8 +713,8 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
: nothing : nothing
} }
${ ${
this.narrow || this.hass.dockedSidebar === "docked" this.narrow || labelsInOverflow
? html` <ha-sub-menu> ? html`<ha-sub-menu>
<ha-menu-item slot="item"> <ha-menu-item slot="item">
<div slot="headline"> <div slot="headline">
${this.hass.localize( ${this.hass.localize(
@ -673,6 +730,24 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
</ha-sub-menu>` </ha-sub-menu>`
: nothing : nothing
} }
${
this.narrow || areasInOverflow
? html`<ha-sub-menu>
<ha-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.move_area"
)}
</div>
<ha-svg-icon
slot="end"
.path=${mdiChevronRight}
></ha-svg-icon>
</ha-menu-item>
<ha-menu slot="menu">${areaItems}</ha-menu>
</ha-sub-menu>`
: nothing
}
</ha-button-menu-new>` </ha-button-menu-new>`
: nothing} : nothing}
${!this.scripts.length ${!this.scripts.length
@ -1111,6 +1186,46 @@ ${rejected
}); });
} }
private async _handleBulkArea(ev) {
const area = ev.currentTarget.value;
this._bulkAddArea(area);
}
private async _bulkAddArea(area: string) {
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
this._selected.forEach((entityId) => {
promises.push(
updateEntityRegistryEntry(this.hass, entityId, {
area_id: area,
})
);
});
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`<pre>
${rejected
.map((r) => r.reason.message || r.reason.code || r.reason)
.join("\r\n")}</pre
>`,
});
}
}
private async _bulkCreateArea() {
showAreaRegistryDetailDialog(this, {
createEntry: async (values) => {
const area = await createAreaRegistryEntry(this.hass, values);
this._bulkAddArea(area.area_id);
return area;
},
});
}
private _handleSortingChanged(ev: CustomEvent) { private _handleSortingChanged(ev: CustomEvent) {
this._activeSorting = ev.detail; this._activeSorting = ev.detail;
} }