feat(ui): structured editors for script schemas and alarm triggers
Replace raw-JSON text inputs with rich UI: script parameter/return types use a JSON Schema builder (SchemaBuilder + JsonSchemaShapeParser, with a migration to convert existing definitions); alarm trigger config uses a type-aware editor with a flattened attribute picker (AlarmTriggerEditor). AlarmActor gains optional direction (rising/falling/either) on RateOfChange triggers.
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace ScadaLink.CentralUI.ScriptAnalysis;
|
||||
|
||||
/// <summary>
|
||||
/// Translates JSON Schema documents stored in
|
||||
/// <c>TemplateScript.ParameterDefinitions</c> / <c>ReturnDefinition</c> into
|
||||
/// the flat <see cref="ParameterShape"/> / type-name vocabulary used by the
|
||||
/// rest of the script-analysis pipeline (completions, inlay hints, signature
|
||||
/// help, hover).
|
||||
///
|
||||
/// Lenient: malformed JSON yields an empty result, never an exception.
|
||||
///
|
||||
/// Also accepts the legacy pre-migration flat shape
|
||||
/// (<c>[{name,type,required,itemType?}]</c> for parameters,
|
||||
/// <c>{type,itemType?}</c> for return) so partially migrated rows don't crash
|
||||
/// the editor.
|
||||
/// </summary>
|
||||
public static class JsonSchemaShapeParser
|
||||
{
|
||||
public static IReadOnlyList<ParameterShape> ParseParameters(string? json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json)) return Array.Empty<ParameterShape>();
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
return doc.RootElement.ValueKind switch
|
||||
{
|
||||
JsonValueKind.Array => ParseLegacyParameterArray(doc.RootElement),
|
||||
JsonValueKind.Object => ParseJsonSchemaObject(doc.RootElement),
|
||||
_ => Array.Empty<ParameterShape>(),
|
||||
};
|
||||
}
|
||||
catch
|
||||
{
|
||||
return Array.Empty<ParameterShape>();
|
||||
}
|
||||
}
|
||||
|
||||
public static string? ParseReturnType(string? json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json)) return null;
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
if (doc.RootElement.ValueKind != JsonValueKind.Object) return null;
|
||||
return ParseReturnSchema(doc.RootElement);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- JSON Schema branch -------------------------------------------------
|
||||
|
||||
private static IReadOnlyList<ParameterShape> ParseJsonSchemaObject(JsonElement root)
|
||||
{
|
||||
if (!root.TryGetProperty("properties", out var props) || props.ValueKind != JsonValueKind.Object)
|
||||
return Array.Empty<ParameterShape>();
|
||||
|
||||
var requiredSet = new HashSet<string>(StringComparer.Ordinal);
|
||||
if (root.TryGetProperty("required", out var req) && req.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (var item in req.EnumerateArray())
|
||||
{
|
||||
if (item.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
var s = item.GetString();
|
||||
if (!string.IsNullOrEmpty(s)) requiredSet.Add(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var result = new List<ParameterShape>();
|
||||
foreach (var prop in props.EnumerateObject())
|
||||
{
|
||||
var name = prop.Name;
|
||||
if (string.IsNullOrEmpty(name)) continue;
|
||||
var type = MapJsonSchemaType(prop.Value);
|
||||
result.Add(new ParameterShape(name, type, requiredSet.Contains(name)));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static string? ParseReturnSchema(JsonElement schema)
|
||||
{
|
||||
if (!schema.TryGetProperty("type", out var typeEl)) return null;
|
||||
if (typeEl.ValueKind != JsonValueKind.String) return null;
|
||||
var type = typeEl.GetString();
|
||||
if (string.IsNullOrEmpty(type)) return null;
|
||||
|
||||
// Legacy form: `{type:"List", itemType:"Integer"}` (post-migration this
|
||||
// should be `{type:"array", items:{type:"integer"}}`, handled below).
|
||||
if (type.Equals("List", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (schema.TryGetProperty("itemType", out var it) && it.ValueKind == JsonValueKind.String)
|
||||
return $"List<{NormalizeLegacyType(it.GetString())}>";
|
||||
return "List<Object>";
|
||||
}
|
||||
|
||||
if (type.Equals("array", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (schema.TryGetProperty("items", out var items) && items.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
var inner = MapJsonSchemaType(items);
|
||||
return $"List<{inner}>";
|
||||
}
|
||||
return "List<Object>";
|
||||
}
|
||||
|
||||
return MapJsonSchemaTypeName(type);
|
||||
}
|
||||
|
||||
private static string MapJsonSchemaType(JsonElement schema)
|
||||
{
|
||||
if (schema.ValueKind != JsonValueKind.Object) return "Object";
|
||||
if (!schema.TryGetProperty("type", out var typeEl) || typeEl.ValueKind != JsonValueKind.String)
|
||||
return "Object";
|
||||
|
||||
var type = typeEl.GetString() ?? "";
|
||||
if (type.Equals("array", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (schema.TryGetProperty("items", out var items) && items.ValueKind == JsonValueKind.Object)
|
||||
return $"List<{MapJsonSchemaType(items)}>";
|
||||
return "List<Object>";
|
||||
}
|
||||
return MapJsonSchemaTypeName(type);
|
||||
}
|
||||
|
||||
private static string MapJsonSchemaTypeName(string type) => type.ToLowerInvariant() switch
|
||||
{
|
||||
"boolean" => "Boolean",
|
||||
"integer" => "Integer",
|
||||
"number" => "Float",
|
||||
"string" => "String",
|
||||
"object" => "Object",
|
||||
"array" => "List",
|
||||
// Legacy aliases (in case a row's been edited by hand pre-migration):
|
||||
"bool" => "Boolean",
|
||||
"int" or "int32" or "int64" => "Integer",
|
||||
"float" or "double" or "decimal" => "Float",
|
||||
_ => type,
|
||||
};
|
||||
|
||||
// ---- Legacy flat-array branch ------------------------------------------
|
||||
|
||||
private static IReadOnlyList<ParameterShape> ParseLegacyParameterArray(JsonElement root)
|
||||
{
|
||||
var result = new List<ParameterShape>();
|
||||
foreach (var el in root.EnumerateArray())
|
||||
{
|
||||
if (el.ValueKind != JsonValueKind.Object) continue;
|
||||
var name = el.TryGetProperty("name", out var n) ? n.GetString() ?? "" : "";
|
||||
if (string.IsNullOrEmpty(name)) continue;
|
||||
var rawType = el.TryGetProperty("type", out var t) ? t.GetString() ?? "String" : "String";
|
||||
var required = !el.TryGetProperty("required", out var rq) || rq.ValueKind != JsonValueKind.False;
|
||||
result.Add(new ParameterShape(name, NormalizeLegacyType(rawType), required));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static string NormalizeLegacyType(string? raw)
|
||||
{
|
||||
if (string.IsNullOrEmpty(raw)) return "String";
|
||||
return raw.ToLowerInvariant() switch
|
||||
{
|
||||
"boolean" or "bool" => "Boolean",
|
||||
"integer" or "int" or "int32" or "int64" => "Integer",
|
||||
"float" or "double" or "decimal" or "number" => "Float",
|
||||
"string" or "datetime" => "String",
|
||||
"object" => "Object",
|
||||
"list" or "array" => "List",
|
||||
_ => raw,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Scripting;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.CodeAnalysis.Formatting;
|
||||
using Microsoft.CodeAnalysis.Scripting;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
@@ -334,11 +333,13 @@ public class ScriptAnalysisService
|
||||
return new FormatResponse(request.Code);
|
||||
try
|
||||
{
|
||||
using var workspace = new AdhocWorkspace();
|
||||
var tree = CSharpSyntaxTree.ParseText(
|
||||
request.Code,
|
||||
new CSharpParseOptions(LanguageVersion.Latest, kind: SourceCodeKind.Script));
|
||||
var formatted = Formatter.Format(tree.GetRoot(), workspace);
|
||||
// NormalizeWhitespace produces canonical layout (indentation + line
|
||||
// breaks). Formatter.Format alone with an empty workspace only
|
||||
// normalizes inter-token spacing — it won't split crammed lines.
|
||||
var formatted = tree.GetRoot().NormalizeWhitespace(indentation: " ", eol: "\n");
|
||||
return new FormatResponse(formatted.ToFullString());
|
||||
}
|
||||
catch
|
||||
|
||||
@@ -1,59 +1,17 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace ScadaLink.CentralUI.ScriptAnalysis;
|
||||
|
||||
/// <summary>
|
||||
/// Parses the parameter-definitions and return-definition JSON written by
|
||||
/// ParameterListEditor / ReturnTypeEditor into a <see cref="ScriptShape"/>.
|
||||
/// Lenient: malformed JSON yields an empty parameter list, not an exception.
|
||||
/// Parses the parameter-definitions and return-definition JSON Schema written
|
||||
/// by SchemaBuilder into a <see cref="ScriptShape"/>. Delegates to
|
||||
/// <see cref="JsonSchemaShapeParser"/>, which also handles legacy flat-shape
|
||||
/// rows during the transition window.
|
||||
/// </summary>
|
||||
public static class ScriptShapeParser
|
||||
{
|
||||
public static ScriptShape Parse(string name, string? parametersJson, string? returnJson)
|
||||
{
|
||||
var parameters = ParseParameters(parametersJson);
|
||||
var returnType = ParseReturnType(returnJson);
|
||||
var parameters = JsonSchemaShapeParser.ParseParameters(parametersJson);
|
||||
var returnType = JsonSchemaShapeParser.ParseReturnType(returnJson);
|
||||
return new ScriptShape(name, parameters, returnType);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<ParameterShape> ParseParameters(string? json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json)) return Array.Empty<ParameterShape>();
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
if (doc.RootElement.ValueKind != JsonValueKind.Array) return Array.Empty<ParameterShape>();
|
||||
return doc.RootElement.EnumerateArray()
|
||||
.Select(el => new ParameterShape(
|
||||
Name: el.TryGetProperty("name", out var n) ? n.GetString() ?? "" : "",
|
||||
Type: el.TryGetProperty("type", out var t) ? t.GetString() ?? "String" : "String",
|
||||
Required: !el.TryGetProperty("required", out var rq) || rq.ValueKind != JsonValueKind.False))
|
||||
.Where(p => !string.IsNullOrEmpty(p.Name))
|
||||
.ToList();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return Array.Empty<ParameterShape>();
|
||||
}
|
||||
}
|
||||
|
||||
private static string? ParseReturnType(string? json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json)) return null;
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
if (doc.RootElement.ValueKind != JsonValueKind.Object) return null;
|
||||
if (!doc.RootElement.TryGetProperty("type", out var t)) return null;
|
||||
var type = t.GetString();
|
||||
if (string.IsNullOrEmpty(type)) return null;
|
||||
if (type == "List" && doc.RootElement.TryGetProperty("itemType", out var it))
|
||||
return $"List<{it.GetString() ?? "Object"}>";
|
||||
return type;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user