diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Uns/TagModal.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Uns/TagModal.razor
index 78a66422..c88e68c6 100644
--- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Uns/TagModal.razor
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Uns/TagModal.razor
@@ -7,6 +7,7 @@
@using System.ComponentModel.DataAnnotations
@using Microsoft.AspNetCore.Components.Forms
@using ZB.MOM.WW.OtOpcUa.AdminUI.Uns
+@using ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors
@using ZB.MOM.WW.OtOpcUa.Configuration.Enums
@inject IUnsTreeService Svc
@@ -81,11 +82,25 @@
-
-
-
Schemaless per driver type — register / address / scaling / byte-order. Validated server-side at deploy.
+
+ @{
+ var editorType = TagConfigEditorMap.Resolve(SelectedDriverType);
+ }
+ @if (string.IsNullOrEmpty(_form.DriverInstanceId))
+ {
+
Pick a driver above to configure this tag.
+ }
+ else if (editorType is not null)
+ {
+
+ }
+ else
+ {
+
+
Schemaless per driver type. Validated server-side at deploy.
+ }
@@ -137,6 +152,16 @@
private bool _busy;
private string? _error;
+ // The DriverType of the currently-selected driver (drives editor dispatch). Null when no driver chosen.
+ private string? SelectedDriverType =>
+ Drivers.FirstOrDefault(d => d.Id == _form.DriverInstanceId).DriverType;
+
+ private IDictionary BuildEditorParameters() => new Dictionary
+ {
+ ["ConfigJson"] = _form.TagConfig,
+ ["ConfigJsonChanged"] = EventCallback.Factory.Create(this, v => _form.TagConfig = v),
+ };
+
protected override void OnParametersSet()
{
// Rebuild the working form whenever the host (re)opens the modal for a fresh target.
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/TagConfigEditorMap.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/TagConfigEditorMap.cs
new file mode 100644
index 00000000..e754f649
--- /dev/null
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/TagConfigEditorMap.cs
@@ -0,0 +1,20 @@
+namespace ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors;
+
+///
+/// Maps a driver's DriverType string to its typed tag-config editor component (mirrors
+/// DriverEditRouter._componentMap). Drivers absent from the map fall back to the generic
+/// raw-JSON textarea in the TagModal.
+///
+public static class TagConfigEditorMap
+{
+ private static readonly IReadOnlyDictionary Map =
+ new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ // Editors registered by later tasks, e.g.:
+ // ["ModbusTcp"] = typeof(Components.Shared.Uns.TagEditors.ModbusTagConfigEditor),
+ };
+
+ /// Returns the editor component type for a driver type, or null if none is registered.
+ public static Type? Resolve(string? driverType)
+ => driverType is not null && Map.TryGetValue(driverType, out var t) ? t : null;
+}
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/TagConfigJson.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/TagConfigJson.cs
new file mode 100644
index 00000000..33e15e73
--- /dev/null
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/TagConfigJson.cs
@@ -0,0 +1,39 @@
+using System.Text.Json;
+using System.Text.Json.Nodes;
+
+namespace ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors;
+
+///
+/// Helpers for per-driver tag-config editors: parse a TagConfig JSON string into a mutable
+/// (preserving every key, so fields the editor doesn't expose survive a
+/// load→save), read typed scalars, and serialise back.
+///
+public static class TagConfigJson
+{
+ /// Parses into a mutable object; returns a fresh empty object on null/blank/malformed/non-object input.
+ public static JsonObject ParseOrNew(string? json)
+ {
+ if (string.IsNullOrWhiteSpace(json)) { return new JsonObject(); }
+ try { return JsonNode.Parse(json) as JsonObject ?? new JsonObject(); }
+ catch (JsonException) { return new JsonObject(); }
+ }
+
+ /// Serialises the object to compact JSON (JsonNode.ToJsonString() defaults to non-indented).
+ public static string Serialize(JsonObject obj) => obj.ToJsonString();
+
+ /// Reads a string value, or null if absent/null/non-string.
+ public static string? GetString(JsonObject o, string name)
+ => o.TryGetPropertyValue(name, out var n) && n is JsonValue v && v.TryGetValue(out var s) ? s : null;
+
+ /// Reads an int value, or if absent/null/non-numeric (incl. object/array nodes).
+ public static int GetInt(JsonObject o, string name, int fallback = 0)
+ => o.TryGetPropertyValue(name, out var n) && n is JsonValue v && v.TryGetValue(out var i) ? i : fallback;
+
+ /// Reads an enum by its serialised name, or if absent/unparseable.
+ public static TEnum GetEnum(JsonObject o, string name, TEnum fallback) where TEnum : struct, Enum
+ => GetString(o, name) is { } s && Enum.TryParse(s, ignoreCase: true, out var v) ? v : fallback;
+
+ /// Sets a string/number/enum-name value (enums via ToString()). Null removes the key.
+ public static void Set(JsonObject o, string name, object? value)
+ => o[name] = value is null ? null : JsonValue.Create(value is Enum e ? e.ToString() : value);
+}
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/TagConfigJsonTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/TagConfigJsonTests.cs
new file mode 100644
index 00000000..87e2f0d9
--- /dev/null
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/TagConfigJsonTests.cs
@@ -0,0 +1,134 @@
+using System.Text.Json.Nodes;
+using Shouldly;
+using ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors;
+using Xunit;
+
+namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns;
+
+public sealed class TagConfigJsonTests
+{
+ private enum Flavor { None, Vanilla, Chocolate }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData(" ")]
+ [InlineData("{not json")]
+ [InlineData("[1,2]")]
+ public void ParseOrNew_returns_empty_object_for_unusable_input(string? json)
+ {
+ var obj = TagConfigJson.ParseOrNew(json);
+
+ obj.ShouldNotBeNull();
+ obj.Count.ShouldBe(0);
+ }
+
+ [Fact]
+ public void ParseOrNew_parses_an_object()
+ {
+ var obj = TagConfigJson.ParseOrNew("""{"foo":"bar"}""");
+
+ obj.Count.ShouldBe(1);
+ TagConfigJson.GetString(obj, "foo").ShouldBe("bar");
+ }
+
+ [Fact]
+ public void RoundTrip_preserves_unknown_keys()
+ {
+ var obj = TagConfigJson.ParseOrNew("""{"foo":"bar","region":"X"}""");
+
+ TagConfigJson.Set(obj, "region", "Y");
+ var json = TagConfigJson.Serialize(obj);
+
+ json.ShouldContain("\"foo\":\"bar\"");
+ json.ShouldContain("\"region\":\"Y\"");
+ }
+
+ [Fact]
+ public void GetString_returns_null_when_absent()
+ {
+ var obj = new JsonObject();
+
+ TagConfigJson.GetString(obj, "missing").ShouldBeNull();
+ }
+
+ [Fact]
+ public void GetInt_reads_an_int()
+ {
+ var obj = TagConfigJson.ParseOrNew("""{"register":40001}""");
+
+ TagConfigJson.GetInt(obj, "register").ShouldBe(40001);
+ }
+
+ [Fact]
+ public void GetInt_falls_back_when_absent()
+ {
+ var obj = new JsonObject();
+
+ TagConfigJson.GetInt(obj, "register", fallback: 7).ShouldBe(7);
+ }
+
+ [Fact]
+ public void GetInt_falls_back_when_value_is_a_non_scalar_node()
+ {
+ // A nested object/array under the key must not throw — GetInt returns the fallback.
+ var obj = TagConfigJson.ParseOrNew("""{"register":{},"other":[1,2]}""");
+
+ TagConfigJson.GetInt(obj, "register", fallback: 7).ShouldBe(7);
+ TagConfigJson.GetInt(obj, "other", fallback: 9).ShouldBe(9);
+ }
+
+ [Fact]
+ public void GetString_returns_null_when_value_is_a_non_string_node()
+ {
+ var obj = TagConfigJson.ParseOrNew("""{"register":40001,"nested":{}}""");
+
+ TagConfigJson.GetString(obj, "register").ShouldBeNull();
+ TagConfigJson.GetString(obj, "nested").ShouldBeNull();
+ }
+
+ [Fact]
+ public void GetEnum_parses_by_name_case_insensitive()
+ {
+ var obj = TagConfigJson.ParseOrNew("""{"flavor":"chocolate"}""");
+
+ TagConfigJson.GetEnum(obj, "flavor", Flavor.None).ShouldBe(Flavor.Chocolate);
+ }
+
+ [Fact]
+ public void GetEnum_falls_back_when_absent()
+ {
+ var obj = new JsonObject();
+
+ TagConfigJson.GetEnum(obj, "flavor", Flavor.Vanilla).ShouldBe(Flavor.Vanilla);
+ }
+
+ [Fact]
+ public void GetEnum_falls_back_on_garbage()
+ {
+ var obj = TagConfigJson.ParseOrNew("""{"flavor":"strawberry"}""");
+
+ TagConfigJson.GetEnum(obj, "flavor", Flavor.Vanilla).ShouldBe(Flavor.Vanilla);
+ }
+
+ [Fact]
+ public void Set_writes_an_enum_as_its_name_string()
+ {
+ var obj = new JsonObject();
+
+ TagConfigJson.Set(obj, "flavor", Flavor.Chocolate);
+ var json = TagConfigJson.Serialize(obj);
+
+ json.ShouldContain($"\"flavor\":\"{Flavor.Chocolate}\"");
+ }
+
+ [Fact]
+ public void Set_with_null_removes_the_key()
+ {
+ var obj = TagConfigJson.ParseOrNew("""{"region":"X"}""");
+
+ TagConfigJson.Set(obj, "region", null);
+
+ TagConfigJson.GetString(obj, "region").ShouldBeNull();
+ }
+}