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:
+55
-11
@@ -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…").</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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user