diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/EquipmentChildRows.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/EquipmentChildRows.cs new file mode 100644 index 00000000..dc589278 --- /dev/null +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/EquipmentChildRows.cs @@ -0,0 +1,10 @@ +using ZB.MOM.WW.OtOpcUa.Configuration.Enums; + +namespace ZB.MOM.WW.OtOpcUa.AdminUI.Uns; + +/// A tag row for the equipment page's Tags tab table — display columns plus the id used to +/// open the edit modal. RowVersion is re-read fresh on delete (matching the tree's delete path). +public sealed record EquipmentTagRow(string TagId, string Name, string DriverInstanceId, string DataType, TagAccessLevel AccessLevel); + +/// A virtual-tag row for the equipment page's Virtual Tags tab table. +public sealed record EquipmentVirtualTagRow(string VirtualTagId, string Name, string DataType, string ScriptId, bool Enabled); diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/IUnsTreeService.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/IUnsTreeService.cs index 3bff22e0..2758a45f 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/IUnsTreeService.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/IUnsTreeService.cs @@ -127,6 +127,28 @@ public interface IUnsTreeService /// Tag nodes followed by VirtualTag nodes; empty if the equipment has none. Task> LoadEquipmentChildrenAsync(string equipmentId, CancellationToken ct = default); + /// + /// Loads the driver tags bound to a single equipment as flat row projections for the equipment + /// page's Tags tab table, ordered by Name. Each row carries the display columns plus the + /// TagId the table uses to open the edit modal. Reads untracked. Returns an empty list when + /// the equipment has no tags. + /// + /// The equipment whose tags to load. + /// A token to cancel the load. + /// The equipment's tag rows ordered by Name; empty if it has none. + Task> LoadTagsForEquipmentAsync(string equipmentId, CancellationToken ct = default); + + /// + /// Loads the virtual tags scoped to a single equipment as flat row projections for the equipment + /// page's Virtual Tags tab table, ordered by Name. Each row carries the display columns plus the + /// VirtualTagId the table uses to open the edit modal. Reads untracked. Returns an empty list + /// when the equipment has no virtual tags. + /// + /// The equipment whose virtual tags to load. + /// A token to cancel the load. + /// The equipment's virtual-tag rows ordered by Name; empty if it has none. + Task> LoadVirtualTagsForEquipmentAsync(string equipmentId, CancellationToken ct = default); + /// /// Loads a single UNS area projected for editing, or null if it no longer exists. /// Reads untracked and captures the current concurrency token for last-write-wins saves. diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/UnsTreeService.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/UnsTreeService.cs index dd21f0b3..335e1249 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/UnsTreeService.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/UnsTreeService.cs @@ -127,6 +127,28 @@ public sealed class UnsTreeService(IDbContextFactory dbF return result; } + /// + public async Task> LoadTagsForEquipmentAsync(string equipmentId, CancellationToken ct = default) + { + await using var db = await dbFactory.CreateDbContextAsync(ct); + return await db.Tags.AsNoTracking() + .Where(t => t.EquipmentId == equipmentId) + .OrderBy(t => t.Name) + .Select(t => new EquipmentTagRow(t.TagId, t.Name, t.DriverInstanceId, t.DataType, t.AccessLevel)) + .ToListAsync(ct); + } + + /// + public async Task> LoadVirtualTagsForEquipmentAsync(string equipmentId, CancellationToken ct = default) + { + await using var db = await dbFactory.CreateDbContextAsync(ct); + return await db.VirtualTags.AsNoTracking() + .Where(v => v.EquipmentId == equipmentId) + .OrderBy(v => v.Name) + .Select(v => new EquipmentVirtualTagRow(v.VirtualTagId, v.Name, v.DataType, v.ScriptId, v.Enabled)) + .ToListAsync(ct); + } + /// public async Task LoadAreaAsync(string unsAreaId, CancellationToken ct = default) { diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/UnsTreeServiceEquipmentChildRowsTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/UnsTreeServiceEquipmentChildRowsTests.cs new file mode 100644 index 00000000..8a4b4016 --- /dev/null +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/UnsTreeServiceEquipmentChildRowsTests.cs @@ -0,0 +1,42 @@ +using Shouldly; +using Xunit; +using ZB.MOM.WW.OtOpcUa.AdminUI.Uns; + +namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns; + +[Trait("Category", "Unit")] +public sealed class UnsTreeServiceEquipmentChildRowsTests +{ + private static UnsTreeService SeededService() + { + var dbName = $"uns-childrows-{Guid.NewGuid():N}"; + UnsTreeTestDb.SeedNamed(dbName); + return new UnsTreeService(UnsTreeTestDb.Factory(dbName)); + } + + [Fact] + public async Task LoadTagsForEquipment_returns_tags_in_name_order_scoped() + { + var rows = await SeededService().LoadTagsForEquipmentAsync(UnsTreeTestDb.SeededEquipmentId); + rows.Count.ShouldBe(2); // the EquipmentId=null orphan tag is excluded + rows[0].TagId.ShouldBe("TAG-2"); // "running" < "speed" + rows[0].Name.ShouldBe("running"); + rows[0].DataType.ShouldBe("Boolean"); + rows[1].TagId.ShouldBe("TAG-1"); + rows[1].DataType.ShouldBe("Float"); + } + + [Fact] + public async Task LoadVirtualTagsForEquipment_returns_vtags_in_name_order() + { + var rows = await SeededService().LoadVirtualTagsForEquipmentAsync(UnsTreeTestDb.SeededEquipmentId); + rows.Count.ShouldBe(1); + rows[0].VirtualTagId.ShouldBe("VTAG-1"); + rows[0].Name.ShouldBe("computed"); + rows[0].DataType.ShouldBe("Double"); + } + + [Fact] + public async Task LoadTagsForEquipment_empty_for_unknown_equipment() + => (await SeededService().LoadTagsForEquipmentAsync("EQ-NONE")).ShouldBeEmpty(); +}