Add bulk area assignment to device dashboard (#20581)

* Add bulk area assignment to device dashboard

* Update ha-config-devices-dashboard.ts
This commit is contained in:
Bram Kragten 2024-04-22 18:35:58 +02:00 committed by GitHub
parent 1b54d51e4a
commit a428ad0655
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 176 additions and 36 deletions

View File

@ -424,9 +424,11 @@ class HaAutomationPicker 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 = const labelsInOverflow =
(this._sizeController.value && this._sizeController.value < 700) || (this._sizeController.value && this._sizeController.value < 700) ||
(!this._sizeController.value && this.hass.dockedSidebar === "docked"); (!this._sizeController.value && this.hass.dockedSidebar === "docked");
const automations = this._automations( const automations = this._automations(
this.automations, this.automations,
this._entityReg, this._entityReg,

View File

@ -1,6 +1,12 @@
import { consume } from "@lit-labs/context"; import { consume } from "@lit-labs/context";
import "@lrnwebcomponents/simple-tooltip/simple-tooltip"; import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
import { mdiChevronRight, mdiMenuDown, mdiPlus } from "@mdi/js"; import {
mdiChevronRight,
mdiDotsVertical,
mdiMenuDown,
mdiPlus,
mdiTextureBox,
} from "@mdi/js";
import { import {
CSSResultGroup, CSSResultGroup,
LitElement, LitElement,
@ -10,10 +16,12 @@ import {
nothing, nothing,
} from "lit"; } from "lit";
import { ResizeController } from "@lit-labs/observers/resize-controller";
import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { computeCssColor } from "../../../common/color/compute-color"; import { computeCssColor } from "../../../common/color/compute-color";
import { storage } from "../../../common/decorators/storage";
import { HASSDomEvent } from "../../../common/dom/fire_event"; import { HASSDomEvent } from "../../../common/dom/fire_event";
import { computeStateDomain } from "../../../common/entity/compute_state_domain"; import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { import {
@ -22,6 +30,10 @@ import {
} from "../../../common/integrations/protocolIntegrationPicked"; } from "../../../common/integrations/protocolIntegrationPicked";
import { navigate } from "../../../common/navigate"; import { navigate } from "../../../common/navigate";
import { LocalizeFunc } from "../../../common/translations/localize"; import { LocalizeFunc } from "../../../common/translations/localize";
import {
hasRejectedItems,
rejectedItems,
} from "../../../common/util/promise-all-settled-results";
import { import {
DataTableColumnContainer, DataTableColumnContainer,
RowClickedEvent, RowClickedEvent,
@ -42,6 +54,7 @@ import "../../../components/ha-filter-states";
import "../../../components/ha-icon-button"; import "../../../components/ha-icon-button";
import "../../../components/ha-menu-item"; import "../../../components/ha-menu-item";
import "../../../components/ha-sub-menu"; import "../../../components/ha-sub-menu";
import { createAreaRegistryEntry } from "../../../data/area_registry";
import { ConfigEntry, sortConfigEntries } from "../../../data/config_entries"; import { ConfigEntry, sortConfigEntries } from "../../../data/config_entries";
import { fullEntitiesContext } from "../../../data/context"; import { fullEntitiesContext } from "../../../data/context";
import { import {
@ -66,16 +79,12 @@ import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { haStyle } from "../../../resources/styles"; import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route } from "../../../types"; import { HomeAssistant, Route } from "../../../types";
import { brandsUrl } from "../../../util/brands-url"; import { brandsUrl } from "../../../util/brands-url";
import { showAreaRegistryDetailDialog } from "../areas/show-dialog-area-registry-detail";
import { configSections } from "../ha-panel-config"; import { configSections } from "../ha-panel-config";
import "../integrations/ha-integration-overflow-menu"; import "../integrations/ha-integration-overflow-menu";
import { showAddIntegrationDialog } from "../integrations/show-add-integration-dialog"; import { showAddIntegrationDialog } from "../integrations/show-add-integration-dialog";
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail"; import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
import { import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
hasRejectedItems,
rejectedItems,
} from "../../../common/util/promise-all-settled-results";
import { showAlertDialog } from "../../lovelace/custom-card-helpers";
import { storage } from "../../../common/decorators/storage";
interface DeviceRowData extends DeviceRegistryEntry { interface DeviceRowData extends DeviceRegistryEntry {
device?: DeviceRowData; device?: DeviceRowData;
@ -125,6 +134,10 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
@storage({ key: "devices-table-grouping", state: false, subscribe: false }) @storage({ key: "devices-table-grouping", state: false, subscribe: false })
private _activeGrouping?: string; private _activeGrouping?: string;
private _sizeController = new ResizeController(this, {
callback: (entries) => entries[0]?.contentRect.width,
});
private _ignoreLocationChange = false; private _ignoreLocationChange = false;
public connectedCallback() { public connectedCallback() {
@ -557,6 +570,41 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
this._labels this._labels
); );
const areasInOverflow =
(this._sizeController.value && this._sizeController.value < 700) ||
(!this._sizeController.value && this.hass.dockedSidebar === "docked");
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 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((deviceId) => const selected = this._selected.every((deviceId) =>
@ -696,36 +744,77 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
${!this.narrow ${!this.narrow
? html`<ha-button-menu-new slot="selection-bar"> ? html`<ha-button-menu-new slot="selection-bar">
<ha-assist-chip <ha-assist-chip
slot="trigger" slot="trigger"
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.add_label" "ui.panel.config.automation.picker.bulk_actions.add_label"
)} )}
> >
<ha-svg-icon <ha-svg-icon
slot="trailing-icon" slot="trailing-icon"
.path=${mdiMenuDown} .path=${mdiMenuDown}
></ha-svg-icon> ></ha-svg-icon>
</ha-assist-chip> </ha-assist-chip>
${labelItems} ${labelItems}
</ha-button-menu-new>` </ha-button-menu-new>
: html` <ha-button-menu-new has-overflow slot="selection-bar"
><ha-assist-chip ${areasInOverflow
.label=${this.hass.localize( ? nothing
"ui.panel.config.automation.picker.bulk_action" : html`<ha-button-menu-new slot="selection-bar">
)} <ha-assist-chip
slot="trigger" slot="trigger"
> .label=${this.hass.localize(
<ha-svg-icon "ui.panel.config.devices.picker.bulk_actions.move_area"
slot="trailing-icon" )}
.path=${mdiMenuDown} >
></ha-svg-icon> <ha-svg-icon
</ha-assist-chip> slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-assist-chip>
${areaItems}
</ha-button-menu-new>`}`
: nothing}
${this.narrow || areasInOverflow
? html`<ha-button-menu-new has-overflow slot="selection-bar">
${this.narrow
? html`<ha-assist-chip
.label=${this.hass.localize(
"ui.panel.config.automation.picker.bulk_action"
)}
slot="trigger"
>
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-assist-chip>`
: html`<ha-icon-button
.path=${mdiDotsVertical}
.label=${"ui.panel.config.automation.picker.bulk_action"}
slot="trigger"
></ha-icon-button>`}
${this.narrow
? html` <ha-sub-menu>
<ha-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.add_label"
)}
</div>
<ha-svg-icon
slot="end"
.path=${mdiChevronRight}
></ha-svg-icon>
</ha-menu-item>
<ha-menu slot="menu">${labelItems}</ha-menu>
</ha-sub-menu>`
: nothing}
<ha-sub-menu> <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(
"ui.panel.config.automation.picker.bulk_actions.add_label" "ui.panel.config.devices.picker.bulk_actions.move_area"
)} )}
</div> </div>
<ha-svg-icon <ha-svg-icon
@ -733,9 +822,10 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
.path=${mdiChevronRight} .path=${mdiChevronRight}
></ha-svg-icon> ></ha-svg-icon>
</ha-menu-item> </ha-menu-item>
<ha-menu slot="menu">${labelItems}</ha-menu> <ha-menu slot="menu">${areaItems}</ha-menu>
</ha-sub-menu> </ha-sub-menu>
</ha-button-menu-new>`} </ha-button-menu-new>`
: nothing}
</hass-tabs-subpage-data-table> </hass-tabs-subpage-data-table>
`; `;
} }
@ -821,6 +911,46 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
this._selected = ev.detail.value; this._selected = ev.detail.value;
} }
private async _handleBulkArea(ev) {
const area = ev.currentTarget.value;
this._bulkAddArea(area);
}
private async _bulkAddArea(area: string) {
const promises: Promise<DeviceRegistryEntry>[] = [];
this._selected.forEach((deviceId) => {
promises.push(
updateDeviceRegistryEntry(this.hass, deviceId, {
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 _handleBulkLabel(ev) { private async _handleBulkLabel(ev) {
const label = ev.currentTarget.value; const label = ev.currentTarget.value;
const action = ev.currentTarget.action; const action = ev.currentTarget.action;
@ -878,6 +1008,9 @@ ${rejected
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
css` css`
:host {
display: block;
}
hass-tabs-subpage-data-table { hass-tabs-subpage-data-table {
--data-table-row-height: 60px; --data-table-row-height: 60px;
} }

View File

@ -4026,7 +4026,12 @@
"confirm_delete_integration": "Are you sure you want to remove this device from {integration}?", "confirm_delete_integration": "Are you sure you want to remove this device from {integration}?",
"picker": { "picker": {
"search": "Search {number} devices", "search": "Search {number} devices",
"state": "State" "state": "State",
"bulk_actions": {
"move_area": "Move to area",
"no_area": "No area",
"add_area": "Add area"
}
} }
}, },
"entities": { "entities": {