Files
scadalink-design/docs/plans/2026-05-12-script-schema-editor.md
Joseph Doherty 783da8e21a 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.
2026-05-13 00:33:00 -04:00

7.8 KiB

Script parameter / return: JSON Schema + JSONJoy editor

Date: 2026-05-12 Status: Superseded — see "Reversal: native Blazor SchemaBuilder" below.

Decision

Replace the custom ParameterListEditor / ReturnTypeEditor Blazor components with jsonjoy-builder (SchemaVisualEditor), embedded as a React island. The on-disk format for TemplateScript.ParameterDefinitions and TemplateScript.ReturnDefinition changes from the project-local flat shape ([{name,type,required,itemType?}] / {type,itemType?}) to standard JSON Schema.

Rationale

The existing flat shape lacked descriptions, defaults, enums, nested objects, and arrays of structured items. JSON Schema covers all of that, is the industry vocabulary other tooling already speaks (OpenAPI 3.1, function-calling APIs, validators), and jsonjoy-builder is a polished pre-built visual editor for it.

Trade-offs

  • Breaks the no-UI-framework rule for this feature. jsonjoy-builder is React 19 + Radix UI + Tailwind. Accepted: the island is isolated to one modal panel, Tailwind is shipped pre-built (no toolchain shared with the Blazor side), and the visual delta is contained.
  • New build pipeline. A small Vite project under src/ScadaLink.CentralUI/Schema.Editor/ builds a single IIFE bundle into wwwroot/lib/schema-editor/. Output is committed so dotnet build doesn't require Node.
  • Monaco overlap. jsonjoy-builder depends on @monaco-editor/react, which depends on monaco-editor. We already load Monaco globally for the script code editor. The island calls @monaco-editor/react's loader.config({ monaco: window.monaco }) at boot to reuse the same instance — no duplicate Monaco download.

Storage format change

Field Before After
ParameterDefinitions [{name,type,required,itemType?}] {"type":"object","properties":{...},"required":[...]}
ReturnDefinition {type,itemType?} Any JSON Schema (root type describes the returned value)

Per the chosen rollout: one-shot migration rewrites all existing rows on deploy. After the migration, the analysis pipeline reads JSON Schema only — no dual-format support code.

Type mapping (flat → JSON Schema):

Flat type JSON Schema
Boolean {"type":"boolean"}
Integer {"type":"integer"}
Float {"type":"number"}
String {"type":"string"}
Object {"type":"object"}
List of X {"type":"array","items":{"type":<X>}}

required: false ⇒ name omitted from the required array. required: true (default) ⇒ name added to required.

Component layout

src/ScadaLink.CentralUI/Schema.Editor/        ← new Vite project (committed)
  package.json
  vite.config.ts
  tsconfig.json
  src/main.tsx                                ← exposes window.ScadaSchemaEditor
  src/SchemaEditorApp.tsx
  src/index.css
  .gitignore                                  ← node_modules only
  dist/                                       ← (Vite outputs to wwwroot, not here)

src/ScadaLink.CentralUI/wwwroot/lib/schema-editor/
  schema-editor.js                            ← built IIFE, committed
  schema-editor.css

src/ScadaLink.CentralUI/Components/Shared/
  SchemaEditor.razor                          ← Blazor wrapper; mirrors MonacoEditor.razor

src/ScadaLink.CentralUI/ScriptAnalysis/
  ScriptShapeParser.cs                        ← rewrite to read JSON Schema
src/ScadaLink.CentralUI/Components/Shared/
  ScriptParameterNames.cs                     ← rewrite to read JSON Schema

Removed after rollout: ParameterListEditor.razor, ReturnTypeEditor.razor.

JS interop contract

window.ScadaSchemaEditor = {
  mount(id: string, host: HTMLElement, options: {
    value: string;          // current schema JSON (may be empty)
    mode: 'parameters' | 'return';
    readOnly?: boolean;
  }, dotNetRef: { invokeMethodAsync(name: 'OnValueChanged', json: string): Promise<void> }): void;
  setValue(id: string, value: string): void;
  dispose(id: string): void;
}

Migration

EF Core migration in ScadaLink.ConfigurationDatabase reads TemplateScripts.ParameterDefinitions and ReturnDefinition from every row, sniffs format (array vs object), translates if legacy, writes back. Idempotent: re-running a row already in JSON Schema is a no-op. Runs once at deploy via the existing auto-apply path.

Out of scope (deferred)

  • Schema-driven value-entry forms (e.g. Inbound API tester) — would also use jsonjoy-builder's value-editor mode, but no caller surface needs it today.
  • Hover/completion enhancements derived from JSON Schema descriptions or defaults. Today's pipeline only needs name + type + required.
  • Reuse of JSON Schema $ref across templates — could be a future template-level schema library.

Reversal: native Blazor SchemaBuilder (2026-05-12, same day)

JSONJoy worked but felt heavy for the actual data we author here. Specifically:

  • The "Add Field" modal flow is two clicks per parameter where the legacy inline-row editor was zero. For the common 1-3 scalar-param case, a visible modal dialog every time is friction.
  • JSONJoy's value-mode UX is awkward — it always renders an "Add Field" button even when the schema's root type is string / integer / etc., so the Return-type tab is mismatched to the underlying single-value model.
  • React 19 + Radix + Tailwind for one form field is a lot of build pipeline surface to maintain.

Decision: replace JSONJoy with a Bootstrap-only Blazor component (SchemaBuilder.razor) that recurses through its own render methods. Storage format unchanged — still JSON Schema. The migration, parser, and downstream analysis code are untouched.

Scope decisions (from refinement session):

  • Type set: only the six JSON Schema primitives (string · integer · number · boolean · object · array). No date-time / format, no enum / pattern / min/max, no $ref / oneOf / anyOf / allOf, no additionalProperties. Power-user expansion can come later behind a per-row "more options" toggle.
  • No description support per property. The row stays a single horizontal line: name + type + (items: type if array) + required + remove.
  • Nested objects and arrays-of-objects recurse — same editor renders at any depth.

Files added:

  • src/ScadaLink.CentralUI/Components/Shared/SchemaBuilderModel.cs — in-memory SchemaNode / SchemaProperty tree plus pure-static parse / serialize. Round-trips through the canonical JSON Schema text and tolerates legacy flat-array shape as a parse fallback.
  • src/ScadaLink.CentralUI/Components/Shared/SchemaBuilder.razor — recursive renderer driven by Mode="object" (parameter list) or Mode="value" (single value, with object/array falling back to the property editor).
  • tests/ScadaLink.CentralUI.Tests/Shared/SchemaBuilderModelTests.cs — parse / serialize / round-trip / legacy-array coverage.

Files removed:

  • src/ScadaLink.CentralUI/Schema.Editor/ (Vite project, node_modules, etc.)
  • src/ScadaLink.CentralUI/wwwroot/lib/schema-editor/ (built bundle)
  • src/ScadaLink.CentralUI/Components/Shared/SchemaEditor.razor (Blazor wrapper)
  • <script> / <link> references to schema-editor in App.razor
  • <DefaultItemExcludes>Schema.Editor/** from CentralUI csproj

Forms updated: TemplateEdit.razor, SharedScriptForm.razor, ApiMethodForm.razor now use <SchemaBuilder> directly.

The original jsonjoy-builder integration sections above are kept for historical context but no longer reflect what's in the codebase.