feat(centralui): Create Instance picks its template from the folder tree (+ ?templateId preselect)

Claude-Session: https://claude.ai/code/session_01G2GHPnbbKGvyyfbr2XBRVH
This commit is contained in:
Joseph Doherty
2026-08-21 07:42:14 -04:00
parent 4bd2651925
commit 5b4a27b23e
3 changed files with 327 additions and 21 deletions
@@ -9,6 +9,7 @@
@using ZB.MOM.WW.ScadaBridge.TemplateEngine.Services
@using ZB.MOM.WW.ScadaBridge.DeploymentManager
@using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Dialogs
@using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared
@using ZB.MOM.WW.ScadaBridge.CentralUI.Services
@attribute [Authorize(Policy = AuthorizationPolicies.RequireDeployment)]
@inject ITemplateEngineRepository TemplateEngineRepository
@@ -18,6 +19,7 @@
@inject IFlatteningPipeline FlatteningPipeline
@inject AuthenticationStateProvider AuthStateProvider
@inject NavigationManager NavigationManager
@inject IDialogService Dialog
<div class="container-fluid mt-3">
<div class="d-flex align-items-center mb-3">
@@ -39,15 +41,22 @@
</div>
<div class="mb-2">
<label class="form-label small">Template</label>
<select class="form-select form-select-sm" data-test="create-template"
value="@_createTemplateId"
@onchange="async e => { _createTemplateId = int.TryParse(e.Value?.ToString(), out var v) ? v : 0; await OnTemplateOrSiteChangedAsync(); }">
<option value="0">Select template...</option>
@foreach (var t in _templates)
{
<option value="@t.Id">@t.Name</option>
}
</select>
<div class="input-group input-group-sm">
<input type="text" class="form-control" data-test="create-template" readonly
placeholder="Choose a template…" value="@TemplatePathLabel"
style="cursor: pointer; background-color: var(--bs-body-bg);"
@onclick="OpenTemplatePickerAsync" />
<button class="btn btn-outline-secondary" type="button" data-test="create-template-choose"
disabled="@(_templates.Count == 0)" @onclick="OpenTemplatePickerAsync">Choose…</button>
</div>
@if (_templates.Count == 0)
{
<div class="form-text">No templates exist yet — <a href="/design/templates">create one on the Templates page</a>.</div>
}
else if (_templateHint is not null)
{
<div class="form-text text-warning-emphasis">@_templateHint</div>
}
</div>
<div class="mb-2">
<label class="form-label small">Site</label>
@@ -228,11 +237,21 @@
</div>
@code {
/// <summary>Query preselect: the template the operator picked elsewhere (e.g. the Templates tree's "New Instance&#8230;").</summary>
[SupplyParameterFromQuery] public int? TemplateId { get; set; }
[SupplyParameterFromQuery] public int? SiteId { get; set; }
[SupplyParameterFromQuery] public int? AreaId { get; set; }
private List<Site> _sites = new();
private List<Template> _templates = new();
private List<TemplateFolder> _folders = new();
private Dictionary<int, TemplateFolder> _foldersById = new();
private string? _templateHint;
/// <summary>Folder-qualified label shown in the read-only template field; blank when nothing is chosen.</summary>
private string TemplatePathLabel => _templates.FirstOrDefault(t => t.Id == _createTemplateId) is { } t
? TemplatePathFormatter.Format(t, _foldersById)
: string.Empty;
private List<Area> _allAreas = new();
private bool _loading = true;
@@ -247,6 +266,13 @@
try
{
_templates = (await TemplateEngineRepository.GetAllTemplatesAsync()).ToList();
_folders = (await TemplateEngineRepository.GetAllFoldersAsync()).ToList();
_foldersById = _folders.ToDictionary(f => f.Id);
if (TemplateId is int tid)
{
if (_templates.Any(t => t.Id == tid)) _createTemplateId = tid;
else _templateHint = $"Template {tid} no longer exists — choose another.";
}
// Site scoping (CentralUI-002): a scoped Deployment user may only
// create instances on sites they are permitted on.
_sites = await SiteScope.FilterSitesAsync(await SiteRepository.GetAllSitesAsync());
@@ -268,8 +294,8 @@
}
// The query string can preselect a site; a template never is, so this
// no-ops today — kept so a future preselect gets the card for free.
// Query-string preselects (siteId + templateId) make this a real preview
// when both are present; it no-ops when either is missing.
await OnTemplateOrSiteChangedAsync();
}
catch (Exception ex)
@@ -353,4 +379,22 @@
// resolved through JwtTokenService rather than a duplicated magic string.
private Task<string> GetCurrentUserAsync()
=> AuthStateProvider.GetCurrentUsernameAsync();
/// <summary>
/// Opens the folder-tree template picker. A pick clears any dangling-id hint and
/// re-runs the bindings preview; Cancel (or re-picking the same template) is a no-op.
/// </summary>
private async Task OpenTemplatePickerAsync()
{
if (_templates.Count == 0) return;
var picked = await Dialog.ShowAsync<TemplatePickerDialog.TemplatePickResult>(
"Choose template",
ctx => @<TemplatePickerDialog Context="ctx" Folders="_folders" Templates="_templates"
InitialTemplateId="@(_createTemplateId > 0 ? _createTemplateId : null)" />,
size: "modal-lg");
if (picked is null || picked.TemplateId == _createTemplateId) return;
_createTemplateId = picked.TemplateId;
_templateHint = null;
await OnTemplateOrSiteChangedAsync();
}
}