diff --git a/src/panels/config/automation/ha-automation-picker.ts b/src/panels/config/automation/ha-automation-picker.ts
index 0d1d506d6a..4d58866e34 100644
--- a/src/panels/config/automation/ha-automation-picker.ts
+++ b/src/panels/config/automation/ha-automation-picker.ts
@@ -424,9 +424,11 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
${this.hass.localize("ui.panel.config.labels.add_label")}
`;
+
const labelsInOverflow =
(this._sizeController.value && this._sizeController.value < 700) ||
(!this._sizeController.value && this.hass.dockedSidebar === "docked");
+
const automations = this._automations(
this.automations,
this._entityReg,
diff --git a/src/panels/config/devices/ha-config-devices-dashboard.ts b/src/panels/config/devices/ha-config-devices-dashboard.ts
index 0e5c0eff43..5cfc421492 100644
--- a/src/panels/config/devices/ha-config-devices-dashboard.ts
+++ b/src/panels/config/devices/ha-config-devices-dashboard.ts
@@ -1,6 +1,12 @@
import { consume } from "@lit-labs/context";
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
-import { mdiChevronRight, mdiMenuDown, mdiPlus } from "@mdi/js";
+import {
+ mdiChevronRight,
+ mdiDotsVertical,
+ mdiMenuDown,
+ mdiPlus,
+ mdiTextureBox,
+} from "@mdi/js";
import {
CSSResultGroup,
LitElement,
@@ -10,10 +16,12 @@ import {
nothing,
} from "lit";
+import { ResizeController } from "@lit-labs/observers/resize-controller";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { computeCssColor } from "../../../common/color/compute-color";
+import { storage } from "../../../common/decorators/storage";
import { HASSDomEvent } from "../../../common/dom/fire_event";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import {
@@ -22,6 +30,10 @@ import {
} from "../../../common/integrations/protocolIntegrationPicked";
import { navigate } from "../../../common/navigate";
import { LocalizeFunc } from "../../../common/translations/localize";
+import {
+ hasRejectedItems,
+ rejectedItems,
+} from "../../../common/util/promise-all-settled-results";
import {
DataTableColumnContainer,
RowClickedEvent,
@@ -42,6 +54,7 @@ import "../../../components/ha-filter-states";
import "../../../components/ha-icon-button";
import "../../../components/ha-menu-item";
import "../../../components/ha-sub-menu";
+import { createAreaRegistryEntry } from "../../../data/area_registry";
import { ConfigEntry, sortConfigEntries } from "../../../data/config_entries";
import { fullEntitiesContext } from "../../../data/context";
import {
@@ -66,16 +79,12 @@ import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route } from "../../../types";
import { brandsUrl } from "../../../util/brands-url";
+import { showAreaRegistryDetailDialog } from "../areas/show-dialog-area-registry-detail";
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";
-import { storage } from "../../../common/decorators/storage";
+import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
interface DeviceRowData extends DeviceRegistryEntry {
device?: DeviceRowData;
@@ -125,6 +134,10 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
@storage({ key: "devices-table-grouping", state: false, subscribe: false })
private _activeGrouping?: string;
+ private _sizeController = new ResizeController(this, {
+ callback: (entries) => entries[0]?.contentRect.width,
+ });
+
private _ignoreLocationChange = false;
public connectedCallback() {
@@ -557,6 +570,41 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
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`
+ ${area.icon
+ ? html``
+ : html``}
+ ${area.name}
+ `
+ )}
+
+
+ ${this.hass.localize(
+ "ui.panel.config.devices.picker.bulk_actions.no_area"
+ )}
+
+
+
+
+
+ ${this.hass.localize(
+ "ui.panel.config.devices.picker.bulk_actions.add_area"
+ )}
+
+ `;
+
const labelItems = html`${this._labels?.map((label) => {
const color = label.color ? computeCssColor(label.color) : undefined;
const selected = this._selected.every((deviceId) =>
@@ -696,36 +744,77 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
${!this.narrow
? html`
-
-
-
- ${labelItems}
- `
- : html`
-
-
+
+
+
+ ${labelItems}
+
+
+ ${areasInOverflow
+ ? nothing
+ : html`
+
+
+
+ ${areaItems}
+ `}`
+ : nothing}
+ ${this.narrow || areasInOverflow
+ ? html`
+ ${this.narrow
+ ? html`
+
+ `
+ : html``}
+ ${this.narrow
+ ? html`
+
+
+ ${this.hass.localize(
+ "ui.panel.config.automation.picker.bulk_actions.add_label"
+ )}
+
+
+
+ ${labelItems}
+ `
+ : nothing}
${this.hass.localize(
- "ui.panel.config.automation.picker.bulk_actions.add_label"
+ "ui.panel.config.devices.picker.bulk_actions.move_area"
)}
- ${labelItems}
+ ${areaItems}
- `}
+ `
+ : nothing}
`;
}
@@ -821,6 +911,46 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
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[] = [];
+ 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`
+${rejected
+ .map((r) => r.reason.message || r.reason.code || r.reason)
+ .join("\r\n")}
`,
+ });
+ }
+ }
+
+ 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) {
const label = ev.currentTarget.value;
const action = ev.currentTarget.action;
@@ -878,6 +1008,9 @@ ${rejected
static get styles(): CSSResultGroup {
return [
css`
+ :host {
+ display: block;
+ }
hass-tabs-subpage-data-table {
--data-table-row-height: 60px;
}
diff --git a/src/translations/en.json b/src/translations/en.json
index 100b5536e5..c360ded244 100644
--- a/src/translations/en.json
+++ b/src/translations/en.json
@@ -4026,7 +4026,12 @@
"confirm_delete_integration": "Are you sure you want to remove this device from {integration}?",
"picker": {
"search": "Search {number} devices",
- "state": "State"
+ "state": "State",
+ "bulk_actions": {
+ "move_area": "Move to area",
+ "no_area": "No area",
+ "add_area": "Add area"
+ }
}
},
"entities": {