feat(uns): equipment is a tree leaf linking to the detail page; drop EquipmentModal
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
@using ZB.MOM.WW.OtOpcUa.AdminUI.Uns
|
||||
@using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Uns
|
||||
@inject IUnsTreeService Svc
|
||||
@inject NavigationManager Nav
|
||||
|
||||
<PageTitle>UNS</PageTitle>
|
||||
|
||||
@@ -41,7 +42,6 @@
|
||||
<UnsTree Roots="_roots" Filter="@_filter"
|
||||
OnToggleExpand="ToggleAsync"
|
||||
OnAddChild="HandleAddChild"
|
||||
OnAddVirtualTag="HandleAddVirtualTag"
|
||||
OnEdit="HandleEdit"
|
||||
OnDelete="HandleDelete" />
|
||||
</div>
|
||||
@@ -64,31 +64,6 @@
|
||||
OnSaved="OnModalSavedAsync"
|
||||
OnCancel="CloseModals" />
|
||||
|
||||
<EquipmentModal Visible="_equipmentModalVisible"
|
||||
IsNew="_equipmentModalIsNew"
|
||||
UnsLineId="@_equipmentModalLineId"
|
||||
Existing="_equipmentModalExisting"
|
||||
Lines="_equipmentModalLineOptions"
|
||||
Drivers="_equipmentModalDriverOptions"
|
||||
OnSaved="OnModalSavedAsync"
|
||||
OnCancel="CloseModals" />
|
||||
|
||||
<TagModal Visible="_tagModalVisible"
|
||||
IsNew="_tagModalIsNew"
|
||||
EquipmentId="@_tagModalEquipmentId"
|
||||
Existing="_tagModalExisting"
|
||||
Drivers="_tagModalDriverOptions"
|
||||
OnSaved="OnEquipmentChildModalSavedAsync"
|
||||
OnCancel="CloseModals" />
|
||||
|
||||
<VirtualTagModal Visible="_vtagModalVisible"
|
||||
IsNew="_vtagModalIsNew"
|
||||
EquipmentId="@_vtagModalEquipmentId"
|
||||
Existing="_vtagModalExisting"
|
||||
Scripts="_vtagModalScriptOptions"
|
||||
OnSaved="OnEquipmentChildModalSavedAsync"
|
||||
OnCancel="CloseModals" />
|
||||
|
||||
<ImportEquipmentModal Visible="_importModalVisible"
|
||||
OnImported="OnImportedAsync"
|
||||
OnCancel="CloseModals" />
|
||||
@@ -127,7 +102,7 @@
|
||||
private string? _filter;
|
||||
private bool _loading = true;
|
||||
|
||||
// Guards the async modal openers (HandleAddChild/HandleAddVirtualTag/HandleEdit) so a rapid
|
||||
// Guards the async modal openers (HandleAddChild/HandleEdit) so a rapid
|
||||
// double-action can't race two service loads into the same modal state.
|
||||
private bool _modalBusy;
|
||||
|
||||
@@ -144,34 +119,9 @@
|
||||
private LineEditDto? _lineModalExisting;
|
||||
private IReadOnlyList<(string Id, string Display)> _lineModalAreaOptions = Array.Empty<(string, string)>();
|
||||
|
||||
// --- Equipment modal state ---
|
||||
private bool _equipmentModalVisible;
|
||||
private bool _equipmentModalIsNew;
|
||||
private string? _equipmentModalLineId;
|
||||
private EquipmentEditDto? _equipmentModalExisting;
|
||||
private IReadOnlyList<(string Id, string Display)> _equipmentModalLineOptions = Array.Empty<(string, string)>();
|
||||
private IReadOnlyList<(string Id, string Display)> _equipmentModalDriverOptions = Array.Empty<(string, string)>();
|
||||
|
||||
// --- Tag modal state ---
|
||||
private bool _tagModalVisible;
|
||||
private bool _tagModalIsNew;
|
||||
private string? _tagModalEquipmentId;
|
||||
private TagEditDto? _tagModalExisting;
|
||||
private IReadOnlyList<(string Id, string Display, string DriverType)> _tagModalDriverOptions = Array.Empty<(string, string, string)>();
|
||||
|
||||
// --- Virtual-tag modal state ---
|
||||
private bool _vtagModalVisible;
|
||||
private bool _vtagModalIsNew;
|
||||
private string? _vtagModalEquipmentId;
|
||||
private VirtualTagEditDto? _vtagModalExisting;
|
||||
private IReadOnlyList<(string Id, string Display)> _vtagModalScriptOptions = Array.Empty<(string, string)>();
|
||||
|
||||
// --- Import-equipment-CSV modal state ---
|
||||
private bool _importModalVisible;
|
||||
|
||||
// --- Owning equipment to refresh in place after a tag/virtual-tag mutation ---
|
||||
private string? _childRefreshEquipmentId;
|
||||
|
||||
// --- Delete-confirm state ---
|
||||
private UnsNode? _confirmNode;
|
||||
private bool _confirmBusy;
|
||||
@@ -214,43 +164,21 @@
|
||||
.ToList();
|
||||
|
||||
/// <summary>
|
||||
/// Toggles a node's expansion. For equipment nodes whose children have not yet
|
||||
/// been loaded, lazily fetches the tag/virtual-tag leaves on first expand.
|
||||
/// Toggles a structural node's expansion. Equipment nodes are leaves (no expander),
|
||||
/// so this path only ever fires for Enterprise/Cluster/Area/Line.
|
||||
/// </summary>
|
||||
private async Task ToggleAsync(UnsNode node)
|
||||
private Task ToggleAsync(UnsNode node)
|
||||
{
|
||||
if (node.Loading) return;
|
||||
if (node.Loading) { return Task.CompletedTask; }
|
||||
|
||||
node.Expanded = !node.Expanded;
|
||||
|
||||
if (node.Kind == UnsNodeKind.Equipment && node.Expanded && !node.Loaded)
|
||||
{
|
||||
node.Error = null;
|
||||
node.Loading = true;
|
||||
StateHasChanged();
|
||||
try
|
||||
{
|
||||
var kids = await Svc.LoadEquipmentChildrenAsync(node.EntityId!);
|
||||
node.Children.Clear();
|
||||
node.Children.AddRange(kids);
|
||||
node.Loaded = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
node.Error = ex.Message;
|
||||
}
|
||||
finally
|
||||
{
|
||||
node.Loading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the create modal for a node's primary child: a cluster gets a new area; an area gets a
|
||||
/// new line scoped to its cluster; a line gets a new equipment scoped to its cluster; an equipment
|
||||
/// gets a new tag scoped to its candidate drivers.
|
||||
/// Opens the create modal for a structural node's primary child: a cluster gets a new area; an area
|
||||
/// gets a new line scoped to its cluster. A line's "+ Equipment" navigates to the equipment detail
|
||||
/// page in create mode rather than opening a modal.
|
||||
/// </summary>
|
||||
private async Task HandleAddChild(UnsNode node)
|
||||
{
|
||||
@@ -277,21 +205,7 @@
|
||||
break;
|
||||
|
||||
case UnsNodeKind.Line:
|
||||
_equipmentModalIsNew = true;
|
||||
_equipmentModalExisting = null;
|
||||
_equipmentModalLineId = node.EntityId;
|
||||
_equipmentModalLineOptions = LinesForCluster(node.ClusterId);
|
||||
_equipmentModalDriverOptions = await Svc.LoadDriversForClusterAsync(node.ClusterId!);
|
||||
_equipmentModalVisible = true;
|
||||
break;
|
||||
|
||||
case UnsNodeKind.Equipment:
|
||||
_tagModalIsNew = true;
|
||||
_tagModalExisting = null;
|
||||
_tagModalEquipmentId = node.EntityId;
|
||||
_childRefreshEquipmentId = node.EntityId;
|
||||
_tagModalDriverOptions = await Svc.LoadTagDriversForEquipmentAsync(node.EntityId!);
|
||||
_tagModalVisible = true;
|
||||
Nav.NavigateTo($"/uns/equipment/new?lineId={node.EntityId}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -301,31 +215,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Opens the create modal for a new virtual tag scoped to the clicked equipment.</summary>
|
||||
private async Task HandleAddVirtualTag(UnsNode node)
|
||||
{
|
||||
if (_modalBusy) { return; }
|
||||
_modalBusy = true;
|
||||
try
|
||||
{
|
||||
CloseModals();
|
||||
_vtagModalIsNew = true;
|
||||
_vtagModalExisting = null;
|
||||
_vtagModalEquipmentId = node.EntityId;
|
||||
_childRefreshEquipmentId = node.EntityId;
|
||||
_vtagModalScriptOptions = await Svc.LoadScriptsAsync();
|
||||
_vtagModalVisible = true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_modalBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the edit modal for the clicked node, loading the entity first to prefill the form and
|
||||
/// capture its RowVersion. Tag/VirtualTag edits also stash the owning equipment id so a successful
|
||||
/// save can refresh just that equipment's children in place.
|
||||
/// capture its RowVersion. Only Area and Line are edited via a modal here; equipment is edited on
|
||||
/// its own detail page (opened via the tree's "Open" link).
|
||||
/// </summary>
|
||||
private async Task HandleEdit(UnsNode node)
|
||||
{
|
||||
@@ -354,39 +247,6 @@
|
||||
_lineModalAreaOptions = AreaOptionsForCluster(node.ClusterId);
|
||||
_lineModalVisible = true;
|
||||
break;
|
||||
|
||||
case UnsNodeKind.Equipment:
|
||||
var equipment = await Svc.LoadEquipmentAsync(node.EntityId!);
|
||||
if (equipment is null) { return; }
|
||||
_equipmentModalIsNew = false;
|
||||
_equipmentModalExisting = equipment;
|
||||
_equipmentModalLineId = equipment.UnsLineId;
|
||||
_equipmentModalLineOptions = LinesForCluster(node.ClusterId);
|
||||
_equipmentModalDriverOptions = await Svc.LoadDriversForClusterAsync(node.ClusterId!);
|
||||
_equipmentModalVisible = true;
|
||||
break;
|
||||
|
||||
case UnsNodeKind.Tag:
|
||||
var tag = await Svc.LoadTagAsync(node.EntityId!);
|
||||
if (tag is null) { return; }
|
||||
_tagModalIsNew = false;
|
||||
_tagModalExisting = tag;
|
||||
_tagModalEquipmentId = tag.EquipmentId;
|
||||
_childRefreshEquipmentId = tag.EquipmentId;
|
||||
_tagModalDriverOptions = await Svc.LoadTagDriversForEquipmentAsync(tag.EquipmentId);
|
||||
_tagModalVisible = true;
|
||||
break;
|
||||
|
||||
case UnsNodeKind.VirtualTag:
|
||||
var vtag = await Svc.LoadVirtualTagAsync(node.EntityId!);
|
||||
if (vtag is null) { return; }
|
||||
_vtagModalIsNew = false;
|
||||
_vtagModalExisting = vtag;
|
||||
_vtagModalEquipmentId = vtag.EquipmentId;
|
||||
_childRefreshEquipmentId = vtag.EquipmentId;
|
||||
_vtagModalScriptOptions = await Svc.LoadScriptsAsync();
|
||||
_vtagModalVisible = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
finally
|
||||
@@ -422,8 +282,7 @@
|
||||
|
||||
/// <summary>
|
||||
/// Performs the pending delete. Loads the entity's RowVersion first, then dispatches on Kind.
|
||||
/// Area/Line/Equipment trigger a full structural reload on success; Tag/VirtualTag refresh only the
|
||||
/// owning equipment's children in place so the rest of the user's expansion is preserved.
|
||||
/// Area/Line/Equipment trigger a full structural reload on success.
|
||||
/// </summary>
|
||||
private async Task ConfirmDeleteAsync()
|
||||
{
|
||||
@@ -436,42 +295,8 @@
|
||||
var node = _confirmNode;
|
||||
UnsMutationResult result;
|
||||
|
||||
// Tag/VirtualTag deletes refresh just the owning equipment's children rather than the
|
||||
// whole tree, so they're handled separately from the structural Area/Line/Equipment path.
|
||||
switch (node.Kind)
|
||||
{
|
||||
case UnsNodeKind.Tag:
|
||||
var tag = await Svc.LoadTagAsync(node.EntityId!);
|
||||
if (tag is null) { await ReloadAndCloseAsync(); return; }
|
||||
result = await Svc.DeleteTagAsync(node.EntityId!, tag.RowVersion);
|
||||
if (result.Ok)
|
||||
{
|
||||
await RefreshEquipmentChildrenAsync(tag.EquipmentId);
|
||||
CloseModals();
|
||||
StateHasChanged();
|
||||
}
|
||||
else
|
||||
{
|
||||
_confirmError = result.Error;
|
||||
}
|
||||
return;
|
||||
|
||||
case UnsNodeKind.VirtualTag:
|
||||
var vtag = await Svc.LoadVirtualTagAsync(node.EntityId!);
|
||||
if (vtag is null) { await ReloadAndCloseAsync(); return; }
|
||||
result = await Svc.DeleteVirtualTagAsync(node.EntityId!, vtag.RowVersion);
|
||||
if (result.Ok)
|
||||
{
|
||||
await RefreshEquipmentChildrenAsync(vtag.EquipmentId);
|
||||
CloseModals();
|
||||
StateHasChanged();
|
||||
}
|
||||
else
|
||||
{
|
||||
_confirmError = result.Error;
|
||||
}
|
||||
return;
|
||||
|
||||
case UnsNodeKind.Area:
|
||||
var area = await Svc.LoadAreaAsync(node.EntityId!);
|
||||
if (area is null) { await ReloadAndCloseAsync(); return; }
|
||||
@@ -519,70 +344,6 @@
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles a successful Tag/VirtualTag modal save by refreshing only the owning equipment's children
|
||||
/// in place — never a full structural reload, which would collapse the user's expansion.
|
||||
/// </summary>
|
||||
private async Task OnEquipmentChildModalSavedAsync()
|
||||
{
|
||||
if (_childRefreshEquipmentId is not null)
|
||||
{
|
||||
await RefreshEquipmentChildrenAsync(_childRefreshEquipmentId);
|
||||
}
|
||||
CloseModals();
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reloads a single equipment node's tag/virtual-tag children in place, leaving the rest of the tree
|
||||
/// (and the user's expansion) untouched. Falls back to a full structural reload only if the node
|
||||
/// can no longer be found in the current tree. Either branch only mutates state — the caller is
|
||||
/// responsible for calling StateHasChanged() afterwards (every current caller does).
|
||||
/// </summary>
|
||||
private async Task RefreshEquipmentChildrenAsync(string equipmentId)
|
||||
{
|
||||
var node = FindEquipmentNode(equipmentId);
|
||||
if (node is null)
|
||||
{
|
||||
// Fallback: the equipment node is no longer in the current tree — reload the whole structure.
|
||||
_roots = await Svc.LoadStructureAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
var kids = await Svc.LoadEquipmentChildrenAsync(equipmentId);
|
||||
node.Children.Clear();
|
||||
node.Children.AddRange(kids);
|
||||
node.ChildCount = node.Children.Count;
|
||||
node.Loaded = true;
|
||||
node.Expanded = true;
|
||||
}
|
||||
|
||||
/// <summary>Recursively walks the current tree for the Equipment node with the given id, or null.</summary>
|
||||
private UnsNode? FindEquipmentNode(string equipmentId)
|
||||
{
|
||||
foreach (var root in _roots)
|
||||
{
|
||||
var found = FindEquipmentNode(root, equipmentId);
|
||||
if (found is not null) { return found; }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static UnsNode? FindEquipmentNode(UnsNode node, string equipmentId)
|
||||
{
|
||||
if (node.Kind == UnsNodeKind.Equipment && node.EntityId == equipmentId)
|
||||
{
|
||||
return node;
|
||||
}
|
||||
|
||||
foreach (var child in node.Children)
|
||||
{
|
||||
var found = FindEquipmentNode(child, equipmentId);
|
||||
if (found is not null) { return found; }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>Reloads the tree after a successful delete and closes the confirm modal.</summary>
|
||||
private async Task ReloadAndCloseAsync()
|
||||
{
|
||||
@@ -603,24 +364,7 @@
|
||||
_lineModalAreaId = null;
|
||||
_lineModalExisting = null;
|
||||
_lineModalAreaOptions = Array.Empty<(string, string)>();
|
||||
_equipmentModalVisible = false;
|
||||
_equipmentModalIsNew = false;
|
||||
_equipmentModalLineId = null;
|
||||
_equipmentModalExisting = null;
|
||||
_equipmentModalLineOptions = Array.Empty<(string, string)>();
|
||||
_equipmentModalDriverOptions = Array.Empty<(string, string)>();
|
||||
_tagModalVisible = false;
|
||||
_tagModalIsNew = false;
|
||||
_tagModalEquipmentId = null;
|
||||
_tagModalExisting = null;
|
||||
_tagModalDriverOptions = Array.Empty<(string, string, string)>();
|
||||
_vtagModalVisible = false;
|
||||
_vtagModalIsNew = false;
|
||||
_vtagModalEquipmentId = null;
|
||||
_vtagModalExisting = null;
|
||||
_vtagModalScriptOptions = Array.Empty<(string, string)>();
|
||||
_importModalVisible = false;
|
||||
_childRefreshEquipmentId = null;
|
||||
_confirmNode = null;
|
||||
_confirmError = null;
|
||||
}
|
||||
|
||||
@@ -1,252 +0,0 @@
|
||||
@* Create/edit modal for an equipment, wired straight into IUnsTreeService. The host page owns
|
||||
visibility and supplies the parent line id (create) or the loaded EquipmentEditDto (edit), plus
|
||||
the cluster-scoped UNS-line and driver lists. The EquipmentId is system-generated (decision #125)
|
||||
so it is never an editable field — only shown read-only on edit. On a successful save it raises
|
||||
OnSaved so the host can reload the tree. *@
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using ZB.MOM.WW.OtOpcUa.AdminUI.Uns
|
||||
@inject IUnsTreeService Svc
|
||||
|
||||
@if (Visible)
|
||||
{
|
||||
<div class="modal-backdrop fade show" style="display:block"></div>
|
||||
<div class="modal fade show" tabindex="-1" role="dialog" style="display:block">
|
||||
<div class="modal-dialog modal-xl" role="document">
|
||||
<div class="modal-content">
|
||||
<EditForm Model="_form" OnValidSubmit="SaveAsync" FormName="equipmentModal">
|
||||
<DataAnnotationsValidator />
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">@(IsNew ? "New equipment" : "Edit equipment")</h5>
|
||||
<button type="button" class="btn-close" aria-label="Close" @onclick="CancelAsync"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<h6 class="text-muted">Identity</h6>
|
||||
@if (!IsNew)
|
||||
{
|
||||
<div class="mb-3">
|
||||
<label class="form-label">EquipmentId</label>
|
||||
<input class="form-control form-control-sm mono" value="@Existing?.EquipmentId" disabled />
|
||||
<div class="form-text">System-generated; never operator-edited.</div>
|
||||
</div>
|
||||
}
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label" for="eq-name">Name</label>
|
||||
<InputText id="eq-name" @bind-Value="_form.Name" class="form-control form-control-sm mono"
|
||||
placeholder="machine-01" />
|
||||
<div class="form-text">UNS level 5 segment; lowercase letters, digits, dashes, up to 32 chars.</div>
|
||||
<ValidationMessage For="@(() => _form.Name)" />
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label" for="eq-machinecode">MachineCode</label>
|
||||
<InputText id="eq-machinecode" @bind-Value="_form.MachineCode" class="form-control form-control-sm mono"
|
||||
placeholder="machine_001" />
|
||||
<ValidationMessage For="@(() => _form.MachineCode)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label" for="eq-line">UNS line</label>
|
||||
<InputSelect id="eq-line" @bind-Value="_form.UnsLineId" class="form-select form-select-sm">
|
||||
<option value="">— pick a line —</option>
|
||||
@foreach (var (id, display) in Lines)
|
||||
{
|
||||
<option value="@id">@display</option>
|
||||
}
|
||||
</InputSelect>
|
||||
<ValidationMessage For="@(() => _form.UnsLineId)" />
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label" for="eq-driver">Driver instance</label>
|
||||
<InputSelect id="eq-driver" @bind-Value="_form.DriverInstanceId" class="form-select form-select-sm">
|
||||
<option value="">(none / driver-less)</option>
|
||||
@foreach (var (id, display) in Drivers)
|
||||
{
|
||||
<option value="@id">@display</option>
|
||||
}
|
||||
</InputSelect>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label" for="eq-ztag">ZTag (ERP)</label>
|
||||
<InputText id="eq-ztag" @bind-Value="_form.ZTag" class="form-control form-control-sm" />
|
||||
<div class="form-text">Unique fleet-wide via ExternalIdReservation.</div>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label" for="eq-sap">SAPID</label>
|
||||
<InputText id="eq-sap" @bind-Value="_form.SAPID" class="form-control form-control-sm" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Enabled</label>
|
||||
<div class="form-check form-switch">
|
||||
<InputCheckbox @bind-Value="_form.Enabled" class="form-check-input" />
|
||||
<label class="form-check-label">Surface in deployments</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
<h6 class="text-muted">OPC 40010 identification (optional)</h6>
|
||||
<div class="row">
|
||||
<div class="col-md-4 mb-3"><label class="form-label">Manufacturer</label><InputText @bind-Value="_form.Manufacturer" class="form-control form-control-sm" /></div>
|
||||
<div class="col-md-4 mb-3"><label class="form-label">Model</label><InputText @bind-Value="_form.Model" class="form-control form-control-sm" /></div>
|
||||
<div class="col-md-4 mb-3"><label class="form-label">SerialNumber</label><InputText @bind-Value="_form.SerialNumber" class="form-control form-control-sm" /></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-3 mb-3"><label class="form-label">HardwareRevision</label><InputText @bind-Value="_form.HardwareRevision" class="form-control form-control-sm" /></div>
|
||||
<div class="col-md-3 mb-3"><label class="form-label">SoftwareRevision</label><InputText @bind-Value="_form.SoftwareRevision" class="form-control form-control-sm" /></div>
|
||||
<div class="col-md-3 mb-3"><label class="form-label">Year of construction</label><InputNumber @bind-Value="_form.YearOfConstruction" class="form-control form-control-sm" /></div>
|
||||
<div class="col-md-3 mb-3"><label class="form-label">AssetLocation</label><InputText @bind-Value="_form.AssetLocation" class="form-control form-control-sm" /></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3"><label class="form-label">ManufacturerUri</label><InputText @bind-Value="_form.ManufacturerUri" class="form-control form-control-sm mono" /></div>
|
||||
<div class="col-md-6 mb-3"><label class="form-label">DeviceManualUri</label><InputText @bind-Value="_form.DeviceManualUri" class="form-control form-control-sm mono" /></div>
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(_error))
|
||||
{
|
||||
<div class="text-danger small mt-2">@_error</div>
|
||||
}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" @onclick="CancelAsync" disabled="@_busy">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary" disabled="@_busy">
|
||||
@if (_busy) { <span class="spinner-border spinner-border-sm me-1"></span> }
|
||||
@(IsNew ? "Create" : "Save changes")
|
||||
</button>
|
||||
</div>
|
||||
</EditForm>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
/// <summary>Whether the modal is shown. The host owns this flag.</summary>
|
||||
[Parameter] public bool Visible { get; set; }
|
||||
|
||||
/// <summary><c>true</c> to create a new equipment; <c>false</c> to edit <see cref="Existing"/>.</summary>
|
||||
[Parameter] public bool IsNew { get; set; }
|
||||
|
||||
/// <summary>The parent line id used to default the UNS-line select on create.</summary>
|
||||
[Parameter] public string? UnsLineId { get; set; }
|
||||
|
||||
/// <summary>The equipment being edited, when <see cref="IsNew"/> is <c>false</c>.</summary>
|
||||
[Parameter] public EquipmentEditDto? Existing { get; set; }
|
||||
|
||||
/// <summary>The selectable UNS lines — scoped to the equipment's cluster by the host — as <c>(Id, Display)</c> pairs.</summary>
|
||||
[Parameter] public IReadOnlyList<(string Id, string Display)> Lines { get; set; } = Array.Empty<(string, string)>();
|
||||
|
||||
/// <summary>The selectable drivers — scoped to the equipment's cluster by the host — as <c>(Id, Display)</c> pairs.</summary>
|
||||
[Parameter] public IReadOnlyList<(string Id, string Display)> Drivers { get; set; } = Array.Empty<(string, string)>();
|
||||
|
||||
/// <summary>Raised after a successful create/save so the host can reload and close.</summary>
|
||||
[Parameter] public EventCallback OnSaved { get; set; }
|
||||
|
||||
/// <summary>Raised when the user cancels so the host can close.</summary>
|
||||
[Parameter] public EventCallback OnCancel { get; set; }
|
||||
|
||||
private FormModel _form = new();
|
||||
private bool _busy;
|
||||
private string? _error;
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
// Rebuild the working form whenever the host (re)opens the modal for a fresh target.
|
||||
if (IsNew)
|
||||
{
|
||||
_form = new FormModel { UnsLineId = UnsLineId ?? "" };
|
||||
}
|
||||
else if (Existing is not null)
|
||||
{
|
||||
_form = new FormModel
|
||||
{
|
||||
Name = Existing.Name,
|
||||
MachineCode = Existing.MachineCode,
|
||||
UnsLineId = Existing.UnsLineId,
|
||||
DriverInstanceId = Existing.DriverInstanceId,
|
||||
ZTag = Existing.ZTag,
|
||||
SAPID = Existing.SAPID,
|
||||
Manufacturer = Existing.Manufacturer,
|
||||
Model = Existing.Model,
|
||||
SerialNumber = Existing.SerialNumber,
|
||||
HardwareRevision = Existing.HardwareRevision,
|
||||
SoftwareRevision = Existing.SoftwareRevision,
|
||||
YearOfConstruction = Existing.YearOfConstruction,
|
||||
AssetLocation = Existing.AssetLocation,
|
||||
ManufacturerUri = Existing.ManufacturerUri,
|
||||
DeviceManualUri = Existing.DeviceManualUri,
|
||||
Enabled = Existing.Enabled,
|
||||
};
|
||||
}
|
||||
_error = null;
|
||||
}
|
||||
|
||||
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(Existing!.EquipmentId, input, Existing.RowVersion);
|
||||
|
||||
if (result.Ok)
|
||||
{
|
||||
await OnSaved.InvokeAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
_error = result.Error;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_busy = false;
|
||||
}
|
||||
}
|
||||
|
||||
private Task CancelAsync() => OnCancel.InvokeAsync();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -13,13 +13,10 @@
|
||||
/// <summary>The top-level UNS nodes to render (typically the enterprise roots).</summary>
|
||||
[Parameter, EditorRequired] public IReadOnlyList<UnsNode> Roots { get; set; } = default!;
|
||||
|
||||
/// <summary>Raised for the primary "add child" action of a node (e.g. "+ Area" on a cluster, "+ Tag" on equipment).</summary>
|
||||
/// <summary>Raised for the primary "add child" action of a structural node (e.g. "+ Area" on a cluster, "+ Equipment" on a line).</summary>
|
||||
[Parameter] public EventCallback<UnsNode> OnAddChild { get; set; }
|
||||
|
||||
/// <summary>Raised for the equipment-only "+ Virtual tag" action, kept distinct from OnAddChild ("+ Tag").</summary>
|
||||
[Parameter] public EventCallback<UnsNode> OnAddVirtualTag { get; set; }
|
||||
|
||||
/// <summary>Raised when the user edits a node (Area/Line/Equipment/Tag/VirtualTag).</summary>
|
||||
/// <summary>Raised when the user edits a node (Area/Line).</summary>
|
||||
[Parameter] public EventCallback<UnsNode> OnEdit { get; set; }
|
||||
|
||||
/// <summary>Raised when the user deletes a node (Area/Line/Equipment/Tag/VirtualTag).</summary>
|
||||
@@ -117,20 +114,7 @@
|
||||
break;
|
||||
|
||||
case UnsNodeKind.Equipment:
|
||||
<button type="button" class="btn btn-sm btn-link p-0 ms-2"
|
||||
@onclick="@(() => OnAddChild.InvokeAsync(node))">+ Tag</button>
|
||||
<button type="button" class="btn btn-sm btn-link p-0 ms-2"
|
||||
@onclick="@(() => OnAddVirtualTag.InvokeAsync(node))">+ Virtual tag</button>
|
||||
<button type="button" class="btn btn-sm btn-link p-0 ms-2"
|
||||
@onclick="@(() => OnEdit.InvokeAsync(node))">Edit</button>
|
||||
<button type="button" class="btn btn-sm btn-link p-0 ms-2 text-danger"
|
||||
@onclick="@(() => OnDelete.InvokeAsync(node))">Delete</button>
|
||||
break;
|
||||
|
||||
case UnsNodeKind.Tag:
|
||||
case UnsNodeKind.VirtualTag:
|
||||
<button type="button" class="btn btn-sm btn-link p-0 ms-2"
|
||||
@onclick="@(() => OnEdit.InvokeAsync(node))">Edit</button>
|
||||
<a class="btn btn-sm btn-link p-0 ms-2" href="@($"/uns/equipment/{node.EntityId}")">Open</a>
|
||||
<button type="button" class="btn btn-sm btn-link p-0 ms-2 text-danger"
|
||||
@onclick="@(() => OnDelete.InvokeAsync(node))">Delete</button>
|
||||
break;
|
||||
|
||||
@@ -241,7 +241,7 @@ public static class UnsTreeAssembly
|
||||
ClusterId = clusterId,
|
||||
EntityId = equipment.EquipmentId,
|
||||
ChildCount = childCount,
|
||||
HasLazyChildren = childCount > 0,
|
||||
HasLazyChildren = false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ public sealed class UnsTreeAssemblyTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_sets_equipment_child_count_and_lazy_flag()
|
||||
public void Build_sets_equipment_child_count_and_leaf_flag()
|
||||
{
|
||||
var clusters = new[] { new ClusterRow("c1", "zb", "SiteA", "Alpha") };
|
||||
var areas = new[] { new AreaRow("a1", "c1", "AreaOne") };
|
||||
@@ -82,9 +82,12 @@ public sealed class UnsTreeAssemblyTests
|
||||
var tree = UnsTreeAssembly.Build(clusters, areas, lines, equipment);
|
||||
var line = tree.Single().Children.Single().Children.Single().Children.Single();
|
||||
|
||||
// Equipment is a tree leaf that links to its own detail page, so it never carries an
|
||||
// expander — HasLazyChildren is always false regardless of its tag/virtual-tag count.
|
||||
// The ChildCount badge still reflects tags + virtual tags.
|
||||
var withChildren = line.Children.Single(e => e.Key == "eq:e1");
|
||||
withChildren.ChildCount.ShouldBe(3);
|
||||
withChildren.HasLazyChildren.ShouldBeTrue();
|
||||
withChildren.HasLazyChildren.ShouldBeFalse();
|
||||
|
||||
var empty = line.Children.Single(e => e.Key == "eq:e2");
|
||||
empty.ChildCount.ShouldBe(0);
|
||||
|
||||
@@ -54,8 +54,8 @@ public sealed class UnsTreeServiceStructureTests
|
||||
equipment.ClusterId.ShouldBe(UnsTreeTestDb.PopulatedClusterId);
|
||||
}
|
||||
|
||||
/// <summary>The seeded equipment node's badge count equals tags + virtual tags and it is
|
||||
/// flagged as lazily expandable.</summary>
|
||||
/// <summary>The seeded equipment node's badge count equals tags + virtual tags. Equipment is a
|
||||
/// tree leaf (no expander), so HasLazyChildren is always false.</summary>
|
||||
[Fact]
|
||||
public async Task LoadStructure_counts_tags_and_vtags_per_equipment()
|
||||
{
|
||||
@@ -72,7 +72,7 @@ public sealed class UnsTreeServiceStructureTests
|
||||
|
||||
// Seed: 2 driver tags + 1 virtual tag (the orphan tag has no equipment and is excluded).
|
||||
equipment.ChildCount.ShouldBe(3);
|
||||
equipment.HasLazyChildren.ShouldBeTrue();
|
||||
equipment.HasLazyChildren.ShouldBeFalse();
|
||||
}
|
||||
|
||||
/// <summary>An empty cluster (no areas) is still rendered as a Cluster node with no children.</summary>
|
||||
|
||||
Reference in New Issue
Block a user