Files
frontend/test/data/device_registry_effective_area.test.ts
70bb08b101 Child devices UI (#53619)
* Add child devices UI

Surface child devices throughout the config UI so they read as first-class
devices nested under their parent:

- Device page: a "Sub-devices" card lists a device's children, and a child's
  page shows a "Part of <parent>" link (hardware/model/config-entry are
  already inherited from the parent by the registry resolver).
- Integration page: children are nested and indented under their parent
  device in the config-entry and subentry device lists.
- Device picker: children are ordered and indented under their parent with a
  tree connector, mirroring the area/floor picker.
- Naming: the device picker's secondary label and search now include the
  parent device name for children, so they stay identifiable in flat views.

Follow-up to the child devices data layer (#53617).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Add child devices to demo and gallery mock data

A power strip parent with two outlet children in the demo device stubs and
the ha-selector gallery demo, so child device rendering (nesting, tree
indentation, parent-context naming, inherited area) can be exercised without
a running backend.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Refine child devices UI and target resolution

Follow-up polish so child devices behave like a normal device everywhere:

- Targets & filters: a parent device qualifies for and resolves to its
  children's entities (getDevices filter, deviceMeetsFilter,
  deviceMeetsTargetSelector, resolveEntityIDs and the target-chip "split into
  entities" expand), matching core's server-side target resolution. Selecting a
  parent excludes its children from the picker.
- Pickers: keep a parent visible when a child matches the search (device and
  target pickers), fix the target picker's nested order (unsorted search +
  recomputed last-child flag), and render the sub-device tree in the target
  picker's device group.
- Area/naming: the area field shows only the (inherited) area again; the parent
  name remains a search term.
- Integration page: correct the tree end connector and align it in narrow mode.
- A parent-disabled child can no longer be enabled from the settings dialog.
- Devices dashboard: show "Part of <parent>" under a sub-device's name and add a
  hidden-by-default, searchable and groupable Parent device column that groups a
  parent together with its children.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Address review on child devices UI

Align target resolution with core and fix picker/table details:

- Only a directly targeted device expands to its child devices. Labels are
  never inherited by children (core's target helper is explicit about this) and
  areas resolve by effective-area membership, so deviceMeetsFilter and
  deviceMeetsTargetSelector evaluate a device's own entities again.
- Add devicesInEffectiveArea, mirroring core's dr.async_entries_for_area: an
  area contains its devices plus children that inherit the area, but not a child
  with a different explicit area. Used for area expansion, area matching and the
  area chip's split action.
- Device picker: add a searchFn that restores the nested parent/child order
  after the fuzzy search and recomputes the last visible child, so a child can
  no longer be ranked above its parent and connectors stay correct.
- Devices dashboard: derive the family group name from the family's parent
  device with the same fallback for parents and children, so an unnamed parent
  cannot end up in a different group than its children.
- Child devices card: pass the device registry so a child shows its inherited
  area.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Fix formatting in device picker row renderer

Reindent the row renderer template after it gained a block body, so Prettier
is satisfied.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Make the child device tree connector span the full row

The connector was a fixed 48px box centred in a taller two-line row, and its
SVG was letterboxed by the default preserveAspectRatio, so the dashed line
stopped short of the row edges and consecutive children never visually
connected.

Let the indicator stretch (preserveAspectRatio="none") and give it the full row
height, so the line runs edge to edge with the elbow at the vertical centre.
non-scaling-stroke keeps the line width and dash pattern identical however far
it is stretched; existing 48x48 usages render unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-13 17:45:31 +03:00

67 lines
1.9 KiB
TypeScript

import { assert, describe, it } from "vitest";
import { devicesInEffectiveArea } from "../../src/data/device/device_registry";
import type { DeviceRegistryEntry } from "../../src/data/device/device_registry";
const device = (
partial: Partial<DeviceRegistryEntry> & { id: string }
): DeviceRegistryEntry =>
({
area_id: null,
parent_device_id: null,
...partial,
}) as DeviceRegistryEntry;
describe("devicesInEffectiveArea", () => {
it("includes devices with the area set", () => {
const devices = {
a: device({ id: "a", area_id: "kitchen" }),
b: device({ id: "b", area_id: "bedroom" }),
};
assert.deepEqual(
devicesInEffectiveArea(devices, "kitchen").map((d) => d.id),
["a"]
);
});
it("includes a child device inheriting its parent's area", () => {
const devices = {
parent: device({ id: "parent", area_id: "kitchen" }),
child: device({ id: "child", parent_device_id: "parent" }),
};
assert.deepEqual(
devicesInEffectiveArea(devices, "kitchen")
.map((d) => d.id)
.sort(),
["child", "parent"]
);
});
it("excludes a child device with a different explicit area", () => {
const devices = {
parent: device({ id: "parent", area_id: "kitchen" }),
child: device({
id: "child",
area_id: "bedroom",
parent_device_id: "parent",
}),
};
assert.deepEqual(
devicesInEffectiveArea(devices, "kitchen").map((d) => d.id),
["parent"]
);
// ...and the child belongs to its own area instead.
assert.deepEqual(
devicesInEffectiveArea(devices, "bedroom").map((d) => d.id),
["child"]
);
});
it("excludes a child whose parent has no area", () => {
const devices = {
parent: device({ id: "parent" }),
child: device({ id: "child", parent_device_id: "parent" }),
};
assert.deepEqual(devicesInEffectiveArea(devices, "kitchen"), []);
});
});