77bc010ba9
Replace the Batch-1-stubbed authored-tag flow with a UnsTagReference list on the equipment page's Tags tab (v3 reference-only equipment): columns are effective name, computed raw path, inherited datatype/access (read-only), display-name override (editable), and remove. The retired equipment-authored-tag editor (TagModal.razor) is deleted; the VirtualTag and ScriptedAlarm flows are untouched. - "+ Add reference" opens a new AddReferenceModal that reuses RawTree in a new opt-in PickerMode (Tag-leaf checkboxes + Device/TagGroup "select all tags below"), scoped structurally to the equipment's cluster via LoadReferencePickerRootAsync (a single cluster root — cross-cluster tags are unreachable, not merely hidden). Default /raw usage is byte-unchanged (PickerMode defaults false). - UnsTreeService (+ IUnsTreeService) gain the reference mutations: LoadReferencesForEquipmentAsync (computes each RawPath via the shared RawPathResolver), AddReferencesAsync (cluster-checked, all-or-nothing), RemoveReferenceAsync, SetReferenceOverrideAsync, plus the picker helpers LoadReferencePickerRootAsync / LoadDescendantTagIdsAsync. - Consume IEffectiveNameGuard (constructor-injected, no-op fallback when unregistered): AddReferences, SetReferenceOverride, and the VirtualTag + ScriptedAlarm create/update mutations now call CheckAsync before persisting and surface a collision as the failure. WP2 supplies the implementation + DI registration. - Drop the retired equipment↔driver binding: remove DriverInstanceId from EquipmentInput, the ImportEquipmentModal CSV column, the EquipmentPage Details driver select, and the dead decision-#122 driver-cluster guard. Tests: new UnsTreeServiceReferenceTests (add/remove/override, cross-cluster + duplicate rejection, guard-consumed rejection via a fake guard, RawPath projection, picker helpers); Equipment/Import test suites rewritten to drop the retired driver-guard cases. AdminUI suite green (639 passed, 3 skipped); full solution builds 0 errors. Claude-Session: https://claude.ai/code/session_01LVneM3eh1UtJxEisFXgmox
126 lines
5.4 KiB
Plaintext
126 lines
5.4 KiB
Plaintext
@* v3 Batch 3 "+ Add reference" picker: reuses the Raw project tree (RawTree) in picker mode to
|
|
multi-select raw tags, scoped to the equipment's own cluster. The cluster scope is structural —
|
|
IUnsTreeService.LoadReferencePickerRootAsync hands back a SINGLE cluster root, so raw tags of any
|
|
other cluster are simply unreachable through the tree, not merely hidden. On commit the selected
|
|
tag ids are added as UnsTagReference rows via AddReferencesAsync; the host reloads its Tags list. *@
|
|
@using ZB.MOM.WW.OtOpcUa.AdminUI.Uns
|
|
@using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Raw
|
|
@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-lg" role="document">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Add tag references</h5>
|
|
<button type="button" class="btn-close" aria-label="Close" @onclick="CancelAsync"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p class="text-muted small mb-2">
|
|
Pick raw tags to reference under this equipment. The tree is scoped to the
|
|
equipment's cluster. Tick individual tags, or use a device / tag-group's
|
|
<em>“Select all tags below”</em> menu to pull many at once.
|
|
</p>
|
|
|
|
@if (_loading)
|
|
{
|
|
<p class="text-muted"><span class="spinner-border spinner-border-sm me-1"></span>Loading…</p>
|
|
}
|
|
else if (_roots.Count == 0)
|
|
{
|
|
<p class="text-muted">This equipment does not resolve to a cluster, so there are no raw tags to reference.</p>
|
|
}
|
|
else
|
|
{
|
|
<div class="border rounded p-2" style="max-height:50vh; overflow:auto">
|
|
<RawTree Roots="_roots" PickerMode="true" SelectedTagIds="_selected"
|
|
OnSelectionChanged="OnSelectionChanged"
|
|
ResolveDescendantTagIds="ResolveDescendantsAsync" />
|
|
</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="button" class="btn btn-primary" @onclick="CommitAsync" disabled="@(_busy || _selected.Count == 0)">
|
|
@if (_busy) { <span class="spinner-border spinner-border-sm me-1"></span> }
|
|
Add @_selected.Count reference@(_selected.Count == 1 ? "" : "s")
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
@code {
|
|
/// <summary>Whether the modal is shown. The host owns this flag.</summary>
|
|
[Parameter] public bool Visible { get; set; }
|
|
|
|
/// <summary>The equipment the selected raw tags are referenced under.</summary>
|
|
[Parameter] public string? EquipmentId { get; set; }
|
|
|
|
/// <summary>Raised after references are committed so the host can reload its Tags list and close.</summary>
|
|
[Parameter] public EventCallback OnCommitted { get; set; }
|
|
|
|
/// <summary>Raised when the user cancels so the host can close.</summary>
|
|
[Parameter] public EventCallback OnCancel { get; set; }
|
|
|
|
private IReadOnlyList<RawNode> _roots = Array.Empty<RawNode>();
|
|
private readonly HashSet<string> _selected = new(StringComparer.Ordinal);
|
|
private bool _busy;
|
|
private bool _loading;
|
|
private string? _error;
|
|
private bool _wasVisible;
|
|
|
|
protected override async Task OnParametersSetAsync()
|
|
{
|
|
// Reset + reload only on the false→true transition so re-renders while open don't wipe state.
|
|
if (Visible && !_wasVisible)
|
|
{
|
|
_selected.Clear();
|
|
_error = null;
|
|
_roots = Array.Empty<RawNode>();
|
|
_loading = true;
|
|
StateHasChanged();
|
|
|
|
var root = string.IsNullOrEmpty(EquipmentId)
|
|
? null
|
|
: await Svc.LoadReferencePickerRootAsync(EquipmentId);
|
|
_roots = root is null ? Array.Empty<RawNode>() : new[] { root };
|
|
_loading = false;
|
|
}
|
|
_wasVisible = Visible;
|
|
}
|
|
|
|
private Task<IReadOnlyList<string>> ResolveDescendantsAsync(RawNode node) =>
|
|
Svc.LoadDescendantTagIdsAsync(node);
|
|
|
|
// RawTree mutates _selected in place; this just re-renders the footer count.
|
|
private void OnSelectionChanged() => StateHasChanged();
|
|
|
|
private async Task CommitAsync()
|
|
{
|
|
if (_selected.Count == 0) { return; }
|
|
_busy = true;
|
|
_error = null;
|
|
try
|
|
{
|
|
var res = await Svc.AddReferencesAsync(EquipmentId!, _selected.ToList());
|
|
if (res.Ok) { await OnCommitted.InvokeAsync(); }
|
|
else { _error = res.Error; }
|
|
}
|
|
finally
|
|
{
|
|
_busy = false;
|
|
}
|
|
}
|
|
|
|
private Task CancelAsync() => OnCancel.InvokeAsync();
|
|
}
|