783da8e21a
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.
208 lines
7.9 KiB
Plaintext
208 lines
7.9 KiB
Plaintext
@namespace ScadaLink.CentralUI.Components.Shared
|
|
|
|
@* Bootstrap-only JSON Schema editor. Two modes:
|
|
- "object" parameters: edits a top-level object schema (named properties).
|
|
- "value" return type: edits a single value schema; object/array fall back
|
|
to the same property editor as Mode=object.
|
|
Recurses through methods (not nested components) so we stay in one file. *@
|
|
|
|
@if (_root.Type == "object" && Mode == "object")
|
|
{
|
|
@PropertyList(_root, isRoot: true)
|
|
}
|
|
else
|
|
{
|
|
@ValueRoot(_root)
|
|
}
|
|
|
|
@code {
|
|
/// <summary><c>"object"</c> for parameters, <c>"value"</c> for return type.</summary>
|
|
[Parameter] public string Mode { get; set; } = "object";
|
|
|
|
/// <summary>JSON Schema text. Empty/null seeds the mode's default.</summary>
|
|
[Parameter] public string? Value { get; set; }
|
|
|
|
[Parameter] public EventCallback<string?> ValueChanged { get; set; }
|
|
|
|
private SchemaNode _root = new();
|
|
private string? _lastSeenJson;
|
|
private bool _initialized;
|
|
|
|
protected override void OnParametersSet()
|
|
{
|
|
// OnInitialized fires before this on first mount; OnParametersSet runs
|
|
// on every parameter change. Guard against the initial null==null case
|
|
// where the early-exit would skip applying the mode-appropriate default.
|
|
if (_initialized && Value == _lastSeenJson) return;
|
|
_initialized = true;
|
|
_lastSeenJson = Value;
|
|
_root = SchemaBuilderModel.Parse(
|
|
Value,
|
|
Mode == "object" ? SchemaBuilderModel.NewObject() : SchemaBuilderModel.NewValue());
|
|
}
|
|
|
|
private async Task Emit()
|
|
{
|
|
var json = SchemaBuilderModel.Serialize(_root);
|
|
_lastSeenJson = json;
|
|
await ValueChanged.InvokeAsync(json);
|
|
}
|
|
|
|
private async Task OnTypeChange(SchemaNode node)
|
|
{
|
|
if (node.Type == "array" && node.Items == null)
|
|
node.Items = new SchemaNode { Type = "string" };
|
|
await Emit();
|
|
}
|
|
|
|
private async Task AddProperty(SchemaNode parent)
|
|
{
|
|
parent.Properties.Add(new SchemaProperty { Schema = new SchemaNode { Type = "string" } });
|
|
await Emit();
|
|
}
|
|
|
|
private async Task RemoveProperty(SchemaNode parent, SchemaProperty prop)
|
|
{
|
|
parent.Properties.Remove(prop);
|
|
await Emit();
|
|
}
|
|
|
|
// ── Render helpers ─────────────────────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Renders the property list for an object schema node. <paramref name="isRoot"/>
|
|
/// just tweaks the wording on the Add button ("parameter" at root vs "field"
|
|
/// inside a nested object).
|
|
/// </summary>
|
|
private RenderFragment PropertyList(SchemaNode node, bool isRoot = false) => __builder =>
|
|
{
|
|
<div class="border rounded bg-white p-2">
|
|
@if (node.Properties.Count == 0)
|
|
{
|
|
<div class="text-muted small fst-italic px-1 py-2">
|
|
@(isRoot ? "No parameters defined." : "No fields defined.")
|
|
</div>
|
|
}
|
|
@foreach (var prop in node.Properties)
|
|
{
|
|
<div @key="prop.Id" class="border rounded p-2 mb-2 bg-light-subtle">
|
|
@PropertyRow(node, prop)
|
|
@NestedEditor(prop.Schema)
|
|
</div>
|
|
}
|
|
<button type="button" class="btn btn-outline-secondary btn-sm" @onclick="() => AddProperty(node)">
|
|
+ Add @(isRoot ? "parameter" : "field")
|
|
</button>
|
|
</div>
|
|
};
|
|
|
|
/// <summary>
|
|
/// One property's compact horizontal row: name, type, (items type if array),
|
|
/// required toggle, remove button. Nested object / array-of-object editors
|
|
/// render below the row via <see cref="NestedEditor"/>.
|
|
/// </summary>
|
|
private RenderFragment PropertyRow(SchemaNode parent, SchemaProperty prop) => __builder =>
|
|
{
|
|
<div class="d-flex flex-wrap align-items-center gap-2">
|
|
<input type="text" class="form-control form-control-sm"
|
|
style="max-width: 14rem;" placeholder="name"
|
|
@bind="prop.Name" @bind:event="oninput" @bind:after="Emit" />
|
|
|
|
<select class="form-select form-select-sm" style="max-width: 9rem;"
|
|
@bind="prop.Schema.Type" @bind:after="() => OnTypeChange(prop.Schema)">
|
|
@foreach (var t in SchemaBuilderModel.PrimitiveTypes)
|
|
{
|
|
<option value="@t">@t</option>
|
|
}
|
|
</select>
|
|
|
|
@if (prop.Schema.Type == "array")
|
|
{
|
|
<span class="small text-muted">items:</span>
|
|
<select class="form-select form-select-sm" style="max-width: 9rem;"
|
|
@bind="prop.Schema.Items!.Type" @bind:after="() => OnTypeChange(prop.Schema.Items!)">
|
|
@foreach (var t in SchemaBuilderModel.PrimitiveTypes)
|
|
{
|
|
<option value="@t">@t</option>
|
|
}
|
|
</select>
|
|
}
|
|
|
|
<div class="form-check form-check-inline mb-0">
|
|
<input class="form-check-input" type="checkbox" id="req-@prop.Id"
|
|
@bind="prop.Required" @bind:after="Emit" />
|
|
<label class="form-check-label small" for="req-@prop.Id">required</label>
|
|
</div>
|
|
|
|
<button type="button"
|
|
class="btn btn-link btn-sm text-danger p-0 ms-auto"
|
|
title="Remove" aria-label="Remove field"
|
|
@onclick="() => RemoveProperty(parent, prop)">
|
|
<i class="bi bi-x-lg"></i>
|
|
</button>
|
|
</div>
|
|
};
|
|
|
|
/// <summary>
|
|
/// Renders the indented sub-editor for object / array-of-object properties.
|
|
/// No-op for scalar properties.
|
|
/// </summary>
|
|
private RenderFragment NestedEditor(SchemaNode schema) => __builder =>
|
|
{
|
|
if (schema.Type == "object")
|
|
{
|
|
<div class="ms-3 mt-2">
|
|
@PropertyList(schema)
|
|
</div>
|
|
}
|
|
else if (schema.Type == "array" && schema.Items?.Type == "object")
|
|
{
|
|
<div class="ms-3 mt-2">
|
|
<div class="small text-muted mb-1">item properties:</div>
|
|
@PropertyList(schema.Items)
|
|
</div>
|
|
}
|
|
};
|
|
|
|
/// <summary>
|
|
/// Mode=value root: a single type picker. When the user picks <c>object</c>
|
|
/// or <c>array</c> we expose the same nested editors used by Mode=object.
|
|
/// </summary>
|
|
private RenderFragment ValueRoot(SchemaNode node) => __builder =>
|
|
{
|
|
<div class="d-flex flex-wrap align-items-center gap-2 mb-2">
|
|
<label class="form-label mb-0">Return type:</label>
|
|
<select class="form-select form-select-sm" style="max-width: 10rem;"
|
|
@bind="node.Type" @bind:after="() => OnTypeChange(node)">
|
|
@foreach (var t in SchemaBuilderModel.PrimitiveTypes)
|
|
{
|
|
<option value="@t">@t</option>
|
|
}
|
|
</select>
|
|
|
|
@if (node.Type == "array")
|
|
{
|
|
<label class="form-label mb-0 ms-2">Item type:</label>
|
|
<select class="form-select form-select-sm" style="max-width: 10rem;"
|
|
@bind="node.Items!.Type" @bind:after="() => OnTypeChange(node.Items!)">
|
|
@foreach (var t in SchemaBuilderModel.PrimitiveTypes)
|
|
{
|
|
<option value="@t">@t</option>
|
|
}
|
|
</select>
|
|
}
|
|
</div>
|
|
|
|
@if (node.Type == "object")
|
|
{
|
|
<div class="text-muted small mb-1">Properties of return value:</div>
|
|
@PropertyList(node)
|
|
}
|
|
else if (node.Type == "array" && node.Items?.Type == "object")
|
|
{
|
|
<div class="text-muted small mb-1">Item properties:</div>
|
|
@PropertyList(node.Items)
|
|
}
|
|
};
|
|
}
|