feat(template-engine): contained names for composition-derived templates
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
This commit is contained in:
@@ -236,7 +236,7 @@
|
||||
<a href="/design/templates/@_baseTemplate.Id"><code>@_baseTemplate.Name</code></a>
|
||||
@if (_ownerTemplate != null && _ownerComposition != null)
|
||||
{
|
||||
<span class="ms-1">— composed inside <a href="/design/templates/@_ownerTemplate.Id"><code>@_ownerTemplate.Name</code></a> as <code>@_ownerComposition.InstanceName</code>.</span>
|
||||
<span class="ms-1">— composed inside <a href="/design/templates/@_ownerTemplate.Id"><code>@QualifiedTemplateName(_ownerTemplate)</code></a> as <code>@_ownerComposition.InstanceName</code>.</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -248,6 +248,12 @@
|
||||
{
|
||||
<span class="text-muted ms-2">inherits @(_templates.FirstOrDefault(t => t.Id == _selectedTemplate.ParentTemplateId)?.Name)</span>
|
||||
}
|
||||
@if (_selectedTemplate.IsDerived)
|
||||
{
|
||||
@* Derived templates store a contained name; show the full
|
||||
qualified path as a breadcrumb subtitle. *@
|
||||
<div class="text-muted small font-monospace">@QualifiedTemplateName(_selectedTemplate)</div>
|
||||
}
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn btn-outline-info btn-sm me-1" @onclick="RunValidation" disabled="@_validating">
|
||||
@@ -1701,6 +1707,18 @@
|
||||
else { _toast.ShowError(result.Error); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes a template's qualified (hierarchical) name from the loaded
|
||||
/// template set — the stored name for a base template, the dotted
|
||||
/// owner-chain path for a composition-derived one.
|
||||
/// </summary>
|
||||
private string QualifiedTemplateName(Template template)
|
||||
{
|
||||
var byId = _templates.ToDictionary(t => t.Id);
|
||||
var compById = _templates.SelectMany(t => t.Compositions).ToDictionary(c => c.Id);
|
||||
return TemplateNaming.QualifiedName(template, byId, compById);
|
||||
}
|
||||
|
||||
// ---- Editor metadata builders ----
|
||||
|
||||
private async Task<IReadOnlyList<ScadaLink.CentralUI.ScriptAnalysis.CompositionContext>> BuildChildContextsAsync(
|
||||
|
||||
Reference in New Issue
Block a user