From 06462a010062057e8a1e3f0dca76c1b4f0cce732 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Mon, 18 May 2026 17:50:30 -0400 Subject: [PATCH] feat(template-engine): contained names for composition-derived templates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A composition-derived template now stores its contained name — the composition slot's InstanceName (e.g. "Pump"), unique only within its owner — instead of the dotted global path ("Motor Controller.Pump"). The qualified hierarchical name is computed on read. - TemplateNaming.QualifiedName: walks the OwnerCompositionId chain to build the dotted path; null-safe, cycle-guarded. - TemplateConfiguration: the unique index on Template.Name becomes filtered (WHERE IsDerived = 0) — base templates stay globally unique; derived templates' uniqueness is the existing (TemplateId, InstanceName) index on TemplateComposition. - Migration ContainedDerivedTemplateNames: rewrites derived rows to the contained name; Down rebuilds the dotted names via a recursive CTE before restoring the global index. - TemplateService: composition create/rename store the contained name; the dotted-name collision pre-checks and cascade-rename are removed (a slot rename no longer touches nested derived templates). - TemplateEdit: title shows the contained name; the qualified path is a breadcrumb subtitle; "composed inside" uses the owner's qualified name. TDD: 4 TemplateNaming tests + updated composition tests. TemplateEngine 293, ConfigurationDatabase 114, CentralUI 316 green. Migration applied to the dev cluster and verified in the browser (Motor Controller.Pump now titled "Pump"; nested Motor Controller.Pump.TempSensor resolves). Design: docs/plans/2026-05-18-contained-template-names-design.md --- ...6-05-18-contained-template-names-design.md | 2 +- docs/requirements/Component-TemplateEngine.md | 12 + .../Pages/Design/TemplateEdit.razor | 20 +- .../Configurations/TemplateConfiguration.cs | 7 +- ..._ContainedDerivedTemplateNames.Designer.cs | 1351 +++++++++++++++++ ...518214444_ContainedDerivedTemplateNames.cs | 81 + .../ScadaLinkDbContextModelSnapshot.cs | 3 +- .../TemplateNaming.cs | 53 + .../TemplateService.cs | 90 +- .../TemplateNamingTests.cs | 87 ++ .../TemplateServiceTests.cs | 95 +- 11 files changed, 1662 insertions(+), 139 deletions(-) create mode 100644 src/ScadaLink.ConfigurationDatabase/Migrations/20260518214444_ContainedDerivedTemplateNames.Designer.cs create mode 100644 src/ScadaLink.ConfigurationDatabase/Migrations/20260518214444_ContainedDerivedTemplateNames.cs create mode 100644 src/ScadaLink.TemplateEngine/TemplateNaming.cs create mode 100644 tests/ScadaLink.TemplateEngine.Tests/TemplateNamingTests.cs diff --git a/docs/plans/2026-05-18-contained-template-names-design.md b/docs/plans/2026-05-18-contained-template-names-design.md index ad9aa4c..5f37fc8 100644 --- a/docs/plans/2026-05-18-contained-template-names-design.md +++ b/docs/plans/2026-05-18-contained-template-names-design.md @@ -1,7 +1,7 @@ # Contained Names for Composition-Derived Templates — Design **Date:** 2026-05-18 -**Status:** Approved (brainstorming) — implementation to follow +**Status:** Implemented ## Context diff --git a/docs/requirements/Component-TemplateEngine.md b/docs/requirements/Component-TemplateEngine.md index bdfb196..61a231f 100644 --- a/docs/requirements/Component-TemplateEngine.md +++ b/docs/requirements/Component-TemplateEngine.md @@ -81,6 +81,18 @@ When a template composes a feature module, members from that module are addresse - The composing template's own members (not from a module) have no prefix — they are top-level names. - Naming collision detection operates on canonical names, so two modules can define the same member name as long as their module instance names differ. +### Derived template naming + +A composition slot is materialized as its own *derived* template. A derived +template stores a **contained name** — the composition slot's instance name +(e.g. `Pump`), unique only within its owner. The **qualified name** +(`Motor Controller.Pump`, or `Motor Controller.Pump.TempSensor` when nested) is +*computed* on read by walking the owner-composition chain — it is not stored. +Only base (user-authored) templates are globally unique by name; a derived +template's uniqueness is the slot-name uniqueness within its owner. The Central +UI shows the contained name as the template title and the qualified path as a +breadcrumb. + ## Override Granularity Override and lock rules apply per entity type at the following granularity: diff --git a/src/ScadaLink.CentralUI/Components/Pages/Design/TemplateEdit.razor b/src/ScadaLink.CentralUI/Components/Pages/Design/TemplateEdit.razor index 64c8840..6732540 100644 --- a/src/ScadaLink.CentralUI/Components/Pages/Design/TemplateEdit.razor +++ b/src/ScadaLink.CentralUI/Components/Pages/Design/TemplateEdit.razor @@ -236,7 +236,7 @@ @_baseTemplate.Name @if (_ownerTemplate != null && _ownerComposition != null) { - — composed inside @_ownerTemplate.Name as @_ownerComposition.InstanceName. + — composed inside @QualifiedTemplateName(_ownerTemplate) as @_ownerComposition.InstanceName. } @@ -248,6 +248,12 @@ { inherits @(_templates.FirstOrDefault(t => t.Id == _selectedTemplate.ParentTemplateId)?.Name) } + @if (_selectedTemplate.IsDerived) + { + @* Derived templates store a contained name; show the full + qualified path as a breadcrumb subtitle. *@ +
@QualifiedTemplateName(_selectedTemplate)
+ }