feat(templates/ui): phase 9 — single-parent editor context

Derive-on-compose guarantees at most one slot owner per template, so the
Parent.* context in the Monaco editor resolves directly via
OwnerCompositionId without a picker. Base templates suppress Parent.*
assistance entirely (empty context).

Removed the multi-parent <select> dropdown from the Add Script form and
the now-redundant _selectedParentIndex / OnParentContextChanged plumbing.
ActiveEditorParent collapses to _editorParents.FirstOrDefault().
This commit is contained in:
Joseph Doherty
2026-05-12 08:57:42 -04:00
parent f05b03f1cc
commit a965d4a5bd

View File

@@ -103,20 +103,15 @@
= Array.Empty<ScadaLink.CentralUI.ScriptAnalysis.CompositionContext>(); = Array.Empty<ScadaLink.CentralUI.ScriptAnalysis.CompositionContext>();
/// <summary> /// <summary>
/// One entry per template that composes this one (in the design-time /// Editor's Parent.* context. Empty for base templates (no owner exists);
/// graph). Populated by <see cref="LoadEditorParentsAsync"/>; the user /// exactly one entry for derived templates — the slot-owner resolved from
/// picks one via <see cref="_selectedParentIndex"/> when multiple exist. /// the template's OwnerCompositionId.
/// </summary> /// </summary>
private List<ScadaLink.CentralUI.ScriptAnalysis.CompositionContext> _editorParents private List<ScadaLink.CentralUI.ScriptAnalysis.CompositionContext> _editorParents
= new(); = new();
/// <summary>Index into <see cref="_editorParents"/>; -1 when none.</summary>
private int _selectedParentIndex = -1;
private ScadaLink.CentralUI.ScriptAnalysis.CompositionContext? ActiveEditorParent => private ScadaLink.CentralUI.ScriptAnalysis.CompositionContext? ActiveEditorParent =>
_selectedParentIndex >= 0 && _selectedParentIndex < _editorParents.Count _editorParents.FirstOrDefault();
? _editorParents[_selectedParentIndex]
: null;
private bool _showCompForm; private bool _showCompForm;
private int _compComposedTemplateId; private int _compComposedTemplateId;
@@ -190,7 +185,6 @@
// editor. // editor.
_editorChildren = await BuildChildContextsAsync(_compositions); _editorChildren = await BuildChildContextsAsync(_compositions);
_editorParents = await BuildParentContextsAsync(Id); _editorParents = await BuildParentContextsAsync(Id);
_selectedParentIndex = _editorParents.Count > 0 ? 0 : -1;
_validationResult = null; _validationResult = null;
} }
@@ -865,21 +859,6 @@
</div> </div>
<div class="col-12"> <div class="col-12">
<label class="form-label">Code</label> <label class="form-label">Code</label>
@if (_editorParents.Count > 1)
{
<div class="small text-muted mb-2">
Parent context for editor:
<select class="form-select form-select-sm d-inline-block w-auto ms-1"
value="@_selectedParentIndex"
@onchange="OnParentContextChanged">
@foreach (var (parent, idx) in _editorParents.Select((p, i) => (p, i)))
{
<option value="@idx">@parent.Name</option>
}
</select>
<span class="ms-2">(this template is composed by @_editorParents.Count parents; pick one for <code>Parent.*</code> assistance)</span>
</div>
}
<MonacoEditor @ref="_scriptEditor" Value="@_scriptCode" ValueChanged="@(v => _scriptCode = v)" <MonacoEditor @ref="_scriptEditor" Value="@_scriptCode" ValueChanged="@(v => _scriptCode = v)"
Language="csharp" Height="320px" Language="csharp" Height="320px"
DeclaredParameters="@ScriptParameterNames.Parse(_scriptParameters)" DeclaredParameters="@ScriptParameterNames.Parse(_scriptParameters)"
@@ -1311,10 +1290,21 @@
private async Task<List<ScadaLink.CentralUI.ScriptAnalysis.CompositionContext>> BuildParentContextsAsync(int templateId) private async Task<List<ScadaLink.CentralUI.ScriptAnalysis.CompositionContext>> BuildParentContextsAsync(int templateId)
{ {
var parents = await TemplateEngineRepository.GetTemplatesComposingAsync(templateId); // Post derive-on-compose: only derived templates have a parent context,
return parents // and exactly one — the template that owns their composition slot.
.Select(p => BuildCompositionContext(p.Name, p)) // Base templates suppress Parent.* assistance.
.ToList(); if (_selectedTemplate?.IsDerived != true || _ownerTemplate == null)
return new List<ScadaLink.CentralUI.ScriptAnalysis.CompositionContext>();
// Resolve the owner with eager-loaded members so the context has shapes.
var owner = await TemplateEngineRepository.GetTemplateByIdAsync(_ownerTemplate.Id);
if (owner == null)
return new List<ScadaLink.CentralUI.ScriptAnalysis.CompositionContext>();
return new List<ScadaLink.CentralUI.ScriptAnalysis.CompositionContext>
{
BuildCompositionContext(owner.Name, owner)
};
} }
private static ScadaLink.CentralUI.ScriptAnalysis.CompositionContext BuildCompositionContext( private static ScadaLink.CentralUI.ScriptAnalysis.CompositionContext BuildCompositionContext(
@@ -1331,12 +1321,6 @@
return new ScadaLink.CentralUI.ScriptAnalysis.CompositionContext(label, attrs, scripts); return new ScadaLink.CentralUI.ScriptAnalysis.CompositionContext(label, attrs, scripts);
} }
private void OnParentContextChanged(ChangeEventArgs e)
{
if (int.TryParse(e.Value?.ToString(), out var idx) && idx >= 0 && idx < _editorParents.Count)
_selectedParentIndex = idx;
}
private static string MapDataType(ScadaLink.Commons.Types.Enums.DataType dt) => dt switch private static string MapDataType(ScadaLink.Commons.Types.Enums.DataType dt) => dt switch
{ {
ScadaLink.Commons.Types.Enums.DataType.Boolean => "Boolean", ScadaLink.Commons.Types.Enums.DataType.Boolean => "Boolean",