From 85769486dfe9d9d545738c19d07d3b1eedef1661 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Tue, 12 May 2026 10:29:52 -0400 Subject: [PATCH] fix(ui/templates): expand composition leaves to show cascaded slots MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../Components/Pages/Design/Templates.razor | 48 +++++++++++++------ 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/src/ScadaLink.CentralUI/Components/Pages/Design/Templates.razor b/src/ScadaLink.CentralUI/Components/Pages/Design/Templates.razor index 0c6a4e1..6b794af 100644 --- a/src/ScadaLink.CentralUI/Components/Pages/Design/Templates.razor +++ b/src/ScadaLink.CentralUI/Components/Pages/Design/Templates.razor @@ -81,7 +81,7 @@
@@ -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())) - .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 BuildCompositionLeaves(Template owner, IReadOnlyDictionary templatesById) + { + var result = new List(); + 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(); + + 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 children) { children.Sort((a, b) =>