From 0139c9ca83085198a58be04030721d4e08da0630 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Tue, 12 May 2026 06:00:02 -0400 Subject: [PATCH] refactor(scripts): scoped parent query + parent picker for multi-parent templates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../Pages/Design/TemplateEdit.razor | 63 +++++++++++++++---- .../Repositories/ITemplateEngineRepository.cs | 7 +++ .../Repositories/TemplateEngineRepository.cs | 10 +++ 3 files changed, 67 insertions(+), 13 deletions(-) diff --git a/src/ScadaLink.CentralUI/Components/Pages/Design/TemplateEdit.razor b/src/ScadaLink.CentralUI/Components/Pages/Design/TemplateEdit.razor index 36af7a1..8ae5ede 100644 --- a/src/ScadaLink.CentralUI/Components/Pages/Design/TemplateEdit.razor +++ b/src/ScadaLink.CentralUI/Components/Pages/Design/TemplateEdit.razor @@ -94,7 +94,22 @@ = Array.Empty(); private IReadOnlyList _editorChildren = Array.Empty(); - private ScadaLink.CentralUI.ScriptAnalysis.CompositionContext? _editorParent; + + /// + /// One entry per template that composes this one (in the design-time + /// graph). Populated by ; the user + /// picks one via when multiple exist. + /// + private List _editorParents + = new(); + + /// Index into ; -1 when none. + private int _selectedParentIndex = -1; + + private ScadaLink.CentralUI.ScriptAnalysis.CompositionContext? ActiveEditorParent => + _selectedParentIndex >= 0 && _selectedParentIndex < _editorParents.Count + ? _editorParents[_selectedParentIndex] + : null; private bool _showCompForm; private int _compComposedTemplateId; @@ -129,12 +144,14 @@ _scripts = (await TemplateEngineRepository.GetScriptsByTemplateIdAsync(Id)).ToList(); _compositions = (await TemplateEngineRepository.GetCompositionsByTemplateIdAsync(Id)).ToList(); - // Editor metadata: child compositions + parent (if exactly one). - // Powers Attributes["X"] / Children["Y"].Attributes["Z"] / - // Parent.Attributes["W"] completion + SCADA006 / SCADA007 diagnostics - // in the Monaco editor. + // Editor metadata: child compositions + every parent that + // composes this template. Powers Attributes["X"] / + // Children["Y"].Attributes["Z"] / Parent.Attributes["W"] + // completion + SCADA006 / SCADA007 diagnostics in the Monaco + // editor. _editorChildren = await BuildChildContextsAsync(_compositions); - _editorParent = await TryGetParentContextAsync(Id); + _editorParents = await BuildParentContextsAsync(Id); + _selectedParentIndex = _editorParents.Count > 0 ? 0 : -1; _validationResult = null; } @@ -667,6 +684,21 @@
+ @if (_editorParents.Count > 1) + { +
+ Parent context for editor: + + (this template is composed by @_editorParents.Count parents; pick one for Parent.* assistance) +
+ }
@@ -999,13 +1031,12 @@ return result; } - private async Task TryGetParentContextAsync(int templateId) + private async Task> BuildParentContextsAsync(int templateId) { - var all = await TemplateEngineRepository.GetAllTemplatesAsync(); - var parents = all.Where(t => t.Compositions.Any(c => c.ComposedTemplateId == templateId)).ToList(); - if (parents.Count != 1) return null; // ambiguous or root — suppress Parent assistance - var p = await TemplateEngineRepository.GetTemplateWithChildrenAsync(parents[0].Id); - return p == null ? null : BuildCompositionContext(p.Name, p); + var parents = await TemplateEngineRepository.GetTemplatesComposingAsync(templateId); + return parents + .Select(p => BuildCompositionContext(p.Name, p)) + .ToList(); } private static ScadaLink.CentralUI.ScriptAnalysis.CompositionContext BuildCompositionContext( @@ -1022,6 +1053,12 @@ 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 { ScadaLink.Commons.Types.Enums.DataType.Boolean => "Boolean", diff --git a/src/ScadaLink.Commons/Interfaces/Repositories/ITemplateEngineRepository.cs b/src/ScadaLink.Commons/Interfaces/Repositories/ITemplateEngineRepository.cs index d53b75c..397aa33 100644 --- a/src/ScadaLink.Commons/Interfaces/Repositories/ITemplateEngineRepository.cs +++ b/src/ScadaLink.Commons/Interfaces/Repositories/ITemplateEngineRepository.cs @@ -10,6 +10,13 @@ public interface ITemplateEngineRepository Task GetTemplateByIdAsync(int id, CancellationToken cancellationToken = default); Task GetTemplateWithChildrenAsync(int id, CancellationToken cancellationToken = default); Task> GetAllTemplatesAsync(CancellationToken cancellationToken = default); + /// + /// Returns every template that contains a composition referencing + /// . 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. + /// + Task> GetTemplatesComposingAsync(int composedTemplateId, CancellationToken cancellationToken = default); Task AddTemplateAsync(Template template, CancellationToken cancellationToken = default); Task UpdateTemplateAsync(Template template, CancellationToken cancellationToken = default); Task DeleteTemplateAsync(int id, CancellationToken cancellationToken = default); diff --git a/src/ScadaLink.ConfigurationDatabase/Repositories/TemplateEngineRepository.cs b/src/ScadaLink.ConfigurationDatabase/Repositories/TemplateEngineRepository.cs index 910d4d2..bc9bfa2 100644 --- a/src/ScadaLink.ConfigurationDatabase/Repositories/TemplateEngineRepository.cs +++ b/src/ScadaLink.ConfigurationDatabase/Repositories/TemplateEngineRepository.cs @@ -50,6 +50,16 @@ public class TemplateEngineRepository : ITemplateEngineRepository .ToListAsync(cancellationToken); } + public async Task> 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) { await _context.Templates.AddAsync(template, cancellationToken);