feat(adminui): B2-WP4 manual tag entry + raw tag modal + Calculation editor

Claude-Session: https://claude.ai/code/session_01LVneM3eh1UtJxEisFXgmox
This commit is contained in:
Joseph Doherty
2026-07-16 03:08:05 -04:00
parent 54ab413396
commit ada552fbec
7 changed files with 1018 additions and 0 deletions
@@ -0,0 +1,76 @@
using System.Text.Json.Nodes;
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors;
/// <summary>Typed working model for a <c>Calculation</c> tag's TagConfig JSON — the calc-driver binding
/// (<c>scriptId</c> + trigger fields). Mirrors the <c>&lt;Driver&gt;TagConfigModel</c> template: camelCase
/// JSON keys, enum-name serialisation, and unknown-key preservation across a load→save. Shape per the
/// Calculation mini-design: <c>{ "scriptId": "…", "changeTriggered": true, "timerIntervalMs": 5000 }</c>.</summary>
public sealed class CalculationTagConfigModel
{
/// <summary>Logical FK to the shared <c>Script</c> entity that computes this tag. Required.</summary>
public string ScriptId { get; set; } = "";
/// <summary>Re-evaluate whenever a declared dependency value changes. Defaults to <c>true</c>.</summary>
public bool ChangeTriggered { get; set; } = true;
/// <summary>Optional periodic re-evaluation interval in milliseconds; <c>null</c> ⇒ change-trigger only.
/// When set it must be ≥ 50 ms (enforced by <see cref="Validate"/>). An absent key stays absent across
/// a load→save (so it never leaks a phantom timer).</summary>
public int? TimerIntervalMs { get; set; }
private JsonObject _bag = new();
/// <summary>Loads a model from a TagConfig JSON string, defaulting any absent field and retaining every
/// original key (so fields this editor doesn't expose survive a load→save).</summary>
/// <param name="json">The raw TagConfig JSON string, or <c>null</c> for a new/empty config.</param>
/// <returns>The populated <see cref="CalculationTagConfigModel"/>.</returns>
public static CalculationTagConfigModel FromJson(string? json)
{
var o = TagConfigJson.ParseOrNew(json);
int? timer = null;
if (o.TryGetPropertyValue("timerIntervalMs", out var tn) && tn is JsonValue tv && tv.TryGetValue<int>(out var ti))
{
timer = ti;
}
return new CalculationTagConfigModel
{
ScriptId = TagConfigJson.GetString(o, "scriptId") ?? "",
ChangeTriggered = TagConfigJson.GetBool(o, "changeTriggered", true),
TimerIntervalMs = timer,
_bag = o,
};
}
/// <summary>Serialises this model back to a TagConfig JSON string over the preserved key bag. A blank
/// <see cref="ScriptId"/> and a null <see cref="TimerIntervalMs"/> remove their keys (never persist an
/// empty/phantom value).</summary>
/// <returns>The serialised TagConfig JSON string.</returns>
public string ToJson()
{
TagConfigJson.Set(_bag, "scriptId", string.IsNullOrWhiteSpace(ScriptId) ? null : ScriptId);
TagConfigJson.Set(_bag, "changeTriggered", ChangeTriggered);
TagConfigJson.Set(_bag, "timerIntervalMs", TimerIntervalMs);
return TagConfigJson.Serialize(_bag);
}
/// <summary>Validates the calc binding: a script is required, at least one trigger (change or timer)
/// must be set, and a set timer must be ≥ 50 ms.</summary>
/// <returns>An error message describing the validation failure, or <c>null</c> when the model is valid.</returns>
public string? Validate()
{
if (string.IsNullOrWhiteSpace(ScriptId))
{
return "A Calculation tag requires a script.";
}
if (!ChangeTriggered && TimerIntervalMs is null)
{
return "A Calculation tag needs at least one trigger: change-triggered and/or a timer interval.";
}
if (TimerIntervalMs is { } ms && ms < 50)
{
return "Timer interval must be at least 50 ms.";
}
return null;
}
}
@@ -17,6 +17,7 @@ public static class TagConfigEditorMap
["TwinCat"] = typeof(Components.Shared.Uns.TagEditors.TwinCATTagConfigEditor),
["Focas"] = typeof(Components.Shared.Uns.TagEditors.FocasTagConfigEditor),
["OpcUaClient"] = typeof(Components.Shared.Uns.TagEditors.OpcUaClientTagConfigEditor),
["Calculation"] = typeof(Components.Shared.Uns.TagEditors.CalculationTagConfigEditor),
};
/// <summary>Returns the editor component type for a driver type, or null if none is registered.</summary>
@@ -19,6 +19,7 @@ public static class TagConfigValidator
["TwinCat"] = j => TwinCATTagConfigModel.FromJson(j).Validate(),
["Focas"] = j => FocasTagConfigModel.FromJson(j).Validate(),
["OpcUaClient"] = j => OpcUaClientTagConfigModel.FromJson(j).Validate(),
["Calculation"] = j => CalculationTagConfigModel.FromJson(j).Validate(),
};
/// <summary>