fix(flow-debugger): method pane takes structured parameters via ParameterValueForm + raw-JSON escape hatch (#37)

The Run-inbound-API-method pane rendered one text box per top-level schema
field and coerced the text on the page; an array/object field went through
JsonNode.Parse and, on failure, was SENT AS A STRING, while an empty box was
silently dropped. A required array-of-object parameter (MesMoveOut WorkOrders)
could not be entered at all — QA runs reached the site with "WorkOrders":[].

- The method pane now renders the same shared ParameterValueForm the script
  pane uses (typed inputs, nested groups, add/remove list item editors,
  $ref:lib: resolution, inline validation), keyed on the method id so a change
  of method remounts it. The page keeps ONE value tree and BuildParameters()
  serializes it as-is; the flat _inputs / Coerce() / SetInput path is deleted.
- Form | Raw JSON toggle: raw mode is a Monaco json editor for the whole
  request body, given the method's root schema for completion, parsed and
  schema-validated (InboundApiSchema.Validate) on every edit with the error
  inline. Entering raw mode seeds the editor from the tree; leaving it
  re-mounts the form over the last valid tree.
- Run is disabled while the form or the raw document is invalid (a required
  field left empty disables Run before the first keystroke).
- localStorage per-method entry stores the value tree; on restore it is
  validated against the schema and dropped if invalid, which also retires the
  pre-#37 flat string-per-field entries.
- ParameterValueForm.ToJsonSchema / JsonElementToObject made internal static
  for page reuse.
- Tests: 7 new bUnit tests (WorkOrders array-of-object round-trip to the
  RunMethodAsync payload, required list-item gating, invalid raw JSON blocks
  Run + shows parse/schema/non-object errors, valid raw JSON sent verbatim and
  survives the switch back to the form, method switch resets, persisted tree
  restored, legacy flat entry dropped); existing method-pane tests moved to
  the form's #param-<field> ids and fill the required field before Run.
  Playwright smoke selector moved to #param-Text.
- Docs: design doc, Component-CentralUI.md entry-points bullet, CLAUDE.md.

CentralUI.Tests 1425/1425 green; Playwright project builds.

Claude-Session: https://claude.ai/code/session_01XPsuy2ZZ5Gbyfpt8PrK5BV
This commit is contained in:
Joseph Doherty
2026-09-10 14:54:21 -04:00
parent 3b1211d79f
commit b5eb08f1a9
7 changed files with 422 additions and 94 deletions
@@ -214,24 +214,53 @@
<div class="col-lg-3">
<div class="card fd-inputs" data-test="flow-inputs">
<div class="card-body">
<h6 class="card-title text-muted">Inputs</h6>
@if (_entry == EntryKind.Method)
{
@foreach (var f in _inputFields)
@* Method entry (#37): the same typed ParameterValueForm the script pane uses —
per-item list editors, nested object groups, inline validation — keyed on
the method so a change of method remounts it; plus a whole-payload raw-JSON
escape hatch to paste the exact body an external caller sends. Both edit
one value tree, and Run stays disabled while it is invalid. *@
<div class="d-flex justify-content-between align-items-center mb-2">
<h6 class="card-title text-muted mb-0">Inputs</h6>
<div class="btn-group btn-group-sm" role="group" aria-label="Input mode">
<button type="button" class="btn @(!_methodRawMode ? "btn-primary" : "btn-outline-secondary")"
data-test="flow-input-mode-form" disabled="@_running" @onclick="() => SetMethodRawMode(false)">Form</button>
<button type="button" class="btn @(_methodRawMode ? "btn-primary" : "btn-outline-secondary")"
data-test="flow-input-mode-raw" disabled="@_running" @onclick="() => SetMethodRawMode(true)">Raw JSON</button>
</div>
</div>
@if (_methodRawMode)
{
var field = f;
<div class="mb-2">
<label class="form-label form-label-sm">
@field.Name
<span class="text-muted">(@field.Schema.Type@(field.Required ? ", required" : ""))</span>
</label>
<input class="form-control form-control-sm" data-test="@("flow-input-" + field.Name)"
value="@InputValue(field.Name)" @oninput="e => SetInput(field.Name, e.Value?.ToString())" disabled="@_running" />
<div data-test="flow-method-raw">
<MonacoEditor Language="json" Height="220px" ShowToolbar="false"
JsonSchema="@MethodRawJsonSchema"
Value="@_methodRawText"
ValueChanged="@(v => SetMethodRawJson(v))" />
@if (_methodRawError is { } rawError)
{
<div class="text-danger small mt-1" data-test="flow-method-raw-error">@rawError</div>
}
else
{
<div class="form-text">The whole request body, validated against the method's parameter definitions.</div>
}
</div>
}
else
{
<div data-test="flow-method-params">
<ParameterValueForm @key="@_selectedMethodId"
ParameterDefinitions="@_methods.FirstOrDefault(m => m.Id == _selectedMethodId)?.ParameterDefinitions"
Values="_methodParams"
ValuesChanged="@(v => OnMethodParamsChanged(v))"
ValidChanged="@(ok => _methodParamsValid = ok)" />
</div>
}
}
else if (SelectedScript is { } script)
{
<h6 class="card-title text-muted">Inputs</h6>
@* A write kind is fired by writing its own trigger attribute — the operator
supplies only the value. Every other kind is called directly with the
parameters its definitions declare. *@