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:
@@ -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><Driver>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>
|
||||
|
||||
Reference in New Issue
Block a user