fix(ui): per-field sub-schema for escape-hatch Monaco editors + dispose guard (#261)
This commit is contained in:
@@ -53,12 +53,6 @@ else
|
||||
private InboundApiSchema? _rootSchema;
|
||||
private string? _parsedFor;
|
||||
|
||||
// The resolved root schema serialized back to a JSON Schema string (with every
|
||||
// {"$ref":"lib:Name"} inlined). Fed to the raw-JSON escape-hatch Monaco editor
|
||||
// so Monaco's built-in JSON language offers schema-driven hover + completion on
|
||||
// the entered JSON. Computed once per parse alongside the resolved schema.
|
||||
private string? _resolvedSchemaJson;
|
||||
|
||||
// Raw text and per-field parse/validation errors keyed by canonical PATH
|
||||
// (e.g. "order.id", "tags[0]") so nested inputs each carry their own state.
|
||||
private readonly Dictionary<string, string> _rawText = new();
|
||||
@@ -76,9 +70,22 @@ else
|
||||
_shapes = ScriptParameterNames.ParseShapes(ParameterDefinitions);
|
||||
_rootSchema = await ParseRootAsync();
|
||||
_topLevelFields = _rootSchema?.Type == "object" ? _rootSchema.Fields : Array.Empty<InboundApiSchemaField>();
|
||||
_resolvedSchemaJson = _rootSchema is null ? null : ToJsonSchema(_rootSchema);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serializes the escape-hatch FIELD's own (resolved) sub-schema to a JSON
|
||||
/// Schema string for that field's Monaco editor — NOT the whole root object
|
||||
/// schema. So a field's raw-JSON editor validates/completes against just that
|
||||
/// field's portion of the type, not the entire object. Any
|
||||
/// <c>{"$ref":"lib:Name"}</c> the parse already inlined is emitted as its
|
||||
/// resolved shape; a node whose ref could NOT be resolved (Type
|
||||
/// <c>"ref"</c>) — or any unknown type — has no derivable sub-schema and
|
||||
/// collapses to a permissive <c>{}</c>, so the editor still validates
|
||||
/// well-formed JSON without false errors. Cheap and on-demand: only the rare
|
||||
/// escape-hatch nodes pay for it, and only at render time.
|
||||
/// </summary>
|
||||
private static string FieldSchemaJson(InboundApiSchema fieldSchema) => ToJsonSchema(fieldSchema);
|
||||
|
||||
/// <summary>
|
||||
/// Serializes a (resolved) <see cref="InboundApiSchema"/> back to a JSON Schema
|
||||
/// string for the escape-hatch Monaco editor. Any <c>{"$ref":"lib:Name"}</c> the
|
||||
@@ -250,7 +257,7 @@ else
|
||||
default: // unresolved $ref / unknown — raw-JSON escape hatch (Monaco).
|
||||
<div class="param-json-editor" data-path="@path">
|
||||
<MonacoEditor Language="json" Height="120px" ShowToolbar="false"
|
||||
JsonSchema="@_resolvedSchemaJson"
|
||||
JsonSchema="@FieldSchemaJson(schema)"
|
||||
Value="@AsRaw(path)"
|
||||
ValueChanged="@(v => SetJson(path, v))" />
|
||||
</div>
|
||||
|
||||
@@ -330,12 +330,16 @@
|
||||
// A json editor with a schema needs an explicitly-URI'd model so the
|
||||
// schema's fileMatch can target exactly this editor.
|
||||
let model = null;
|
||||
let hasSchema = false;
|
||||
if (isJson) {
|
||||
const uri = modelUriFor(id);
|
||||
model = monaco.editor.getModel(uri);
|
||||
if (model) { model.setValue(options.value || ""); }
|
||||
else { model = monaco.editor.createModel(options.value || "", "json", uri); }
|
||||
if (options.jsonSchema) { applyJsonSchema(id, options.jsonSchema); }
|
||||
if (options.jsonSchema && options.jsonSchema.trim().length > 0) {
|
||||
applyJsonSchema(id, options.jsonSchema);
|
||||
hasSchema = true;
|
||||
}
|
||||
}
|
||||
|
||||
const editor = monaco.editor.create(host, {
|
||||
@@ -372,7 +376,7 @@
|
||||
dotNetRef.invokeMethodAsync("OnValueChanged", value).catch(function () {});
|
||||
if (options.language === "csharp") scheduleDiagnostics();
|
||||
});
|
||||
editors[id] = { editor: editor, dotNetRef: dotNetRef, isJson: isJson };
|
||||
editors[id] = { editor: editor, dotNetRef: dotNetRef, isJson: isJson, hasSchema: hasSchema };
|
||||
|
||||
// Run an initial diagnostic pass so existing scripts show their markers.
|
||||
if (language === "csharp") scheduleDiagnostics();
|
||||
@@ -381,6 +385,9 @@
|
||||
function setJsonSchema(id, schemaJson) {
|
||||
const entry = editors[id];
|
||||
if (!entry || !entry.isJson) return;
|
||||
// Track whether this editor currently owns a schema entry in jsonDefaults
|
||||
// so dispose can skip the teardown interop when there is nothing to drop.
|
||||
entry.hasSchema = !!(schemaJson && schemaJson.trim().length > 0);
|
||||
applyJsonSchema(id, schemaJson);
|
||||
}
|
||||
|
||||
@@ -436,7 +443,9 @@
|
||||
if (!entry) return;
|
||||
// Drop this editor's json schema entry and its explicitly-URI'd model so
|
||||
// a disposed json editor leaves nothing behind in the global defaults.
|
||||
if (entry.isJson) {
|
||||
// Skip the schema teardown for a json editor that never had a schema —
|
||||
// there is no entry to remove, so the applyJsonSchema interop is spurious.
|
||||
if (entry.isJson && entry.hasSchema) {
|
||||
try { applyJsonSchema(id, null); } catch (e) {}
|
||||
}
|
||||
try { entry.editor.dispose(); } catch (e) {}
|
||||
|
||||
Reference in New Issue
Block a user