From 1b98d37919d51bdf6eaf6ce6914b40591a1b1792 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Tue, 12 May 2026 04:22:58 -0400 Subject: [PATCH] refactor(ui/design): replace JSON inputs with structured editors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two new shared components in Components/Shared: - ParameterListEditor: table of rows (name + type + item type + required + remove) - ReturnTypeEditor: single type (+ item type when List) Both round-trip the same JSON shape already stored on the entity: parameters: [{"name":"x","type":"String","required":true},...] return: {"type":"List","itemType":"Integer"} | null Type set follows the Inbound API validator (Boolean, Integer, Float, String, Object, List). Legacy values normalize on read — Int32 / int64 / Double / Decimal / lowercase string / etc all coalesce to the new set so existing rows render correctly. Re-saving persists the normalized form. Applied to: - SharedScriptForm - TemplateEdit Add Script form (also surfaces ParameterDefinitions + ReturnDefinition which the entity supported but the form was never wiring through) - ApiMethodForm Graceful degradation: invalid JSON is shown with a "Start fresh" escape hatch instead of crashing the form. --- .../Pages/Design/ApiMethodForm.razor | 10 +- .../Pages/Design/SharedScriptForm.razor | 24 +-- .../Pages/Design/TemplateEdit.razor | 14 +- .../Shared/ParameterListEditor.razor | 203 ++++++++++++++++++ .../Components/Shared/ReturnTypeEditor.razor | 117 ++++++++++ 5 files changed, 343 insertions(+), 25 deletions(-) create mode 100644 src/ScadaLink.CentralUI/Components/Shared/ParameterListEditor.razor create mode 100644 src/ScadaLink.CentralUI/Components/Shared/ReturnTypeEditor.razor diff --git a/src/ScadaLink.CentralUI/Components/Pages/Design/ApiMethodForm.razor b/src/ScadaLink.CentralUI/Components/Pages/Design/ApiMethodForm.razor index 67b3cfe..7baf35c 100644 --- a/src/ScadaLink.CentralUI/Components/Pages/Design/ApiMethodForm.razor +++ b/src/ScadaLink.CentralUI/Components/Pages/Design/ApiMethodForm.razor @@ -29,14 +29,12 @@
- - + +
- - + +
diff --git a/src/ScadaLink.CentralUI/Components/Pages/Design/SharedScriptForm.razor b/src/ScadaLink.CentralUI/Components/Pages/Design/SharedScriptForm.razor index 83b04e6..cfeb833 100644 --- a/src/ScadaLink.CentralUI/Components/Pages/Design/SharedScriptForm.razor +++ b/src/ScadaLink.CentralUI/Components/Pages/Design/SharedScriptForm.razor @@ -29,25 +29,13 @@
-
- - +
+ +
-
- - +
+ +
diff --git a/src/ScadaLink.CentralUI/Components/Pages/Design/TemplateEdit.razor b/src/ScadaLink.CentralUI/Components/Pages/Design/TemplateEdit.razor index f0fa16e..45b37bb 100644 --- a/src/ScadaLink.CentralUI/Components/Pages/Design/TemplateEdit.razor +++ b/src/ScadaLink.CentralUI/Components/Pages/Design/TemplateEdit.razor @@ -85,6 +85,8 @@ private string _scriptCode = string.Empty; private string? _scriptTriggerType; private string? _scriptTriggerConfig; + private string? _scriptParameters; + private string? _scriptReturn; private bool _scriptIsLocked; private string? _scriptFormError; @@ -615,7 +617,7 @@ {
Scripts
- +
@if (_showScriptForm) @@ -636,6 +638,14 @@
+
+ + +
+
+ + +
@@ -884,6 +894,8 @@ { TriggerType = _scriptTriggerType?.Trim(), TriggerConfiguration = _scriptTriggerConfig?.Trim(), + ParameterDefinitions = _scriptParameters, + ReturnDefinition = _scriptReturn, IsLocked = _scriptIsLocked }; diff --git a/src/ScadaLink.CentralUI/Components/Shared/ParameterListEditor.razor b/src/ScadaLink.CentralUI/Components/Shared/ParameterListEditor.razor new file mode 100644 index 0000000..88a10e7 --- /dev/null +++ b/src/ScadaLink.CentralUI/Components/Shared/ParameterListEditor.razor @@ -0,0 +1,203 @@ +@namespace ScadaLink.CentralUI.Components.Shared +@using System.Text.Json + +@if (_parseError != null) +{ +
+ Could not parse existing parameter JSON: @_parseError + +
+} + +@if (_rows.Count > 0) +{ +
+ + + + + + + + + + + + @foreach (var row in _rows) + { + var r = row; + + + + + + + + } + +
NameTypeItem typeRequired
+ + + + + @if (r.Type == "List") + { + + } + else + { + + } + + + + +
+
+} +else if (_parseError == null) +{ +

No parameters defined.

+} + + + +@code { + [Parameter] public string? Json { get; set; } + [Parameter] public EventCallback JsonChanged { get; set; } + + private static readonly string[] Types = { "Boolean", "Integer", "Float", "String", "Object", "List" }; + private static readonly string[] ItemTypes = { "Boolean", "Integer", "Float", "String", "Object" }; + + private List _rows = new(); + private string? _parseError; + private string? _lastSeenJson; + + protected override void OnParametersSet() + { + if (Json != _lastSeenJson) + { + _lastSeenJson = Json; + ParseFromJson(); + } + } + + private void ParseFromJson() + { + _parseError = null; + _rows = new(); + if (string.IsNullOrWhiteSpace(Json)) return; + + try + { + using var doc = JsonDocument.Parse(Json); + if (doc.RootElement.ValueKind != JsonValueKind.Array) + { + _parseError = "Expected a JSON array of parameter objects."; + return; + } + + foreach (var el in doc.RootElement.EnumerateArray()) + { + var name = el.TryGetProperty("name", out var n) ? n.GetString() ?? "" : ""; + var rawType = el.TryGetProperty("type", out var t) ? t.GetString() ?? "String" : "String"; + var rawItem = el.TryGetProperty("itemType", out var it) ? it.GetString() ?? "String" : "String"; + var required = !el.TryGetProperty("required", out var rq) || rq.ValueKind != JsonValueKind.False; + _rows.Add(new ParamRow + { + Name = name, + Type = NormalizeType(rawType), + ItemType = NormalizeType(rawItem), + Required = required + }); + } + } + catch (JsonException ex) + { + _parseError = ex.Message; + } + } + + private static string NormalizeType(string raw) + { + if (string.IsNullOrEmpty(raw)) return "String"; + return raw.ToLowerInvariant() switch + { + "boolean" or "bool" => "Boolean", + "integer" or "int" or "int32" or "int64" or "int16" or "byte" or "sbyte" or "uint32" or "uint64" or "uint16" => "Integer", + "float" or "double" or "single" or "decimal" => "Float", + "string" or "datetime" => "String", + "object" => "Object", + "list" => "List", + _ => raw + }; + } + + private async Task StartFresh() + { + _parseError = null; + _rows = new(); + await Emit(); + } + + private async Task Add() + { + _rows.Add(new ParamRow { Type = "String", ItemType = "String", Required = true }); + await Emit(); + } + + private async Task Remove(ParamRow row) + { + _rows.Remove(row); + await Emit(); + } + + private async Task Emit() + { + var json = SerializeToJson(); + _lastSeenJson = json; + await JsonChanged.InvokeAsync(json); + } + + private string? SerializeToJson() + { + if (_rows.Count == 0) return null; + var list = new List>(); + foreach (var r in _rows) + { + var obj = new Dictionary + { + ["name"] = r.Name, + ["type"] = r.Type, + }; + if (r.Type == "List") obj["itemType"] = r.ItemType; + if (!r.Required) obj["required"] = false; + list.Add(obj); + } + return JsonSerializer.Serialize(list); + } + + private class ParamRow + { + public string Name { get; set; } = ""; + public string Type { get; set; } = "String"; + public string ItemType { get; set; } = "String"; + public bool Required { get; set; } = true; + } +} diff --git a/src/ScadaLink.CentralUI/Components/Shared/ReturnTypeEditor.razor b/src/ScadaLink.CentralUI/Components/Shared/ReturnTypeEditor.razor new file mode 100644 index 0000000..1792500 --- /dev/null +++ b/src/ScadaLink.CentralUI/Components/Shared/ReturnTypeEditor.razor @@ -0,0 +1,117 @@ +@namespace ScadaLink.CentralUI.Components.Shared +@using System.Text.Json + +@if (_parseError != null) +{ +
+ Could not parse existing return JSON: @_parseError + +
+} + +
+
+ + +
+ @if (_type == "List") + { +
+ + +
+ } +
+ +@code { + [Parameter] public string? Json { get; set; } + [Parameter] public EventCallback JsonChanged { get; set; } + + private static readonly string[] Types = { "Boolean", "Integer", "Float", "String", "Object", "List" }; + private static readonly string[] ItemTypes = { "Boolean", "Integer", "Float", "String", "Object" }; + + private string _type = ""; + private string _itemType = "String"; + private string? _parseError; + private string? _lastSeenJson; + + protected override void OnParametersSet() + { + if (Json != _lastSeenJson) + { + _lastSeenJson = Json; + ParseFromJson(); + } + } + + private void ParseFromJson() + { + _parseError = null; + _type = ""; + _itemType = "String"; + if (string.IsNullOrWhiteSpace(Json)) return; + + try + { + using var doc = JsonDocument.Parse(Json); + if (doc.RootElement.ValueKind != JsonValueKind.Object) + { + _parseError = "Expected a JSON object with a type field."; + return; + } + _type = doc.RootElement.TryGetProperty("type", out var t) ? NormalizeType(t.GetString() ?? "") : ""; + _itemType = doc.RootElement.TryGetProperty("itemType", out var it) ? NormalizeType(it.GetString() ?? "String") : "String"; + } + catch (JsonException ex) + { + _parseError = ex.Message; + } + } + + private static string NormalizeType(string raw) + { + if (string.IsNullOrEmpty(raw)) return ""; + return raw.ToLowerInvariant() switch + { + "boolean" or "bool" => "Boolean", + "integer" or "int" or "int32" or "int64" or "int16" or "byte" or "sbyte" or "uint32" or "uint64" or "uint16" => "Integer", + "float" or "double" or "single" or "decimal" => "Float", + "string" or "datetime" => "String", + "object" => "Object", + "list" => "List", + _ => raw + }; + } + + private async Task StartFresh() + { + _parseError = null; + _type = ""; + _itemType = "String"; + await Emit(); + } + + private async Task Emit() + { + string? json = null; + if (!string.IsNullOrEmpty(_type)) + { + var obj = new Dictionary { ["type"] = _type }; + if (_type == "List") obj["itemType"] = _itemType; + json = JsonSerializer.Serialize(obj); + } + _lastSeenJson = json; + await JsonChanged.InvokeAsync(json); + } +}