From 0fbd4305944924157eee02a11865401367fee81c Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 30 Jun 2025 18:09:42 +0200 Subject: [PATCH] Allow to re-order floors in areas dashboard (#26002) * Allow to re-order floors in areas dashboard * Move drag handle to right * Improve typings * Only show drag handle if there is at least 2 floors --- .../ha-areas-floors-display-editor.ts | 208 +++++++++++------- src/components/ha-items-display-editor.ts | 38 ++-- .../areas/areas-dashboard-strategy.ts | 4 + .../areas/areas-overview-view-strategy.ts | 20 +- .../hui-areas-dashboard-strategy-editor.ts | 19 +- .../areas/helpers/areas-strategy-helper.ts | 25 ++- src/translations/en.json | 2 +- 7 files changed, 199 insertions(+), 117 deletions(-) diff --git a/src/components/ha-areas-floors-display-editor.ts b/src/components/ha-areas-floors-display-editor.ts index cd56b94ffc..5b372f932f 100644 --- a/src/components/ha-areas-floors-display-editor.ts +++ b/src/components/ha-areas-floors-display-editor.ts @@ -1,14 +1,15 @@ -import { mdiTextureBox } from "@mdi/js"; +import { mdiDrag, mdiTextureBox } from "@mdi/js"; import type { TemplateResult } from "lit"; import { LitElement, css, html, nothing } from "lit"; import { customElement, property } from "lit/decorators"; +import { repeat } from "lit/directives/repeat"; 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 { getFloors } from "../panels/lovelace/strategies/areas/helpers/areas-strategy-helper"; import type { HomeAssistant } from "../types"; import "./ha-expansion-panel"; import "./ha-floor-icon"; @@ -17,9 +18,14 @@ import type { DisplayItem, DisplayValue } from "./ha-items-display-editor"; import "./ha-svg-icon"; import "./ha-textfield"; -export interface AreasDisplayValue { - hidden?: string[]; - order?: string[]; +export interface AreasFloorsDisplayValue { + areas_display?: { + hidden?: string[]; + order?: string[]; + }; + floors_display?: { + order?: string[]; + }; } const UNASSIGNED_FLOOR = "__unassigned__"; @@ -30,12 +36,10 @@ export class HaAreasFloorsDisplayEditor extends LitElement { @property() public label?: string; - @property({ attribute: false }) public value?: AreasDisplayValue; + @property({ attribute: false }) public value?: AreasFloorsDisplayValue; @property() public helper?: string; - @property({ type: Boolean }) public expanded = false; - @property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public required = false; @@ -44,55 +48,78 @@ export class HaAreasFloorsDisplayEditor extends LitElement { public showNavigationButton = false; protected render(): TemplateResult { - const groupedItems = this._groupedItems(this.hass.areas, this.hass.floors); + const groupedAreasItems = this._groupedAreasItems( + this.hass.areas, + this.hass.floors + ); - const filteredFloors = this._sortedFloors(this.hass.floors).filter( + const filteredFloors = this._sortedFloors( + this.hass.floors, + this.value?.floors_display?.order + ).filter( (floor) => // Only include floors that have areas assigned to them - groupedItems[floor.floor_id]?.length > 0 + groupedAreasItems[floor.floor_id]?.length > 0 ); const value: DisplayValue = { - order: this.value?.order ?? [], - hidden: this.value?.hidden ?? [], + order: this.value?.areas_display?.order ?? [], + hidden: this.value?.areas_display?.hidden ?? [], }; + const canReorderFloors = + filteredFloors.filter((floor) => floor.floor_id !== UNASSIGNED_FLOOR) + .length > 1; + return html` - ${this.label}` : nothing} + - - ${filteredFloors.map((floor, _, array) => { - const noFloors = - array.length === 1 && floor.floor_id === UNASSIGNED_FLOOR; - return html` -
- ${noFloors - ? nothing - : html`
- -

${computeFloorName(floor)}

-
`} -
+
+ ${repeat( + filteredFloors, + (floor) => floor.floor_id, + (floor: FloorRegistryEntry) => html` + + + ${floor.floor_id === UNASSIGNED_FLOOR || !canReorderFloors + ? nothing + : html` + + `} -
-
- `; - })} - + + ` + )} +
+
`; } - private _groupedItems = memoizeOne( + private _groupedAreasItems = memoizeOne( ( hassAreas: HomeAssistant["areas"], // update items if floors change @@ -116,7 +143,6 @@ export class HaAreasFloorsDisplayEditor extends LitElement { label: area.name, icon: area.icon ?? undefined, iconPath: mdiTextureBox, - description: floor?.name, }); return acc; @@ -128,18 +154,17 @@ export class HaAreasFloorsDisplayEditor extends LitElement { ); 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); - }); + ( + hassFloors: HomeAssistant["floors"], + order: string[] | undefined + ): FloorRegistryEntry[] => { + const floors = getFloors(hassFloors, order); + const noFloors = floors.length === 0; floors.push({ floor_id: UNASSIGNED_FLOOR, - name: this.hass.localize( - "ui.panel.lovelace.strategy.areas.others_areas" - ), + name: noFloors + ? this.hass.localize("ui.panel.lovelace.strategy.areas.areas") + : this.hass.localize("ui.panel.lovelace.strategy.areas.other_areas"), icon: null, level: null, aliases: [], @@ -150,17 +175,43 @@ export class HaAreasFloorsDisplayEditor extends LitElement { } ); - private async _areaDisplayChanged(ev) { + private _floorMoved(ev: CustomEvent) { ev.stopPropagation(); - const value = ev.detail.value as DisplayValue; - const currentFloorId = ev.currentTarget.floorId; + const newIndex = ev.detail.newIndex; + const oldIndex = ev.detail.oldIndex; + const floorIds = this._sortedFloors( + this.hass.floors, + this.value?.floors_display?.order + ).map((floor) => floor.floor_id); + const newOrder = [...floorIds]; + const movedFloorId = newOrder.splice(oldIndex, 1)[0]; + newOrder.splice(newIndex, 0, movedFloorId); + const newValue: AreasFloorsDisplayValue = { + areas_display: this.value?.areas_display, + floors_display: { + order: newOrder, + }, + }; + if (newValue.floors_display?.order?.length === 0) { + delete newValue.floors_display.order; + } + fireEvent(this, "value-changed", { value: newValue }); + } - const floorIds = this._sortedFloors(this.hass.floors).map( - (floor) => floor.floor_id - ); + private async _areaDisplayChanged(ev: CustomEvent<{ value: DisplayValue }>) { + ev.stopPropagation(); + const value = ev.detail.value; + const currentFloorId = (ev.currentTarget as any).floorId; - const oldHidden = this.value?.hidden ?? []; - const oldOrder = this.value?.order ?? []; + const floorIds = this._sortedFloors( + this.hass.floors, + this.value?.floors_display?.order + ).map((floor) => floor.floor_id); + + const oldAreaDisplay = this.value?.areas_display ?? {}; + + const oldHidden = oldAreaDisplay?.hidden ?? []; + const oldOrder = oldAreaDisplay?.order ?? []; const newHidden: string[] = []; const newOrder: string[] = []; @@ -187,37 +238,27 @@ export class HaAreasFloorsDisplayEditor extends LitElement { } } - const newValue: AreasDisplayValue = { - hidden: newHidden, - order: newOrder, + const newValue: AreasFloorsDisplayValue = { + areas_display: { + hidden: newHidden, + order: newOrder, + }, + floors_display: this.value?.floors_display, }; - if (newValue.hidden?.length === 0) { - delete newValue.hidden; + if (newValue.areas_display?.hidden?.length === 0) { + delete newValue.areas_display.hidden; } - if (newValue.order?.length === 0) { - delete newValue.order; + if (newValue.areas_display?.order?.length === 0) { + delete newValue.areas_display.order; } - this.value = newValue; + if (newValue.floors_display?.order?.length === 0) { + delete newValue.floors_display.order; + } + fireEvent(this, "value-changed", { value: newValue }); } static styles = 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; - } ha-expansion-panel { margin-bottom: 8px; --expansion-panel-summary-padding: 0 16px; @@ -225,6 +266,11 @@ export class HaAreasFloorsDisplayEditor extends LitElement { ha-expansion-panel [slot="leading-icon"] { margin-inline-end: 16px; } + label { + display: block; + font-weight: var(--ha-font-weight-bold); + margin-bottom: 8px; + } `; } diff --git a/src/components/ha-items-display-editor.ts b/src/components/ha-items-display-editor.ts index e87ecfaef0..91d55820bf 100644 --- a/src/components/ha-items-display-editor.ts +++ b/src/components/ha-items-display-editor.ts @@ -122,22 +122,6 @@ export class HaItemDisplayEditor extends LitElement { ${description ? html`${description}` : nothing} - ${isVisible && !disableSorting - ? html` - - ` - : html``} ${!showIcon ? nothing : icon @@ -162,6 +146,9 @@ export class HaItemDisplayEditor extends LitElement { ${this.actionsRenderer(item)} ` : nothing} + ${this.showNavigationButton + ? html`` + : nothing} - ${this.showNavigationButton - ? html` ` - : nothing} + ${isVisible && !disableSorting + ? html` + + ` + : html``} `; } diff --git a/src/panels/lovelace/strategies/areas/areas-dashboard-strategy.ts b/src/panels/lovelace/strategies/areas/areas-dashboard-strategy.ts index 9ec7bc9486..6154719ec6 100644 --- a/src/panels/lovelace/strategies/areas/areas-dashboard-strategy.ts +++ b/src/panels/lovelace/strategies/areas/areas-dashboard-strategy.ts @@ -22,6 +22,9 @@ export interface AreasDashboardStrategyConfig { hidden?: string[]; order?: string[]; }; + floors_display?: { + order?: string[]; + }; areas_options?: Record; } @@ -84,6 +87,7 @@ export class AreasDashboardStrategy extends ReactiveElement { type: "areas-overview", areas_display: config.areas_display, areas_options: config.areas_options, + floors_display: config.floors_display, } satisfies AreasViewStrategyConfig, }, ...areaViews, diff --git a/src/panels/lovelace/strategies/areas/areas-overview-view-strategy.ts b/src/panels/lovelace/strategies/areas/areas-overview-view-strategy.ts index 9c47f38ddd..9d2d75e2d2 100644 --- a/src/panels/lovelace/strategies/areas/areas-overview-view-strategy.ts +++ b/src/panels/lovelace/strategies/areas/areas-overview-view-strategy.ts @@ -1,6 +1,5 @@ import { ReactiveElement } from "lit"; import { customElement } from "lit/decorators"; -import { stringCompare } from "../../../../common/string/compare"; import { floorDefaultIcon } from "../../../../components/ha-floor-icon"; import type { LovelaceSectionConfig } from "../../../../data/lovelace/config/section"; import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view"; @@ -9,7 +8,11 @@ import { getAreaControlEntities } from "../../card-features/hui-area-controls-ca import { AREA_CONTROLS, type AreaControl } from "../../card-features/types"; import type { AreaCardConfig, HeadingCardConfig } from "../../cards/types"; import type { EntitiesDisplay } from "./area-view-strategy"; -import { computeAreaPath, getAreas } from "./helpers/areas-strategy-helper"; +import { + computeAreaPath, + getAreas, + getFloors, +} from "./helpers/areas-strategy-helper"; const UNASSIGNED_FLOOR = "__unassigned__"; @@ -23,6 +26,9 @@ export interface AreasViewStrategyConfig { hidden?: string[]; order?: string[]; }; + floors_display?: { + order?: string[]; + }; areas_options?: Record; } @@ -38,19 +44,13 @@ export class AreasOverviewViewStrategy extends ReactiveElement { config.areas_display?.order ); - const floors = Object.values(hass.floors); - floors.sort((floorA, floorB) => { - if (floorA.level !== floorB.level) { - return (floorA.level ?? 0) - (floorB.level ?? 0); - } - return stringCompare(floorA.name, floorB.name); - }); + const floors = getFloors(hass.floors, config.floors_display?.order); const floorSections = [ ...floors, { floor_id: UNASSIGNED_FLOOR, - name: hass.localize("ui.panel.lovelace.strategy.areas.others_areas"), + name: hass.localize("ui.panel.lovelace.strategy.areas.other_areas"), level: null, icon: null, }, diff --git a/src/panels/lovelace/strategies/areas/editor/hui-areas-dashboard-strategy-editor.ts b/src/panels/lovelace/strategies/areas/editor/hui-areas-dashboard-strategy-editor.ts index 73e7d890bb..96448afc83 100644 --- a/src/panels/lovelace/strategies/areas/editor/hui-areas-dashboard-strategy-editor.ts +++ b/src/panels/lovelace/strategies/areas/editor/hui-areas-dashboard-strategy-editor.ts @@ -1,10 +1,12 @@ import { mdiThermometerWater } from "@mdi/js"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; import { fireEvent } from "../../../../../common/dom/fire_event"; import "../../../../../components/ha-areas-display-editor"; import type { AreasDisplayValue } from "../../../../../components/ha-areas-display-editor"; import "../../../../../components/ha-areas-floors-display-editor"; +import type { AreasFloorsDisplayValue } from "../../../../../components/ha-areas-floors-display-editor"; import "../../../../../components/ha-entities-display-editor"; import "../../../../../components/ha-icon"; import "../../../../../components/ha-icon-button"; @@ -126,7 +128,7 @@ export class HuiAreasDashboardStrategyEditor `; } - const value = this._config.areas_display; + const value = this._areasFloorsDisplayValue(this._config); return html` ({ + areas_display: config.areas_display, + floors_display: config.floors_display, + }) + ); + private _editArea(ev: Event): void { ev.stopPropagation(); const area = (ev.currentTarget! as any).area as AreaRegistryEntry; @@ -163,11 +172,11 @@ export class HuiAreasDashboardStrategyEditor this._area = ev.detail.value; } - private _areasDisplayChanged(ev: CustomEvent): void { - const value = ev.detail.value as AreasDisplayValue; + private _areasFloorsDisplayChanged(ev: CustomEvent): void { + const value = ev.detail.value as AreasFloorsDisplayValue; const newConfig: AreasDashboardStrategyConfig = { ...this._config!, - areas_display: value, + ...value, }; fireEvent(this, "config-changed", { config: newConfig }); diff --git a/src/panels/lovelace/strategies/areas/helpers/areas-strategy-helper.ts b/src/panels/lovelace/strategies/areas/helpers/areas-strategy-helper.ts index cbebee032b..79582d02fa 100644 --- a/src/panels/lovelace/strategies/areas/helpers/areas-strategy-helper.ts +++ b/src/panels/lovelace/strategies/areas/helpers/areas-strategy-helper.ts @@ -3,9 +3,13 @@ import { computeStateName } from "../../../../../common/entity/compute_state_nam import type { EntityFilterFunc } from "../../../../../common/entity/entity_filter"; import { generateEntityFilter } from "../../../../../common/entity/entity_filter"; import { stripPrefixFromEntityName } from "../../../../../common/entity/strip_prefix_from_entity_name"; -import { orderCompare } from "../../../../../common/string/compare"; +import { + orderCompare, + stringCompare, +} from "../../../../../common/string/compare"; import type { AreaRegistryEntry } from "../../../../../data/area_registry"; import { areaCompare } from "../../../../../data/area_registry"; +import type { FloorRegistryEntry } from "../../../../../data/floor_registry"; import type { LovelaceCardConfig } from "../../../../../data/lovelace/config/card"; import type { HomeAssistant } from "../../../../../types"; import { supportsAlarmModesCardFeature } from "../../../card-features/hui-alarm-modes-card-feature"; @@ -290,4 +294,23 @@ export const getAreas = ( return sortedAreas; }; +export const getFloors = ( + entries: HomeAssistant["floors"], + floorsOrder?: string[] +): FloorRegistryEntry[] => { + const floors = Object.values(entries); + const compare = orderCompare(floorsOrder || []); + + return floors.sort((floorA, floorB) => { + const order = compare(floorA.floor_id, floorB.floor_id); + if (order !== 0) { + return order; + } + if (floorA.level !== floorB.level) { + return (floorA.level ?? 0) - (floorB.level ?? 0); + } + return stringCompare(floorA.name, floorB.name); + }); +}; + export const computeAreaPath = (areaId: string): string => `areas-${areaId}`; diff --git a/src/translations/en.json b/src/translations/en.json index 6b144bf04e..948e4113cd 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -6686,7 +6686,7 @@ "actions": "Actions", "others": "Others" }, - "others_areas": "Other areas", + "other_areas": "Other areas", "areas": "Areas" } },