Compare commits

...

6 Commits

Author SHA1 Message Date
Aidan Timson cb84e3d608 Add counts 2026-05-21 13:40:29 +01:00
Aidan Timson bbdde15a5c Add support for helpers 2026-05-21 13:25:41 +01:00
Aidan Timson 0b025293be hint for the rest 2026-05-21 11:41:17 +01:00
Aidan Timson 1772dbf421 Add query param support for areas 2026-05-21 11:39:46 +01:00
Aidan Timson 1f4dee984e Typing 2026-05-21 10:29:11 +01:00
Aidan Timson 74e312f529 Add quick links to area page 2026-05-21 10:24:18 +01:00
9 changed files with 221 additions and 18 deletions
+4 -1
View File
@@ -5,7 +5,10 @@ export interface DataTableFilter {
export type DataTableFilters = Record<string, DataTableFilter>;
export type DataTableFiltersValue = string[] | { key: string[] } | undefined;
export type DataTableFiltersValue =
| string[]
| Record<"key" | string, string[]>
| undefined;
export type DataTableFiltersValues = Record<string, DataTableFiltersValue>;
+132 -10
View File
@@ -1,5 +1,16 @@
import { consume } from "@lit/context";
import { mdiDelete, mdiDotsVertical, mdiImagePlus, mdiPencil } from "@mdi/js";
import {
mdiDelete,
mdiDevices,
mdiDotsVertical,
mdiImagePlus,
mdiPalette,
mdiPencil,
mdiRobot,
mdiScriptText,
mdiShape,
mdiTools,
} from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket/dist/types";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
@@ -10,7 +21,7 @@ import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { computeDeviceNameDisplay } from "../../../common/entity/compute_device_name";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { goBack } from "../../../common/navigate";
import { goBack, navigate } from "../../../common/navigate";
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
import { slugify } from "../../../common/string/slugify";
import { groupBy } from "../../../common/util/group-by";
@@ -18,6 +29,7 @@ import { afterNextRender } from "../../../common/util/render-status";
import "../../../components/ha-button";
import "../../../components/ha-card";
import "../../../components/ha-dropdown";
import type { HASSDomCurrentTargetEvent } from "../../../common/dom/fire_event";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import "../../../components/ha-icon-button";
@@ -48,6 +60,7 @@ import "../../../layouts/hass-error-screen";
import "../../../layouts/hass-subpage";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import { isHelperDomain } from "../helpers/const";
import "../../logbook/ha-logbook";
import {
loadAreaRegistryDetailDialog,
@@ -59,6 +72,58 @@ declare interface NameAndEntity<EntityType extends HassEntity> {
entity: EntityType;
}
type AreaQuickLinkKey =
| "devices"
| "entities"
| "helpers"
| "automations"
| "scenes"
| "scripts";
const NAVIGATION_ACTIONS: {
value: string;
path: string;
icon: string;
countKey: AreaQuickLinkKey;
}[] = [
{
value: "navigate-devices",
path: "/config/devices/dashboard",
icon: mdiDevices,
countKey: "devices",
},
{
value: "navigate-entities",
path: "/config/entities",
icon: mdiShape,
countKey: "entities",
},
{
value: "navigate-helpers",
path: "/config/helpers",
icon: mdiTools,
countKey: "helpers",
},
{
value: "navigate-automations",
path: "/config/automation/dashboard",
icon: mdiRobot,
countKey: "automations",
},
{
value: "navigate-scenes",
path: "/config/scene/dashboard",
icon: mdiPalette,
countKey: "scenes",
},
{
value: "navigate-scripts",
path: "/config/script/dashboard",
icon: mdiScriptText,
countKey: "scripts",
},
] as const;
@customElement("ha-config-area-page")
class HaConfigAreaPage extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -128,6 +193,31 @@ class HaConfigAreaPage extends LitElement {
.concat(memberships.indirectEntities.map((entry) => entry.entity_id))
);
private _getQuickLinkCounts = memoizeOne(
(
memberships: {
devices: DeviceRegistryEntry[];
entities: EntityRegistryEntry[];
indirectEntities: EntityRegistryEntry[];
},
related?: RelatedResult
) => {
const allEntityIds = this._allEntities(memberships);
const entityIds = related?.entity ?? allEntityIds;
return {
devices: related?.device?.length ?? memberships.devices.length,
entities: entityIds.length,
helpers: entityIds.filter((entityId) =>
isHelperDomain(computeDomain(entityId))
).length,
automations: related?.automation?.length ?? 0,
scenes: related?.scene?.length ?? 0,
scripts: related?.script?.length ?? 0,
};
}
);
protected firstUpdated(changedProps: PropertyValues<this>) {
super.firstUpdated(changedProps);
loadAreaRegistryDetailDialog();
@@ -162,6 +252,10 @@ class HaConfigAreaPage extends LitElement {
this._entityReg
);
const { devices, entities } = memberships;
const quickLinkCounts = this._getQuickLinkCounts(
memberships,
this._related
);
// Pre-compute the entity and device names, so we can sort by them
if (devices) {
@@ -238,6 +332,21 @@ class HaConfigAreaPage extends LitElement {
.path=${mdiDotsVertical}
></ha-icon-button>
${NAVIGATION_ACTIONS.map(
(action) => html`
<ha-dropdown-item value=${action.value}>
<ha-svg-icon slot="icon" .path=${action.icon}></ha-svg-icon>
${this.hass.localize(
`ui.panel.config.areas.quick_links.${action.countKey}`,
{ count: quickLinkCounts[action.countKey] }
)}
<ha-icon-next slot="details"></ha-icon-next>
</ha-dropdown-item>
`
)}
<wa-divider></wa-divider>
<ha-dropdown-item value="edit" .data=${area}>
<ha-svg-icon slot="icon" .path=${mdiPencil}> </ha-svg-icon>
${this.hass.localize("ui.panel.config.areas.edit_settings")}
@@ -609,9 +718,18 @@ class HaConfigAreaPage extends LitElement {
this._related = await findRelated(this.hass, "area", this.areaId);
}
private _handleMenuAction(ev: HaDropdownSelectEvent) {
private _handleMenuAction(
ev: HaDropdownSelectEvent<string, AreaRegistryEntry>
) {
const action = ev.detail?.item?.value;
const entry = (ev.detail?.item as any)?.data as AreaRegistryEntry;
const entry = ev.detail?.item?.data;
const navAction = NAVIGATION_ACTIONS.find((a) => a.value === action);
if (navAction) {
navigate(`${navAction.path}?historyBack=1&area=${this.areaId}`);
return;
}
switch (action) {
case "edit":
this._openDialog(entry);
@@ -622,15 +740,19 @@ class HaConfigAreaPage extends LitElement {
}
}
private _showSettings(ev: MouseEvent) {
const entry: AreaRegistryEntry = (ev.currentTarget! as any).entry;
this._openDialog(entry);
private _showSettings(
ev: HASSDomCurrentTargetEvent<
HTMLButtonElement & { entry: AreaRegistryEntry }
>
) {
this._openDialog(ev.currentTarget.entry);
}
private _openEntity(ev) {
const entry: EntityRegistryEntry = (ev.currentTarget as any).entity;
private _openEntity(
ev: HASSDomCurrentTargetEvent<HTMLElement & { entity: EntityRegistryEntry }>
) {
showMoreInfoDialog(this, {
entityId: entry.entity_id,
entityId: ev.currentTarget.entity.entity_id,
});
}
@@ -768,10 +768,15 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
super.willUpdate(changedProps);
if (!this.hasUpdated) {
const hasUrlFilter =
this._searchParms.has("blueprint") || this._searchParms.has("label");
this._searchParms.has("area") ||
this._searchParms.has("blueprint") ||
this._searchParms.has("label");
if (!hasUrlFilter) {
this._filters = this._storageFilters;
}
if (this._searchParms.has("area")) {
this._filterArea();
}
if (this._searchParms.has("blueprint")) {
this._filterBlueprint();
}
@@ -871,6 +876,22 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
this._filteredEntityIds = filteredEntityIds;
}
private _filterArea() {
const area = this._searchParms.get("area");
if (!area) {
return;
}
this._fromUrl = true;
this._filters = {
...this._filters,
"ha-filter-floor-areas": {
value: { areas: [area] },
items: undefined,
},
};
this._applyFilters();
}
private _filterLabel() {
const label = this._searchParms.get("label");
if (!label) {
@@ -249,12 +249,13 @@ export class HaConfigDeviceDashboard extends LitElement {
}
private _setFiltersFromUrl() {
const area = this._searchParms.get("area");
const domain = this._searchParms.get("domain");
const configEntry = this._searchParms.get("config_entry");
const subEntry = this._searchParms.get("sub_entry");
const label = this._searchParms.has("label");
if (!domain && !configEntry && !label) {
if (!area && !domain && !configEntry && !label) {
return;
}
@@ -269,6 +270,10 @@ export class HaConfigDeviceDashboard extends LitElement {
],
items: undefined,
},
"ha-filter-floor-areas": {
value: area ? { areas: [area] } : undefined,
items: undefined,
},
"ha-filter-integrations": {
value: domain ? [domain] : [],
items: undefined,
@@ -1092,6 +1092,7 @@ export class HaConfigEntities extends LitElement {
}
private _setFiltersFromUrl() {
const area = this._searchParms.get("area");
const domain = this._searchParms.get("domain");
const configEntry = this._searchParms.get("config_entry");
const subEntry = this._searchParms.get("sub_entry");
@@ -1099,7 +1100,7 @@ export class HaConfigEntities extends LitElement {
const label = this._searchParms.get("label");
const voiceAssistant = this._searchParms.get("voice_assistant");
if (!domain && !configEntry && !label && !device) {
if (!area && !domain && !configEntry && !label && !device) {
return;
}
@@ -1108,6 +1109,7 @@ export class HaConfigEntities extends LitElement {
this._filters = {
"ha-filter-states": [],
"ha-filter-floor-areas": area ? { areas: [area] } : undefined,
"ha-filter-integrations": domain ? [domain] : [],
"ha-filter-devices": device ? [device] : [],
"ha-filter-labels": label ? [label] : [],
@@ -625,7 +625,9 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
<hass-tabs-subpage-data-table
.hass=${this.hass}
.narrow=${this.narrow}
back-path="/config"
.backPath=${this._searchParms.has("historyBack")
? undefined
: "/config"}
.route=${this.route}
.tabs=${configSections.devices}
.searchLabel=${this.hass.localize(
@@ -964,12 +966,13 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
};
private _setFiltersFromUrl() {
const area = this._searchParms.get("area");
const device = this._searchParms.get("device");
const label = this._searchParms.get("label");
const category = this._searchParms.get("category");
const voiceAssistant = this._searchParms.get("voice_assistant");
if (!category && !label && !device) {
if (!area && !category && !label && !device && !voiceAssistant) {
return;
}
@@ -977,6 +980,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
this._filter = history.state?.filter || "";
this._filters = {
"ha-filter-floor-areas": area ? { areas: [area] } : undefined,
"ha-filter-devices": device ? [device] : [],
"ha-filter-labels": label ? [label] : [],
"ha-filter-categories": category ? [category] : [],
+18 -1
View File
@@ -410,9 +410,10 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
protected willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
if (!this.hasUpdated) {
if (!this._searchParms.has("label")) {
if (!this._searchParms.has("area") && !this._searchParms.has("label")) {
this._filters = this._storageFilters;
}
this._filterArea();
this._filterLabel();
}
}
@@ -785,6 +786,22 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
this._applyFilters();
}
private _filterArea() {
const area = this._searchParms.get("area");
if (!area) {
return;
}
this._fromUrl = true;
this._filters = {
...this._filters,
"ha-filter-floor-areas": {
value: { areas: [area] },
items: undefined,
},
};
this._applyFilters();
}
private _filterLabel() {
const label = this._searchParms.get("label");
if (!label) {
+22 -1
View File
@@ -783,10 +783,15 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
super.willUpdate(changedProps);
if (!this.hasUpdated) {
const hasUrlFilter =
this._searchParms.has("blueprint") || this._searchParms.has("label");
this._searchParms.has("area") ||
this._searchParms.has("blueprint") ||
this._searchParms.has("label");
if (!hasUrlFilter) {
this._filters = this._storageFilters;
}
if (this._searchParms.has("area")) {
this._filterArea();
}
if (this._searchParms.has("blueprint")) {
this._filterBlueprint();
}
@@ -803,6 +808,22 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
}
}
private _filterArea() {
const area = this._searchParms.get("area");
if (!area) {
return;
}
this._fromUrl = true;
this._filters = {
...this._filters,
"ha-filter-floor-areas": {
value: { areas: [area] },
items: undefined,
},
};
this._applyFilters();
}
private _filterLabel() {
const label = this._searchParms.get("label");
if (!label) {
+8
View File
@@ -3126,6 +3126,14 @@
"caption": "Areas",
"description": "Group devices and entities into areas",
"edit_settings": "Area settings",
"quick_links": {
"devices": "{count} {count, plural,\n one {device}\n other {devices}\n}",
"entities": "{count} {count, plural,\n one {entity}\n other {entities}\n}",
"helpers": "{count} {count, plural,\n one {helper}\n other {helpers}\n}",
"automations": "{count} {count, plural,\n one {automation}\n other {automations}\n}",
"scenes": "{count} {count, plural,\n one {scene}\n other {scenes}\n}",
"scripts": "{count} {count, plural,\n one {script}\n other {scripts}\n}"
},
"add_picture": "Add a picture",
"assigned_to_area": "Assigned to this area",
"targeting_area": "Targeting this area",