diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/AliasTagEditDto.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/AliasTagEditDto.cs
new file mode 100644
index 00000000..3115a57e
--- /dev/null
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/AliasTagEditDto.cs
@@ -0,0 +1,15 @@
+namespace ZB.MOM.WW.OtOpcUa.AdminUI.Uns;
+using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
+
+/// The editable state of a Galaxy alias tag, loaded for the alias edit modal.
+/// is parsed out of the tag's TagConfig {"FullName":…}.
+/// The tag's stable id (read-only on edit).
+/// The tag name.
+/// The bound Galaxy gateway driver id.
+/// The OPC UA built-in type name.
+/// The tag-level access baseline.
+/// The Galaxy object reference parsed from TagConfig; empty string when absent.
+/// The optimistic-concurrency token last read.
+public sealed record AliasTagEditDto(
+ string TagId, string Name, string DriverInstanceId, string DataType,
+ TagAccessLevel AccessLevel, string FullName, byte[] RowVersion);
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 f9934fa5..c47d84e6 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/IUnsTreeService.cs
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/IUnsTreeService.cs
@@ -175,6 +175,12 @@ public interface IUnsTreeService
/// The tag's edit projection, or null when missing.
Task LoadTagAsync(string tagId, CancellationToken ct = default);
+ /// Load a Galaxy alias tag for editing (FullName parsed from TagConfig). Null if not found.
+ /// The alias tag to load.
+ /// A token to cancel the load.
+ /// The alias tag's edit projection, or null when missing.
+ Task LoadAliasTagAsync(string tagId, CancellationToken ct = default);
+
///
/// Loads a single equipment-bound virtual tag 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 55d9a146..25512183 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/UnsTreeService.cs
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/UnsTreeService.cs
@@ -204,6 +204,16 @@ public sealed class UnsTreeService(IDbContextFactory dbF
.FirstOrDefaultAsync(ct);
}
+ ///
+ public async Task LoadAliasTagAsync(string tagId, CancellationToken ct = default)
+ {
+ await using var db = await dbFactory.CreateDbContextAsync(ct);
+ var t = await db.Tags.AsNoTracking().FirstOrDefaultAsync(x => x.TagId == tagId, ct);
+ if (t is null) return null;
+ return new AliasTagEditDto(t.TagId, t.Name, t.DriverInstanceId, t.DataType, t.AccessLevel,
+ ExtractTagConfigFullName(t.TagConfig) ?? string.Empty, t.RowVersion);
+ }
+
///
public async Task LoadVirtualTagAsync(string virtualTagId, CancellationToken ct = default)
{
@@ -975,6 +985,8 @@ public sealed class UnsTreeService(IDbContextFactory dbF
entity.DataType = input.DataType;
entity.AccessLevel = input.AccessLevel;
entity.FolderPath = null;
+ entity.WriteIdempotent = false;
+ entity.PollGroupId = null;
entity.TagConfig = BuildAliasTagConfig(input.FullName);
try
@@ -1397,6 +1409,9 @@ public sealed class UnsTreeService(IDbContextFactory dbF
string? equipmentCluster,
CancellationToken ct)
{
+ if (string.IsNullOrWhiteSpace(driverInstanceId))
+ return new UnsMutationResult(false, "An alias must be bound to a Galaxy gateway.");
+
var driver = await db.DriverInstances.FirstOrDefaultAsync(d => d.DriverInstanceId == driverInstanceId, ct);
if (driver is null)
{
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/UnsTreeServiceAliasTagTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/UnsTreeServiceAliasTagTests.cs
index 37e88769..77924e98 100644
--- a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/UnsTreeServiceAliasTagTests.cs
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/UnsTreeServiceAliasTagTests.cs
@@ -288,6 +288,8 @@ public sealed class UnsTreeServiceAliasTagTests
doc.RootElement.GetProperty("FullName").GetString().ShouldBe("TestMachine_020.Speed");
tag.TagConfig.ShouldContain("FullName");
tag.TagConfig.ShouldContain("TestMachine_020.Speed");
+ tag.WriteIdempotent.ShouldBeFalse();
+ tag.PollGroupId.ShouldBeNull();
}
/// An empty/whitespace Galaxy reference is rejected before anything is written.
@@ -364,6 +366,44 @@ public sealed class UnsTreeServiceAliasTagTests
result.Error!.ShouldContain("speed-alias");
}
+ ///
+ /// returns a DTO whose FullName matches the
+ /// TagConfig and whose RowVersion is non-empty.
+ ///
+ [Fact]
+ public async Task LoadAliasTag_returns_dto_with_FullName()
+ {
+ var dbName = SeedCluster();
+
+ using (var db = UnsTreeTestDb.CreateNamed(dbName))
+ {
+ db.Tags.Add(new Tag
+ {
+ TagId = "TAG-LOAD-ALIAS",
+ DriverInstanceId = GatewayDriverId,
+ EquipmentId = EquipmentId,
+ Name = "loaded-speed",
+ DataType = "Float",
+ AccessLevel = TagAccessLevel.ReadWrite,
+ TagConfig = "{\"FullName\":\"TestMachine_020.LoadedSpeed\"}",
+ });
+ db.SaveChanges();
+ }
+
+ var service = new UnsTreeService(UnsTreeTestDb.Factory(dbName));
+
+ var dto = await service.LoadAliasTagAsync("TAG-LOAD-ALIAS");
+
+ dto.ShouldNotBeNull();
+ dto!.TagId.ShouldBe("TAG-LOAD-ALIAS");
+ dto.Name.ShouldBe("loaded-speed");
+ dto.DriverInstanceId.ShouldBe(GatewayDriverId);
+ dto.DataType.ShouldBe("Float");
+ dto.AccessLevel.ShouldBe(TagAccessLevel.ReadWrite);
+ dto.FullName.ShouldBe("TestMachine_020.LoadedSpeed");
+ dto.RowVersion.ShouldNotBeNull();
+ }
+
///
/// Update changes the alias's name, data type, access level, and Galaxy reference, returning Ok and
/// persisting the new values and the refreshed {"FullName":…} TagConfig.