@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 { /// "object" for parameters, "value" for return type. [Parameter] public string Mode { get; set; } = "object"; /// JSON Schema text. Empty/null seeds the mode's default. [Parameter] public string? Value { get; set; } [Parameter] public EventCallback 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 ───────────────────────────────────────────────────────── /// /// Renders the property list for an object schema node. /// just tweaks the wording on the Add button ("parameter" at root vs "field" /// inside a nested object). /// private RenderFragment PropertyList(SchemaNode node, bool isRoot = false) => __builder => { @if (node.Properties.Count == 0) { @(isRoot ? "No parameters defined." : "No fields defined.") } @foreach (var prop in node.Properties) { @PropertyRow(node, prop) @NestedEditor(prop.Schema) } AddProperty(node)"> + Add @(isRoot ? "parameter" : "field") }; /// /// 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 . /// private RenderFragment PropertyRow(SchemaNode parent, SchemaProperty prop) => __builder => { OnTypeChange(prop.Schema)"> @foreach (var t in SchemaBuilderModel.PrimitiveTypes) { @t } @if (prop.Schema.Type == "array") { items: OnTypeChange(prop.Schema.Items!)"> @foreach (var t in SchemaBuilderModel.PrimitiveTypes) { @t } } required RemoveProperty(parent, prop)"> }; /// /// Renders the indented sub-editor for object / array-of-object properties. /// No-op for scalar properties. /// private RenderFragment NestedEditor(SchemaNode schema) => __builder => { if (schema.Type == "object") { @PropertyList(schema) } else if (schema.Type == "array" && schema.Items?.Type == "object") { item properties: @PropertyList(schema.Items) } }; /// /// Mode=value root: a single type picker. When the user picks object /// or array we expose the same nested editors used by Mode=object. /// private RenderFragment ValueRoot(SchemaNode node) => __builder => { Return type: OnTypeChange(node)"> @foreach (var t in SchemaBuilderModel.PrimitiveTypes) { @t } @if (node.Type == "array") { Item type: OnTypeChange(node.Items!)"> @foreach (var t in SchemaBuilderModel.PrimitiveTypes) { @t } } @if (node.Type == "object") { Properties of return value: @PropertyList(node) } else if (node.Type == "array" && node.Items?.Type == "object") { Item properties: @PropertyList(node.Items) } }; }