refactor(scripts): scoped parent query + parent picker for multi-parent templates
Two caveats from the script-scope rollout addressed: 1. ITemplateEngineRepository.GetTemplatesComposingAsync — a scoped query that returns only the templates referencing a given template via Compositions, eager-loaded with their Attributes / Scripts / Compositions. Replaces the GetAllTemplatesAsync + filter pattern in TemplateEdit so the Monaco metadata fetch doesn't pull the entire template catalog to find one parent. 2. Multi-parent picker. The previous implementation suppressed Parent assistance entirely when more than one template composes the open one. Now TemplateEdit collects every parent into _editorParents and renders a small `select` above the script editor when there are >1, letting the user choose which parent's metadata drives Parent.Attributes / Parent.CallScript completion + diagnostics. Single-parent templates skip the picker (no UI change). Zero parents (root template) hide the picker and surface no Parent assistance. Browser-verified on the Sensor Module template (composed by both Pump and Variable Speed Motor): picker shows both options, switching updates the editor's parent metadata immediately via the existing GetContext callback. Test counts unchanged (159 / 199); the new repo method is exercised end-to-end by the parent-picker browser path.
This commit is contained in:
@@ -94,7 +94,22 @@
|
|||||||
= Array.Empty<ScadaLink.CentralUI.ScriptAnalysis.DiagnosticMarker>();
|
= Array.Empty<ScadaLink.CentralUI.ScriptAnalysis.DiagnosticMarker>();
|
||||||
private IReadOnlyList<ScadaLink.CentralUI.ScriptAnalysis.CompositionContext> _editorChildren
|
private IReadOnlyList<ScadaLink.CentralUI.ScriptAnalysis.CompositionContext> _editorChildren
|
||||||
= Array.Empty<ScadaLink.CentralUI.ScriptAnalysis.CompositionContext>();
|
= Array.Empty<ScadaLink.CentralUI.ScriptAnalysis.CompositionContext>();
|
||||||
private ScadaLink.CentralUI.ScriptAnalysis.CompositionContext? _editorParent;
|
|
||||||
|
/// <summary>
|
||||||
|
/// One entry per template that composes this one (in the design-time
|
||||||
|
/// graph). Populated by <see cref="LoadEditorParentsAsync"/>; the user
|
||||||
|
/// picks one via <see cref="_selectedParentIndex"/> when multiple exist.
|
||||||
|
/// </summary>
|
||||||
|
private List<ScadaLink.CentralUI.ScriptAnalysis.CompositionContext> _editorParents
|
||||||
|
= new();
|
||||||
|
|
||||||
|
/// <summary>Index into <see cref="_editorParents"/>; -1 when none.</summary>
|
||||||
|
private int _selectedParentIndex = -1;
|
||||||
|
|
||||||
|
private ScadaLink.CentralUI.ScriptAnalysis.CompositionContext? ActiveEditorParent =>
|
||||||
|
_selectedParentIndex >= 0 && _selectedParentIndex < _editorParents.Count
|
||||||
|
? _editorParents[_selectedParentIndex]
|
||||||
|
: null;
|
||||||
|
|
||||||
private bool _showCompForm;
|
private bool _showCompForm;
|
||||||
private int _compComposedTemplateId;
|
private int _compComposedTemplateId;
|
||||||
@@ -129,12 +144,14 @@
|
|||||||
_scripts = (await TemplateEngineRepository.GetScriptsByTemplateIdAsync(Id)).ToList();
|
_scripts = (await TemplateEngineRepository.GetScriptsByTemplateIdAsync(Id)).ToList();
|
||||||
_compositions = (await TemplateEngineRepository.GetCompositionsByTemplateIdAsync(Id)).ToList();
|
_compositions = (await TemplateEngineRepository.GetCompositionsByTemplateIdAsync(Id)).ToList();
|
||||||
|
|
||||||
// Editor metadata: child compositions + parent (if exactly one).
|
// Editor metadata: child compositions + every parent that
|
||||||
// Powers Attributes["X"] / Children["Y"].Attributes["Z"] /
|
// composes this template. Powers Attributes["X"] /
|
||||||
// Parent.Attributes["W"] completion + SCADA006 / SCADA007 diagnostics
|
// Children["Y"].Attributes["Z"] / Parent.Attributes["W"]
|
||||||
// in the Monaco editor.
|
// completion + SCADA006 / SCADA007 diagnostics in the Monaco
|
||||||
|
// editor.
|
||||||
_editorChildren = await BuildChildContextsAsync(_compositions);
|
_editorChildren = await BuildChildContextsAsync(_compositions);
|
||||||
_editorParent = await TryGetParentContextAsync(Id);
|
_editorParents = await BuildParentContextsAsync(Id);
|
||||||
|
_selectedParentIndex = _editorParents.Count > 0 ? 0 : -1;
|
||||||
|
|
||||||
_validationResult = null;
|
_validationResult = null;
|
||||||
}
|
}
|
||||||
@@ -667,6 +684,21 @@
|
|||||||
</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)"
|
||||||
@@ -674,7 +706,7 @@
|
|||||||
SiblingScripts="@(_scripts.Select(s => ScadaLink.CentralUI.ScriptAnalysis.ScriptShapeParser.Parse(s.Name, s.ParameterDefinitions, s.ReturnDefinition)).ToArray())"
|
SiblingScripts="@(_scripts.Select(s => ScadaLink.CentralUI.ScriptAnalysis.ScriptShapeParser.Parse(s.Name, s.ParameterDefinitions, s.ReturnDefinition)).ToArray())"
|
||||||
SelfAttributes="@(_attributes.Select(a => new ScadaLink.CentralUI.ScriptAnalysis.AttributeShape(a.Name, MapDataType(a.DataType))).ToArray())"
|
SelfAttributes="@(_attributes.Select(a => new ScadaLink.CentralUI.ScriptAnalysis.AttributeShape(a.Name, MapDataType(a.DataType))).ToArray())"
|
||||||
Children="@_editorChildren"
|
Children="@_editorChildren"
|
||||||
Parent="@_editorParent"
|
Parent="@ActiveEditorParent"
|
||||||
MarkersChanged="@(m => { _scriptMarkers = m; StateHasChanged(); })" />
|
MarkersChanged="@(m => { _scriptMarkers = m; StateHasChanged(); })" />
|
||||||
<ProblemsPanel Markers="@_scriptMarkers" OnNavigate="@(m => _scriptEditor?.RevealLineAsync(m.StartLineNumber, m.StartColumn) ?? Task.CompletedTask)" />
|
<ProblemsPanel Markers="@_scriptMarkers" OnNavigate="@(m => _scriptEditor?.RevealLineAsync(m.StartLineNumber, m.StartColumn) ?? Task.CompletedTask)" />
|
||||||
</div>
|
</div>
|
||||||
@@ -999,13 +1031,12 @@
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<ScadaLink.CentralUI.ScriptAnalysis.CompositionContext?> TryGetParentContextAsync(int templateId)
|
private async Task<List<ScadaLink.CentralUI.ScriptAnalysis.CompositionContext>> BuildParentContextsAsync(int templateId)
|
||||||
{
|
{
|
||||||
var all = await TemplateEngineRepository.GetAllTemplatesAsync();
|
var parents = await TemplateEngineRepository.GetTemplatesComposingAsync(templateId);
|
||||||
var parents = all.Where(t => t.Compositions.Any(c => c.ComposedTemplateId == templateId)).ToList();
|
return parents
|
||||||
if (parents.Count != 1) return null; // ambiguous or root — suppress Parent assistance
|
.Select(p => BuildCompositionContext(p.Name, p))
|
||||||
var p = await TemplateEngineRepository.GetTemplateWithChildrenAsync(parents[0].Id);
|
.ToList();
|
||||||
return p == null ? null : BuildCompositionContext(p.Name, p);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ScadaLink.CentralUI.ScriptAnalysis.CompositionContext BuildCompositionContext(
|
private static ScadaLink.CentralUI.ScriptAnalysis.CompositionContext BuildCompositionContext(
|
||||||
@@ -1022,6 +1053,12 @@
|
|||||||
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",
|
||||||
|
|||||||
@@ -10,6 +10,13 @@ public interface ITemplateEngineRepository
|
|||||||
Task<Template?> GetTemplateByIdAsync(int id, CancellationToken cancellationToken = default);
|
Task<Template?> GetTemplateByIdAsync(int id, CancellationToken cancellationToken = default);
|
||||||
Task<Template?> GetTemplateWithChildrenAsync(int id, CancellationToken cancellationToken = default);
|
Task<Template?> GetTemplateWithChildrenAsync(int id, CancellationToken cancellationToken = default);
|
||||||
Task<IReadOnlyList<Template>> GetAllTemplatesAsync(CancellationToken cancellationToken = default);
|
Task<IReadOnlyList<Template>> GetAllTemplatesAsync(CancellationToken cancellationToken = default);
|
||||||
|
/// <summary>
|
||||||
|
/// Returns every template that contains a composition referencing
|
||||||
|
/// <paramref name="composedTemplateId"/>. Each result is eager-loaded with
|
||||||
|
/// its Attributes / Scripts / Compositions so the caller can build a
|
||||||
|
/// CompositionContext without a follow-up round-trip per parent.
|
||||||
|
/// </summary>
|
||||||
|
Task<IReadOnlyList<Template>> GetTemplatesComposingAsync(int composedTemplateId, CancellationToken cancellationToken = default);
|
||||||
Task AddTemplateAsync(Template template, CancellationToken cancellationToken = default);
|
Task AddTemplateAsync(Template template, CancellationToken cancellationToken = default);
|
||||||
Task UpdateTemplateAsync(Template template, CancellationToken cancellationToken = default);
|
Task UpdateTemplateAsync(Template template, CancellationToken cancellationToken = default);
|
||||||
Task DeleteTemplateAsync(int id, CancellationToken cancellationToken = default);
|
Task DeleteTemplateAsync(int id, CancellationToken cancellationToken = default);
|
||||||
|
|||||||
@@ -50,6 +50,16 @@ public class TemplateEngineRepository : ITemplateEngineRepository
|
|||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<Template>> GetTemplatesComposingAsync(int composedTemplateId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return await _context.Templates
|
||||||
|
.Where(t => t.Compositions.Any(c => c.ComposedTemplateId == composedTemplateId))
|
||||||
|
.Include(t => t.Attributes)
|
||||||
|
.Include(t => t.Scripts)
|
||||||
|
.Include(t => t.Compositions)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task AddTemplateAsync(Template template, CancellationToken cancellationToken = default)
|
public async Task AddTemplateAsync(Template template, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
await _context.Templates.AddAsync(template, cancellationToken);
|
await _context.Templates.AddAsync(template, cancellationToken);
|
||||||
|
|||||||
Reference in New Issue
Block a user