@page "/uns/equipment/new" @page "/uns/equipment/{EquipmentId}" @* Dedicated tabbed editor for a single equipment, replacing the /uns EquipmentModal. This task builds the shell + the Details tab (lifted from EquipmentModal's EditForm) + the create→edit redirect; the Tags / Virtual Tags / Alarms tabs render placeholders wired in later tasks. On a successful create the page redirects to /uns/equipment/{newId} so the other tabs (disabled while new) become available. *@ @attribute [Microsoft.AspNetCore.Authorization.Authorize] @rendermode RenderMode.InteractiveServer @using System.ComponentModel.DataAnnotations @using Microsoft.AspNetCore.Components.Forms @using ZB.MOM.WW.OtOpcUa.AdminUI.Uns @using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Uns @inject IUnsTreeService Svc @inject NavigationManager Nav Equipment

@(IsNew ? "New equipment" : (_equipment?.Name ?? EquipmentId))

Back to UNS
@if (_loading) {

Loading…

} else if (!IsNew && _equipment is null) {
@EquipmentId not found.
} else { @if (_activeTab == "details") {
Identity
@if (!IsNew) {
System-generated; never operator-edited.
}
UNS level 5 segment; lowercase letters, digits, dashes, up to 32 chars.
@foreach (var (id, display) in _lineOptions) { }
@foreach (var (id, display) in _driverOptions) { }
Unique fleet-wide via ExternalIdReservation.

OPC 40010 identification (optional)
@if (!string.IsNullOrWhiteSpace(_error)) {
@_error
}
} else if (_activeTab == "tags") {
@if (!string.IsNullOrWhiteSpace(_tagError)) {
@_tagError
} @if (_tags is null) {

Loading…

} else if (_tags.Count == 0) {

No tags yet.

} else { @foreach (var t in _tags) { }
NameDriverData typeAccessActions
@t.Name @t.DriverInstanceId @t.DataType @t.AccessLevel
} } else if (_activeTab == "vtags") {
@if (!string.IsNullOrWhiteSpace(_vtagError)) {
@_vtagError
} @if (_vtags is null) {

Loading…

} else if (_vtags.Count == 0) {

No virtual tags yet.

} else { @foreach (var v in _vtags) { }
NameData typeScriptEnabledActions
@v.Name @v.DataType @v.ScriptId @(v.Enabled ? "Yes" : "No")
} } else if (_activeTab == "alarms") {

Alarms tab — wired in a later task.

} } @code { /// The equipment id from the route; null/empty on the /new route (create mode). [Parameter] public string? EquipmentId { get; set; } /// Optional parent line id supplied as a query string on create, used to default the line select. [SupplyParameterFromQuery] public string? LineId { get; set; } private bool IsNew => string.IsNullOrEmpty(EquipmentId); private string _activeTab = "details"; private bool _loading = true; private bool _busy; private string? _error; private EquipmentEditDto? _equipment; private FormModel _form = new(); private IReadOnlyList<(string Id, string Display)> _lineOptions = Array.Empty<(string, string)>(); private IReadOnlyList<(string Id, string Display)> _driverOptions = Array.Empty<(string, string)>(); // --- Tags tab state. _tags is null until the tab is first activated (drives the lazy load + spinner). --- private IReadOnlyList? _tags; private string? _tagError; private bool _tagModalVisible; private bool _tagModalIsNew; private TagEditDto? _tagModalExisting; private IReadOnlyList<(string Id, string Display, string DriverType)> _tagDriverOptions = Array.Empty<(string, string, string)>(); // --- Virtual Tags tab state. _vtags is null until the tab is first activated. --- private IReadOnlyList? _vtags; private string? _vtagError; private bool _vtagModalVisible; private bool _vtagModalIsNew; private VirtualTagEditDto? _vtagModalExisting; private IReadOnlyList<(string Id, string Display)> _vtagScriptOptions = Array.Empty<(string, string)>(); private string TabClass(string tab) => _activeTab == tab ? "active" : ""; /// /// Switches to a tab, lazily loading its list on first activation. The Tags/Virtual Tags lists are /// null until first shown (and are reset to null in OnParametersSetAsync when the equipment changes), /// so the fetch runs once per equipment rather than on every render. Equipment is fixed to /// ; the tabs are disabled while IsNew so this is only ever reached with a /// persisted equipment. /// private async Task ShowTabAsync(string tab) { _activeTab = tab; if (IsNew) { return; } if (tab == "tags" && _tags is null) { await ReloadTagsAsync(); } else if (tab == "vtags" && _vtags is null) { await ReloadVirtualTagsAsync(); } } // --- Tags tab handlers (mirror GlobalUns; the owning equipment is fixed = EquipmentId) --- private async Task ReloadTagsAsync() { _tags = await Svc.LoadTagsForEquipmentAsync(EquipmentId!); } private async Task OpenAddTag() { _tagError = null; _tagModalIsNew = true; _tagModalExisting = null; _tagDriverOptions = await Svc.LoadTagDriversForEquipmentAsync(EquipmentId!); _tagModalVisible = true; } private async Task OpenEditTag(string tagId) { _tagError = null; var dto = await Svc.LoadTagAsync(tagId); if (dto is null) { _tagError = "That tag no longer exists; the list was refreshed."; await ReloadTagsAsync(); return; } _tagModalIsNew = false; _tagModalExisting = dto; _tagDriverOptions = await Svc.LoadTagDriversForEquipmentAsync(EquipmentId!); _tagModalVisible = true; } private async Task OnTagSavedAsync() { _tagModalVisible = false; await ReloadTagsAsync(); } private async Task DeleteTag(string tagId) { _tagModalVisible = false; _tagError = null; // Load the tag fresh to capture its current RowVersion for the optimistic-concurrency delete. var dto = await Svc.LoadTagAsync(tagId); if (dto is null) { await ReloadTagsAsync(); return; } var r = await Svc.DeleteTagAsync(tagId, dto.RowVersion); if (r.Ok) { await ReloadTagsAsync(); } else { _tagError = r.Error; } } // --- Virtual Tags tab handlers --- private async Task ReloadVirtualTagsAsync() { _vtags = await Svc.LoadVirtualTagsForEquipmentAsync(EquipmentId!); } private async Task OpenAddVirtualTag() { _vtagError = null; _vtagModalIsNew = true; _vtagModalExisting = null; _vtagScriptOptions = await Svc.LoadScriptsAsync(); _vtagModalVisible = true; } private async Task OpenEditVirtualTag(string vtagId) { _vtagError = null; var dto = await Svc.LoadVirtualTagAsync(vtagId); if (dto is null) { _vtagError = "That virtual tag no longer exists; the list was refreshed."; await ReloadVirtualTagsAsync(); return; } _vtagModalIsNew = false; _vtagModalExisting = dto; _vtagScriptOptions = await Svc.LoadScriptsAsync(); _vtagModalVisible = true; } private async Task OnVirtualTagSavedAsync() { _vtagModalVisible = false; await ReloadVirtualTagsAsync(); } private async Task DeleteVirtualTag(string vtagId) { _vtagModalVisible = false; _vtagError = null; // Load the virtual tag fresh to capture its current RowVersion for the concurrency-guarded delete. var dto = await Svc.LoadVirtualTagAsync(vtagId); if (dto is null) { await ReloadVirtualTagsAsync(); return; } var r = await Svc.DeleteVirtualTagAsync(vtagId, dto.RowVersion); if (r.Ok) { await ReloadVirtualTagsAsync(); } else { _vtagError = r.Error; } } protected override async Task OnParametersSetAsync() { _loading = true; _error = null; // _activeTab is intentionally NOT reset here: an in-place save reloads the page (re-runs // OnParametersSetAsync) and the user's tab selection should survive that. The create→redirect // path lands on Details because the field initializes to "details" and a fresh page instance // starts with that initial value. The Tags/Virtual Tags lists are reset to null below so the // lazy loaders re-fetch for the (possibly different) equipment this parameter set targets. _tags = null; _vtags = null; if (!IsNew) { _equipment = await Svc.LoadEquipmentAsync(EquipmentId!); if (_equipment is not null) { LoadFormFrom(_equipment); var ctx = await Svc.LoadEquipmentPickContextAsync(_equipment.UnsLineId); _lineOptions = ctx.Lines; _driverOptions = ctx.Drivers; } } else { _form = new FormModel { UnsLineId = LineId ?? "" }; var ctx = await Svc.LoadEquipmentPickContextAsync(LineId); _lineOptions = ctx.Lines; _driverOptions = ctx.Drivers; } _loading = false; } /// Maps a loaded equipment's fields into the working form (mirrors EquipmentModal's edit branch). private void LoadFormFrom(EquipmentEditDto e) { _form = new FormModel { Name = e.Name, MachineCode = e.MachineCode, UnsLineId = e.UnsLineId, DriverInstanceId = e.DriverInstanceId, ZTag = e.ZTag, SAPID = e.SAPID, Manufacturer = e.Manufacturer, Model = e.Model, SerialNumber = e.SerialNumber, HardwareRevision = e.HardwareRevision, SoftwareRevision = e.SoftwareRevision, YearOfConstruction = e.YearOfConstruction, AssetLocation = e.AssetLocation, ManufacturerUri = e.ManufacturerUri, DeviceManualUri = e.DeviceManualUri, Enabled = e.Enabled, }; } private async Task SaveAsync() { _busy = true; _error = null; try { var input = new EquipmentInput( _form.Name, _form.MachineCode, _form.UnsLineId, _form.DriverInstanceId, _form.ZTag, _form.SAPID, _form.Manufacturer, _form.Model, _form.SerialNumber, _form.HardwareRevision, _form.SoftwareRevision, _form.YearOfConstruction, _form.AssetLocation, _form.ManufacturerUri, _form.DeviceManualUri, _form.Enabled); var result = IsNew ? await Svc.CreateEquipmentAsync(input) : await Svc.UpdateEquipmentAsync(_equipment!.EquipmentId, input, _equipment.RowVersion); if (!result.Ok) { _error = result.Error; return; } if (IsNew) { // Redirect to the persisted editor so the other tabs (disabled while new) become available. Nav.NavigateTo($"/uns/equipment/{result.CreatedId}"); } else { // Reload to pick up the fresh RowVersion for the next save. _equipment = await Svc.LoadEquipmentAsync(EquipmentId!); if (_equipment is not null) { LoadFormFrom(_equipment); } } } finally { _busy = false; } } /// The working form for the Details tab — identical to EquipmentModal.FormModel. private sealed class FormModel { [Required, RegularExpression("^[a-z0-9-]{1,32}$", ErrorMessage = "Lowercase letters, digits, dashes only; max 32 chars.")] public string Name { get; set; } = ""; [Required] public string MachineCode { get; set; } = ""; [Required] public string UnsLineId { get; set; } = ""; public string? DriverInstanceId { get; set; } public string? ZTag { get; set; } public string? SAPID { get; set; } public string? Manufacturer { get; set; } public string? Model { get; set; } public string? SerialNumber { get; set; } public string? HardwareRevision { get; set; } public string? SoftwareRevision { get; set; } public short? YearOfConstruction { get; set; } public string? AssetLocation { get; set; } public string? ManufacturerUri { get; set; } public string? DeviceManualUri { get; set; } public bool Enabled { get; set; } = true; } }