mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-24 17:56:46 +00:00
Group area per floor in the editor
This commit is contained in:
parent
976bf7c512
commit
b890ef2b01
213
src/components/ha-areas-floors-display-editor.ts
Normal file
213
src/components/ha-areas-floors-display-editor.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -202,7 +202,7 @@ export class HaNumericStateTrigger extends LitElement {
|
|||||||
entity: { domain: ["input_number", "number", "sensor"] },
|
entity: { domain: ["input_number", "number", "sensor"] },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
] as const)
|
] as const satisfies HaFormSchema[])
|
||||||
: ([
|
: ([
|
||||||
{
|
{
|
||||||
name: "below",
|
name: "below",
|
||||||
|
@ -11,6 +11,8 @@ import type { AreaCardConfig, HeadingCardConfig } from "../../cards/types";
|
|||||||
import type { EntitiesDisplay } from "./area-view-strategy";
|
import type { EntitiesDisplay } from "./area-view-strategy";
|
||||||
import { computeAreaPath, getAreas } from "./helpers/areas-strategy-helper";
|
import { computeAreaPath, getAreas } from "./helpers/areas-strategy-helper";
|
||||||
|
|
||||||
|
const UNASSIGNED_FLOOR = "__unassigned__";
|
||||||
|
|
||||||
interface AreaOptions {
|
interface AreaOptions {
|
||||||
groups_options?: Record<string, EntitiesDisplay>;
|
groups_options?: Record<string, EntitiesDisplay>;
|
||||||
}
|
}
|
||||||
@ -46,13 +48,20 @@ export class AreasOverviewViewStrategy extends ReactiveElement {
|
|||||||
|
|
||||||
const floorSections = [
|
const floorSections = [
|
||||||
...floors,
|
...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) => {
|
.map<LovelaceSectionConfig | undefined>((floor) => {
|
||||||
const areasInFloors = areas.filter(
|
const areasInFloors = areas.filter(
|
||||||
(area) =>
|
(area) =>
|
||||||
area.floor_id === floor.floor_id ||
|
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) {
|
if (areasInFloors.length === 0) {
|
||||||
|
@ -22,6 +22,7 @@ import {
|
|||||||
type AreaRegistryEntry,
|
type AreaRegistryEntry,
|
||||||
} from "../../../../../data/area_registry";
|
} from "../../../../../data/area_registry";
|
||||||
import { buttonLinkStyle } from "../../../../../resources/styles";
|
import { buttonLinkStyle } from "../../../../../resources/styles";
|
||||||
|
import "../../../../../components/ha-areas-floors-display-editor";
|
||||||
|
|
||||||
@customElement("hui-areas-dashboard-strategy-editor")
|
@customElement("hui-areas-dashboard-strategy-editor")
|
||||||
export class HuiAreasDashboardStrategyEditor
|
export class HuiAreasDashboardStrategyEditor
|
||||||
@ -122,7 +123,7 @@ export class HuiAreasDashboardStrategyEditor
|
|||||||
const value = this._config.areas_display;
|
const value = this._config.areas_display;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-areas-display-editor
|
<ha-areas-floors-display-editor
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.value=${value}
|
.value=${value}
|
||||||
.label=${this.hass.localize(
|
.label=${this.hass.localize(
|
||||||
@ -132,7 +133,7 @@ export class HuiAreasDashboardStrategyEditor
|
|||||||
expanded
|
expanded
|
||||||
show-navigation-button
|
show-navigation-button
|
||||||
@item-display-navigate-clicked=${this._handleAreaNavigate}
|
@item-display-navigate-clicked=${this._handleAreaNavigate}
|
||||||
></ha-areas-display-editor>
|
></ha-areas-floors-display-editor>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6618,7 +6618,8 @@
|
|||||||
"security": "Security",
|
"security": "Security",
|
||||||
"actions": "Actions",
|
"actions": "Actions",
|
||||||
"others": "Others"
|
"others": "Others"
|
||||||
}
|
},
|
||||||
|
"unassigned_areas": "[%key:ui::panel::config::areas::picker::unassigned_areas%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"cards": {
|
"cards": {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user