diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Uns/EquipmentPage.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Uns/EquipmentPage.razor index b1711eeb..34f99e9d 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Uns/EquipmentPage.razor +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Uns/EquipmentPage.razor @@ -78,17 +78,9 @@ else -
- - - - @foreach (var (id, display) in _driverOptions) - { - - } - -
+ @* v3: equipment no longer binds a driver — device I/O is authored in the /raw tree and surfaced + here by reference on the Tags tab. The old "Driver instance" select was removed. *@
@@ -138,38 +130,48 @@ else } else if (_activeTab == "tags") { + @* v3 Batch 3: equipment is reference-only — the Tags tab lists UnsTagReference rows pointing at + raw tags. Datatype/access are inherited (read-only) from the backing raw tag; the display-name + override is the only per-reference editable field. *@
- +
- @if (!string.IsNullOrWhiteSpace(_tagError)) + @if (!string.IsNullOrWhiteSpace(_refError)) { -
@_tagError
+
@_refError
} - @if (_tags is null) + @if (_refs is null) {

Loading…

} - else if (_tags.Count == 0) + else if (_refs.Count == 0) { -

No tags yet.

+

No tag references yet.

} else { - +
- + + + + - @foreach (var t in _tags) + @foreach (var r in _refs) { - - - - - - + + + + + + } @@ -177,9 +179,8 @@ else
NameDriverData typeAccessActions
Effective nameRaw pathData typeAccessDisplay-name overrideActions
@t.Name@t.DriverInstanceId@t.DataType@t.AccessLevel - - +
@r.EffectiveName@r.RawPath@r.DataType@r.AccessLevel + + + +
} - + } else if (_activeTab == "vtags") { @@ -290,15 +291,13 @@ else 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, string DriverConfig)> _tagDriverOptions = Array.Empty<(string, string, string, string)>(); + // --- Tags tab state (v3 reference-only). _refs is null until the tab is first activated (lazy load + + // spinner). _overrideEdits carries the per-row editable display-name override, keyed by reference id. --- + private IReadOnlyList? _refs; + private readonly Dictionary _overrideEdits = new(StringComparer.Ordinal); + private string? _refError; + private bool _refModalVisible; // --- Virtual Tags tab state. _vtags is null until the tab is first activated. --- private IReadOnlyList? _vtags; @@ -329,54 +328,48 @@ else { _activeTab = tab; if (IsNew) { return; } - if (tab == "tags" && _tags is null) { await ReloadTagsAsync(); } + if (tab == "tags" && _refs is null) { await ReloadReferencesAsync(); } else if (tab == "vtags" && _vtags is null) { await ReloadVirtualTagsAsync(); } else if (tab == "alarms" && _alarms is null) { await ReloadAlarmsAsync(); } } - // --- Tags tab handlers (mirror GlobalUns; the owning equipment is fixed = EquipmentId) --- + // --- Tags tab handlers (v3 reference-only; the owning equipment is fixed = EquipmentId) --- - private async Task ReloadTagsAsync() + private async Task ReloadReferencesAsync() { - _tags = await Svc.LoadTagsForEquipmentAsync(EquipmentId!); + _refs = await Svc.LoadReferencesForEquipmentAsync(EquipmentId!); + // Seed the per-row override edit buffer so each row's binds to a live value. + _overrideEdits.Clear(); + foreach (var r in _refs) { _overrideEdits[r.UnsTagReferenceId] = r.DisplayNameOverride; } } - private async Task OpenAddTag() + private void OpenAddReference() { - _tagError = null; - _tagModalIsNew = true; - _tagModalExisting = null; - _tagDriverOptions = await Svc.LoadTagDriversForEquipmentAsync(EquipmentId!); - _tagModalVisible = true; + _refError = null; + _refModalVisible = true; } - private async Task OpenEditTag(string tagId) + private async Task OnReferencesCommittedAsync() { - _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; + _refModalVisible = false; + await ReloadReferencesAsync(); } - private async Task OnTagSavedAsync() + private async Task SaveOverride(EquipmentReferenceRow r) { - _tagModalVisible = false; - await ReloadTagsAsync(); + _refError = null; + var edited = _overrideEdits.GetValueOrDefault(r.UnsTagReferenceId); + var res = await Svc.SetReferenceOverrideAsync(r.UnsTagReferenceId, edited, r.RowVersion); + if (res.Ok) { await ReloadReferencesAsync(); } + else { _refError = res.Error; } } - private async Task DeleteTag(string tagId) + private async Task RemoveReference(EquipmentReferenceRow r) { - _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; } + _refError = null; + var res = await Svc.RemoveReferenceAsync(r.UnsTagReferenceId, r.RowVersion); + if (res.Ok) { await ReloadReferencesAsync(); } + else { _refError = res.Error; } } // --- Virtual Tags tab handlers --- @@ -478,7 +471,7 @@ else // 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; + _refs = null; _vtags = null; _alarms = null; if (!IsNew) @@ -489,7 +482,6 @@ else LoadFormFrom(_equipment); var ctx = await Svc.LoadEquipmentPickContextAsync(_equipment.UnsLineId); _lineOptions = ctx.Lines; - _driverOptions = ctx.Drivers; } } else @@ -497,7 +489,6 @@ else _form = new FormModel { UnsLineId = LineId ?? "" }; var ctx = await Svc.LoadEquipmentPickContextAsync(LineId); _lineOptions = ctx.Lines; - _driverOptions = ctx.Drivers; } _loading = false; } @@ -510,7 +501,6 @@ else Name = e.Name, MachineCode = e.MachineCode, UnsLineId = e.UnsLineId, - DriverInstanceId = e.DriverInstanceId, ZTag = e.ZTag, SAPID = e.SAPID, Manufacturer = e.Manufacturer, @@ -536,7 +526,6 @@ else _form.Name, _form.MachineCode, _form.UnsLineId, - _form.DriverInstanceId, _form.ZTag, _form.SAPID, _form.Manufacturer, @@ -582,7 +571,6 @@ else 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; } diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Raw/RawTree.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Raw/RawTree.razor index 6aa815f3..fef10dcd 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Raw/RawTree.razor +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Raw/RawTree.razor @@ -69,6 +69,28 @@ /// [Parameter] public EventCallback OnRootsChanged { get; set; } + // --------------------------------------------------------------------------- + // v3 Batch 3 — raw-tag PICKER mode (opt-in; default off keeps the /raw usage byte-unchanged). + // In picker mode the mutation context-menus are suppressed: Tag leaves render a multi-select + // checkbox, and Device/TagGroup containers offer a "select all tags below" menu. The equipment + // page's "+ Add reference" modal drives this against a single cluster-scoped root. + // --------------------------------------------------------------------------- + + /// When true, render the tree as a raw-tag picker (checkboxes + select-all) instead of + /// the editing tree. Default false — the /raw editing usage is unaffected. + [Parameter] public bool PickerMode { get; set; } + + /// The shared, caller-owned set of selected raw TagIds (picker mode only). The component + /// mutates it in place and raises after every change. + [Parameter] public HashSet? SelectedTagIds { get; set; } + + /// Raised after the selection set changes (picker mode) so the host can update its count / footer. + [Parameter] public EventCallback OnSelectionChanged { get; set; } + + /// Supplies the raw TagIds beneath a Device/TagGroup node for the "select all tags below" + /// affordance (picker mode). The host wires this to IUnsTreeService.LoadDescendantTagIdsAsync. + [Parameter] public Func>>? ResolveDescendantTagIds { get; set; } + // --- Configure-driver modal (edit) --- private bool _driverCfgVisible; private string? _driverCfgId; @@ -198,6 +220,12 @@ private RenderFragment RenderRowBody(RawNode node, bool muted) => __builder => { + @if (PickerMode && node.Kind == RawNodeKind.Tag) + { + + } @node.DisplayName @if (muted) @@ -270,17 +298,67 @@ // named handlers below are the seam Wave B/C replaces with the real modals. // --------------------------------------------------------------------------- - private IReadOnlyList MenuFor(RawNode node) => node.Kind switch + private IReadOnlyList MenuFor(RawNode node) { - RawNodeKind.Cluster => ClusterMenu(node), - RawNodeKind.Folder => FolderMenu(node), - RawNodeKind.Driver => DriverMenu(node), - RawNodeKind.Device => DeviceMenu(node), - RawNodeKind.TagGroup => TagGroupMenu(node), - RawNodeKind.Tag => TagMenu(node), - _ => Array.Empty(), // Enterprise: label only, no menu. + // Picker mode: no mutation menus. Device/TagGroup containers get a "select all tags below" + // affordance; every other kind (and Tag leaves, which carry the checkbox) has no menu. + if (PickerMode) + { + return node.Kind is RawNodeKind.Device or RawNodeKind.TagGroup + ? PickerContainerMenu(node) + : Array.Empty(); + } + + return node.Kind switch + { + RawNodeKind.Cluster => ClusterMenu(node), + RawNodeKind.Folder => FolderMenu(node), + RawNodeKind.Driver => DriverMenu(node), + RawNodeKind.Device => DeviceMenu(node), + RawNodeKind.TagGroup => TagGroupMenu(node), + RawNodeKind.Tag => TagMenu(node), + _ => Array.Empty(), // Enterprise: label only, no menu. + }; + } + + // --- Picker-mode menu + selection helpers --- + + private List PickerContainerMenu(RawNode node) => new() + { + new() { Label = "Select all tags below", Icon = "☑", OnClick = () => SelectAllUnderAsync(node) }, + new() { Label = "Clear all tags below", Icon = "☐", OnClick = () => ClearAllUnderAsync(node) }, }; + private bool IsTagSelected(RawNode node) => + node.EntityId is not null && SelectedTagIds is not null && SelectedTagIds.Contains(node.EntityId); + + private async Task ToggleTagSelection(RawNode node, bool selected) + { + if (SelectedTagIds is null || node.EntityId is null) { return; } + if (selected) { SelectedTagIds.Add(node.EntityId); } + else { SelectedTagIds.Remove(node.EntityId); } + await OnSelectionChanged.InvokeAsync(); + StateHasChanged(); + } + + private async Task SelectAllUnderAsync(RawNode node) + { + if (SelectedTagIds is null || ResolveDescendantTagIds is null) { return; } + var ids = await ResolveDescendantTagIds(node); + foreach (var id in ids) { SelectedTagIds.Add(id); } + await OnSelectionChanged.InvokeAsync(); + StateHasChanged(); + } + + private async Task ClearAllUnderAsync(RawNode node) + { + if (SelectedTagIds is null || ResolveDescendantTagIds is null) { return; } + var ids = await ResolveDescendantTagIds(node); + foreach (var id in ids) { SelectedTagIds.Remove(id); } + await OnSelectionChanged.InvokeAsync(); + StateHasChanged(); + } + private List ClusterMenu(RawNode node) => new() { new() { Label = "New folder", Icon = "📁", OnClick = () => OnNewFolder(node) }, diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Uns/AddReferenceModal.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Uns/AddReferenceModal.razor new file mode 100644 index 00000000..3d05abd8 --- /dev/null +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Uns/AddReferenceModal.razor @@ -0,0 +1,125 @@ +@* 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) +{ + + +} + +@code { + /// Whether the modal is shown. The host owns this flag. + [Parameter] public bool Visible { get; set; } + + /// The equipment the selected raw tags are referenced under. + [Parameter] public string? EquipmentId { get; set; } + + /// Raised after references are committed so the host can reload its Tags list and close. + [Parameter] public EventCallback OnCommitted { get; set; } + + /// Raised when the user cancels so the host can close. + [Parameter] public EventCallback OnCancel { get; set; } + + private IReadOnlyList _roots = Array.Empty(); + private readonly HashSet _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(); + _loading = true; + StateHasChanged(); + + var root = string.IsNullOrEmpty(EquipmentId) + ? null + : await Svc.LoadReferencePickerRootAsync(EquipmentId); + _roots = root is null ? Array.Empty() : new[] { root }; + _loading = false; + } + _wasVisible = Visible; + } + + private Task> 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(); +} diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Uns/ImportEquipmentModal.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Uns/ImportEquipmentModal.razor index e6cf2cf4..759875b4 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Uns/ImportEquipmentModal.razor +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Uns/ImportEquipmentModal.razor @@ -2,9 +2,10 @@ the modal parses it into EquipmentInput rows and calls ImportEquipmentAsync, then shows the Inserted / Skipped / Errors summary in place. The host owns visibility; "Close" raises OnImported so the page can reload the whole tree (an import can add equipment across many lines/clusters). - Required header columns (in order): Name, MachineCode, UnsLineId, DriverInstanceId. + Required header columns (in order): Name, MachineCode, UnsLineId. Optional: ZTag, SAPID, Manufacturer, Model. Existing rows are detected by MachineCode and - skipped (additive-only — no updates), matching the retired /clusters/{id}/equipment/import page. *@ + skipped (additive-only — no updates), matching the retired /clusters/{id}/equipment/import page. + v3: equipment no longer carries a driver, so the old DriverInstanceId column is gone. *@ @using ZB.MOM.WW.OtOpcUa.AdminUI.Uns @inject IUnsTreeService Svc @@ -21,7 +22,7 @@