Group area per floor in the editor

This commit is contained in:
Paul Bottein 2025-06-24 14:23:13 +02:00
parent 976bf7c512
commit b890ef2b01
No known key found for this signature in database
5 changed files with 230 additions and 6 deletions

View File

@ -0,0 +1,213 @@
import { mdiTextureBox } from "@mdi/js";
import type { TemplateResult } from "lit";
import { LitElement, css, html } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import { computeFloorName } from "../common/entity/compute_floor_name";
import { getAreaContext } from "../common/entity/context/get_area_context";
import { stringCompare } from "../common/string/compare";
import { areaCompare } from "../data/area_registry";
import type { FloorRegistryEntry } from "../data/floor_registry";
import type { HomeAssistant } from "../types";
import "./ha-expansion-panel";
import "./ha-floor-icon";
import "./ha-items-display-editor";
import type { DisplayItem, DisplayValue } from "./ha-items-display-editor";
import "./ha-svg-icon";
import "./ha-textfield";
export interface AreasDisplayValue {
hidden?: string[];
order?: string[];
}
const UNASSIGNED_FLOOR = "__unassigned__";
@customElement("ha-areas-floors-display-editor")
export class HaAreasFloorsDisplayEditor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public label?: string;
@property({ attribute: false }) public value?: AreasDisplayValue;
@property() public helper?: string;
@property({ type: Boolean }) public expanded = false;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = false;
@property({ type: Boolean, attribute: "show-navigation-button" })
public showNavigationButton = false;
protected render(): TemplateResult {
const compare = areaCompare(this.hass.areas);
const areas = Object.values(this.hass.areas).sort((areaA, areaB) =>
compare(areaA.area_id, areaB.area_id)
);
const groupedItems: Record<string, DisplayItem[]> = areas.reduce(
(acc, area) => {
const { floor } = getAreaContext(area, this.hass!);
const floorId = floor?.floor_id ?? UNASSIGNED_FLOOR;
if (!acc[floorId]) {
acc[floorId] = [];
}
acc[floorId].push({
value: area.area_id,
label: area.name,
icon: area.icon ?? undefined,
iconPath: mdiTextureBox,
description: floor?.name,
});
return acc;
},
{} as Record<string, DisplayItem[]>
);
const filteredFloors = this._sortedFloors(this.hass.floors).filter(
(floor) =>
// Only include floors that have areas assigned to them
groupedItems[floor.floor_id]?.length > 0
);
const value: DisplayValue = {
order: this.value?.order ?? [],
hidden: this.value?.hidden ?? [],
};
return html`
<ha-expansion-panel
outlined
.header=${this.label}
.expanded=${this.expanded}
>
<ha-svg-icon slot="leading-icon" .path=${mdiTextureBox}></ha-svg-icon>
${filteredFloors.map(
(floor) => html`
<div class="floor">
<div class="header">
<ha-floor-icon .floor=${floor}></ha-floor-icon>
<p>${computeFloorName(floor)}</p>
</div>
<div class="areas">
<ha-items-display-editor
.hass=${this.hass}
.items=${groupedItems[floor.floor_id] || []}
.value=${value}
.floorId=${floor.floor_id}
@value-changed=${this._areaDisplayChanged}
.showNavigationButton=${this.showNavigationButton}
></ha-items-display-editor>
</div>
</div>
`
)}
</ha-expansion-panel>
`;
}
private _sortedFloors = memoizeOne(
(hassFloors: HomeAssistant["floors"]): FloorRegistryEntry[] => {
const floors = Object.values(hassFloors).sort((floorA, floorB) => {
if (floorA.level !== floorB.level) {
return (floorA.level ?? 0) - (floorB.level ?? 0);
}
return stringCompare(floorA.name, floorB.name);
});
floors.push({
floor_id: UNASSIGNED_FLOOR,
name: this.hass.localize(
"ui.panel.lovelace.strategy.areas.unassigned_areas"
),
icon: null,
level: 999999,
aliases: [],
created_at: 0,
modified_at: 0,
});
return floors;
}
);
private async _areaDisplayChanged(ev) {
ev.stopPropagation();
const value = ev.detail.value as DisplayValue;
const currentFloorId = ev.currentTarget.floorId;
const floorIds = this._sortedFloors(this.hass.floors).map(
(floor) => floor.floor_id
);
const newHidden: string[] = [];
const newOrder: string[] = [];
for (const floorId of floorIds) {
if (currentFloorId === floorId) {
newHidden.push(...(value.hidden ?? []));
newOrder.push(...(value.order ?? []));
continue;
}
const hidden = this.value?.hidden?.filter(
(areaId) => this.hass.areas[areaId]?.floor_id === floorId
);
if (hidden) {
newHidden.push(...hidden);
}
const order = this.value?.order?.filter(
(areaId) => this.hass.areas[areaId]?.floor_id === floorId
);
if (order) {
newOrder.push(...order);
}
}
const newValue: AreasDisplayValue = {
hidden: newHidden,
order: newOrder,
};
if (newValue.hidden?.length === 0) {
delete newValue.hidden;
}
if (newValue.order?.length === 0) {
delete newValue.order;
}
fireEvent(this, "value-changed", { value: newValue });
}
static get styles() {
return [
css`
.floor .header p {
margin: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
flex: 1:
}
.floor .header {
margin: 16px 0 8px 0;
padding: 0 8px;
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-areas-floors-display-editor": HaAreasFloorsDisplayEditor;
}
}

View File

@ -202,7 +202,7 @@ export class HaNumericStateTrigger extends LitElement {
entity: { domain: ["input_number", "number", "sensor"] },
},
},
] as const)
] as const satisfies HaFormSchema[])
: ([
{
name: "below",

View File

@ -11,6 +11,8 @@ import type { AreaCardConfig, HeadingCardConfig } from "../../cards/types";
import type { EntitiesDisplay } from "./area-view-strategy";
import { computeAreaPath, getAreas } from "./helpers/areas-strategy-helper";
const UNASSIGNED_FLOOR = "__unassigned__";
interface AreaOptions {
groups_options?: Record<string, EntitiesDisplay>;
}
@ -46,13 +48,20 @@ export class AreasOverviewViewStrategy extends ReactiveElement {
const floorSections = [
...floors,
{ floor_id: "default", name: "Default", level: null, icon: null },
{
floor_id: UNASSIGNED_FLOOR,
name: hass.localize(
"ui.panel.lovelace.strategy.areas.unassigned_areas"
),
level: null,
icon: null,
},
]
.map<LovelaceSectionConfig | undefined>((floor) => {
const areasInFloors = areas.filter(
(area) =>
area.floor_id === floor.floor_id ||
(!area.floor_id && floor.floor_id === "default")
(!area.floor_id && floor.floor_id === UNASSIGNED_FLOOR)
);
if (areasInFloors.length === 0) {

View File

@ -22,6 +22,7 @@ import {
type AreaRegistryEntry,
} from "../../../../../data/area_registry";
import { buttonLinkStyle } from "../../../../../resources/styles";
import "../../../../../components/ha-areas-floors-display-editor";
@customElement("hui-areas-dashboard-strategy-editor")
export class HuiAreasDashboardStrategyEditor
@ -122,7 +123,7 @@ export class HuiAreasDashboardStrategyEditor
const value = this._config.areas_display;
return html`
<ha-areas-display-editor
<ha-areas-floors-display-editor
.hass=${this.hass}
.value=${value}
.label=${this.hass.localize(
@ -132,7 +133,7 @@ export class HuiAreasDashboardStrategyEditor
expanded
show-navigation-button
@item-display-navigate-clicked=${this._handleAreaNavigate}
></ha-areas-display-editor>
></ha-areas-floors-display-editor>
`;
}

View File

@ -6618,7 +6618,8 @@
"security": "Security",
"actions": "Actions",
"others": "Others"
}
},
"unassigned_areas": "[%key:ui::panel::config::areas::picker::unassigned_areas%]"
}
},
"cards": {