fix(ui/templates): expand composition leaves to show cascaded slots

Composition leaves were rendered flat — the cascaded inner derived
templates existed in the DB but the tree only showed the outer slot
name (e.g. "Tank Monitor > DrivePump") with no way to see DrivePump's
own TempSensor + AlarmSensor slots.

BuildCompositionLeaves now recurses: for each composition under a
template, look up the composed template (which after derive-on-compose
is a derived row carrying its own Compositions) and build its slot
leaves as children. HasChildrenSelector loses the
"not a composition" guard so nested leaves render with the expand
chevron.
This commit is contained in:
Joseph Doherty
2026-05-12 10:29:52 -04:00
parent 4f90f952d0
commit 85769486df

View File

@@ -81,7 +81,7 @@
<div style="max-height: calc(100vh - 160px); overflow-y: auto; padding: 4px;">
<TreeView @ref="_tree" TItem="TmplNode" Items="_treeRoots"
ChildrenSelector="n => n.Children"
HasChildrenSelector="n => n.Kind != TmplNodeKind.Composition && n.Children.Count > 0"
HasChildrenSelector="n => n.Children.Count > 0"
KeySelector="n => (object)n.Key"
StorageKey="templates-tree">
<NodeContent Context="node">
@@ -178,22 +178,13 @@
// 3. Template nodes with composition leaves. Derived templates are
// slot-owned and reached via their parent's composition leaf — never
// shown as standalone tree nodes.
// shown as standalone tree nodes. Composition leaves recurse so a
// composite slot (e.g. Pump composed with TempSensor) reveals its own
// child slots when expanded.
var templatesById = _templates.ToDictionary(t => t.Id);
foreach (var t in _templates.Where(t => !t.IsDerived).OrderBy(t => t.Name, StringComparer.OrdinalIgnoreCase))
{
var compChildren = t.Compositions
.OrderBy(c => c.InstanceName, StringComparer.OrdinalIgnoreCase)
.Select(c => new TmplNode(
Key: $"c:{c.Id}",
Kind: TmplNodeKind.Composition,
EntityId: c.Id,
Label: c.InstanceName,
ParentFolderId: null,
OwnerTemplateId: t.Id,
Template: null,
Composition: c,
Children: new List<TmplNode>()))
.ToList();
var compChildren = BuildCompositionLeaves(t, templatesById);
var tNode = new TmplNode(
Key: $"t:{t.Id}",
@@ -220,6 +211,33 @@
_treeRoots = roots;
}
// Recursive: each composition leaf's children are the composed-template's
// own composition leaves. Cascaded derived templates carry their slot
// compositions, so walking ComposedTemplateId surfaces the full nested
// structure.
private static List<TmplNode> BuildCompositionLeaves(Template owner, IReadOnlyDictionary<int, Template> templatesById)
{
var result = new List<TmplNode>();
foreach (var c in owner.Compositions.OrderBy(c => c.InstanceName, StringComparer.OrdinalIgnoreCase))
{
var nestedChildren = templatesById.TryGetValue(c.ComposedTemplateId, out var composed)
? BuildCompositionLeaves(composed, templatesById)
: new List<TmplNode>();
result.Add(new TmplNode(
Key: $"c:{c.Id}",
Kind: TmplNodeKind.Composition,
EntityId: c.Id,
Label: c.InstanceName,
ParentFolderId: null,
OwnerTemplateId: owner.Id,
Template: null,
Composition: c,
Children: nestedChildren));
}
return result;
}
private static void SortChildren(List<TmplNode> children)
{
children.Sort((a, b) =>