Merge branch 'v2/main' into v2/feat/2018-custom-authorization-params

This commit is contained in:
Cliff Hall 2026-08-15 22:26:42 -04:00 • committed by GitHub
commit 9c4356fb8c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 2457 additions and 260 deletions

View file

@ -2,6 +2,7 @@ import { useState } from "react";
import { describe, it, expect, vi } from "vitest";
import userEvent from "@testing-library/user-event";
import type { InspectorFormSchema } from "../../../utils/jsonUtils";
import { toFormSchema } from "../../../utils/jsonUtils";
import {
fireEvent,
renderWithMantine,
@ -737,7 +738,52 @@ describe("SchemaForm", () => {
expect(lastCall.config).toEqual([1, 2]);
});
it("falls back to passing raw string to onChange when JSON is invalid in JsonInput", async () => {
// The JSON field used to store unparseable text back as the *value*, which
// the next render re-stringified — so each keystroke added a layer of
// escaping (`[` → `"["` → `"\"[\""`). That compounding escape is #1928's
// original symptom, and it lived here rather than in the dispatch. It matters
// more since #2007, whose fix deliberately routes object unions to this
// editor: a fallback nobody can type into is not a fallback.
it("shows an error while the JSON draft does not parse", async () => {
const user = userEvent.setup();
const schema: InspectorFormSchema = {
type: "object",
properties: {
config: { type: "array", title: "Config" },
},
};
function Harness() {
const [values, setValues] = useState<Record<string, unknown>>({});
return (
<SchemaForm schema={schema} values={values} onChange={setValues} />
);
}
renderWithMantine(<Harness />);
const jsonInput = screen.getByLabelText(/Config/) as HTMLTextAreaElement;
await user.type(jsonInput, "[[1,");
// Invalid text yields no value, so without this the field would submit as
// absent while the user is still looking at what they typed.
expect(screen.getByText(/Not valid JSON/)).toBeInTheDocument();
await user.type(jsonInput, "2]");
expect(screen.queryByText(/Not valid JSON/)).not.toBeInTheDocument();
});
it("shows no error for an empty optional field", () => {
const schema: InspectorFormSchema = {
type: "object",
properties: {
config: { type: "array", title: "Config" },
},
};
renderWithMantine(
<SchemaForm schema={schema} values={{}} onChange={vi.fn()} />,
);
expect(screen.queryByText(/Not valid JSON/)).not.toBeInTheDocument();
});
it("reports no value, not raw text, while the JSON is mid-edit", async () => {
const user = userEvent.setup();
const onChange = vi.fn();
const schema: InspectorFormSchema = {
@ -751,9 +797,59 @@ describe("SchemaForm", () => {
);
const jsonInput = screen.getByLabelText(/Config/) as HTMLTextAreaElement;
await user.type(jsonInput, "x");
expect(onChange).toHaveBeenCalled();
const lastCall = onChange.mock.calls[onChange.mock.calls.length - 1][0];
expect(typeof lastCall.config).toBe("string");
expect(onChange).toHaveBeenLastCalledWith({ config: undefined });
});
it("lets an array literal be typed one character at a time", async () => {
const user = userEvent.setup();
const schema: InspectorFormSchema = {
type: "object",
properties: {
config: { type: "array", title: "Config" },
},
};
// Drive the real controlled loop: each onChange feeds straight back in as
// `values`, which is what turned the old handler's raw-text write into a
// compounding re-escape.
function Harness() {
const [values, setValues] = useState<Record<string, unknown>>({});
return (
<SchemaForm schema={schema} values={values} onChange={setValues} />
);
}
renderWithMantine(<Harness />);
const jsonInput = screen.getByLabelText(/Config/) as HTMLTextAreaElement;
// `[` is a userEvent keyboard descriptor, so it is escaped as `[[`.
await user.type(jsonInput, '[[1,"a"]');
// The box shows exactly what was typed — no injected quotes or backslashes.
expect(jsonInput.value).toBe('[1,"a"]');
});
it("keeps an in-progress draft visible instead of rewriting it", async () => {
const user = userEvent.setup();
const schema: InspectorFormSchema = {
type: "object",
properties: {
config: { type: "array", title: "Config" },
},
};
function Harness() {
const [values, setValues] = useState<Record<string, unknown>>({});
return (
<SchemaForm schema={schema} values={values} onChange={setValues} />
);
}
renderWithMantine(<Harness />);
const jsonInput = screen.getByLabelText(/Config/) as HTMLTextAreaElement;
await user.type(jsonInput, "[[1,");
// Unparseable so far, and it must survive the re-render untouched.
expect(jsonInput.value).toBe("[1,");
await user.type(jsonInput, "2]");
expect(jsonInput.value).toBe("[1,2]");
});
it("uses default values when value is undefined", () => {
@ -811,3 +907,380 @@ describe("SchemaForm", () => {
expect(container.firstChild).not.toBeNull();
});
});
// #1928: "optional AND explicitly nullable" (Zod's `.nullish()`) compiles to a
// nullable union rather than a plain type. Before the normalization step these
// matched no branch and fell through to the raw-JSON fallback, where every
// keystroke re-escaped the value.
describe("SchemaForm nullable unions", () => {
it("renders a Select for an anyOf string-enum|null field", async () => {
const user = userEvent.setup();
const onChange = vi.fn();
const schema: InspectorFormSchema = {
type: "object",
properties: {
type: {
title: "Type",
anyOf: [
{ type: "string", enum: ["envio", "recebimento"] },
{ type: "null" },
],
},
},
};
renderWithMantine(
<SchemaForm schema={schema} values={{}} onChange={onChange} />,
);
await user.click(screen.getByRole("textbox", { name: "Type" }));
await user.click(
await screen.findByRole("option", { name: "envio", hidden: true }),
);
expect(onChange).toHaveBeenCalledWith({ type: "envio" });
});
it("clears a nullable enum back to null", async () => {
const user = userEvent.setup();
const onChange = vi.fn();
const schema: InspectorFormSchema = {
type: "object",
properties: {
type: {
title: "Type",
anyOf: [{ type: "string", enum: ["envio"] }, { type: "null" }],
},
},
};
renderWithMantine(
<SchemaForm
schema={schema}
values={{ type: "envio" }}
onChange={onChange}
/>,
);
// Mantine marks its combobox clear button `aria-hidden` (it is mouse-only,
// `tabIndex={-1}`), so it is only reachable with `hidden: true`.
await user.click(screen.getByRole("button", { hidden: true }));
expect(onChange).toHaveBeenCalledWith({ type: null });
});
it("offers no clear affordance on a non-nullable enum", () => {
const schema: InspectorFormSchema = {
type: "object",
properties: {
format: { type: "string", title: "Format", enum: ["json"] },
},
};
renderWithMantine(
<SchemaForm
schema={schema}
values={{ format: "json" }}
onChange={vi.fn()}
/>,
);
expect(
screen.queryByRole("button", { hidden: true }),
).not.toBeInTheDocument();
});
// The other supported nullable encoding: keywords stay at the top level, so
// the null sentinel sits inside the enum list rather than on a branch.
it("renders a Select for a type: [string, null] enum, without a null option", async () => {
const user = userEvent.setup();
const onChange = vi.fn();
// Built through `toFormSchema`, the same narrowing boundary every
// production call site uses, rather than cast into `InspectorFormSchema`.
// A `null` member is valid JSON Schema but outside that type's `string[]`
// `enum`, and this fixture *is* wire data — so the honest way to introduce
// it is the wire→form narrow, not a double cast that erases the mismatch.
const schema = toFormSchema({
type: "object",
properties: {
direction: {
title: "Direction",
type: ["string", "null"],
enum: ["envio", "recebimento", null],
},
},
});
renderWithMantine(
<SchemaForm schema={schema!} values={{}} onChange={onChange} />,
);
await user.click(screen.getByRole("textbox", { name: "Direction" }));
const options = await screen.findAllByRole("option", { hidden: true });
expect(options.map((option) => option.textContent)).toEqual([
"envio",
"recebimento",
]);
await user.click(options[0]);
expect(onChange).toHaveBeenCalledWith({ direction: "envio" });
});
it("renders a TextInput for a type: [string, null] field", async () => {
const user = userEvent.setup();
const onChange = vi.fn();
const schema: InspectorFormSchema = {
type: "object",
properties: {
note: { type: ["string", "null"], title: "Note" },
},
};
renderWithMantine(
<SchemaForm schema={schema} values={{}} onChange={onChange} />,
);
await user.type(screen.getByLabelText(/Note/), "a");
expect(onChange).toHaveBeenCalledWith({ note: "a" });
});
it("renders a checkbox for an anyOf boolean|null field", async () => {
const user = userEvent.setup();
const onChange = vi.fn();
const schema: InspectorFormSchema = {
type: "object",
properties: {
enabled: {
title: "Enabled",
anyOf: [{ type: "boolean" }, { type: "null" }],
},
},
};
renderWithMantine(
<SchemaForm schema={schema} values={{}} onChange={onChange} />,
);
await user.click(screen.getByLabelText("Enabled"));
expect(onChange).toHaveBeenCalledWith({ enabled: true });
});
it("renders a MultiSelect for an anyOf array-of-enum|null field", async () => {
const user = userEvent.setup();
const onChange = vi.fn();
const schema: InspectorFormSchema = {
type: "object",
properties: {
tags: {
title: "Tags",
anyOf: [
{ type: "array", items: { type: "string", enum: ["a", "b"] } },
{ type: "null" },
],
},
},
};
renderWithMantine(
<SchemaForm schema={schema} values={{}} onChange={onChange} />,
);
await user.click(screen.getByRole("textbox", { name: "Tags" }));
await user.click(
await screen.findByRole("option", { name: "a", hidden: true }),
);
expect(onChange).toHaveBeenCalledWith({ tags: ["a"] });
});
it("renders nested fields for an anyOf object|null field", async () => {
const user = userEvent.setup();
const onChange = vi.fn();
const schema: InspectorFormSchema = {
type: "object",
properties: {
profile: {
title: "Profile",
anyOf: [
{
type: "object",
properties: { nick: { type: "string", title: "Nick" } },
},
{ type: "null" },
],
},
},
};
renderWithMantine(
<SchemaForm schema={schema} values={{}} onChange={onChange} />,
);
await user.type(screen.getByLabelText(/Nick/), "z");
expect(onChange).toHaveBeenCalledWith({ profile: { nick: "z" } });
});
// #2007: `z.array(z.union([z.object(…), z.object(…)]))` gives an `items.anyOf`
// whose branches carry no top-level `const`, so every MultiSelect option was
// the empty string — and Mantine *throws* on duplicate option values, greying
// out the whole tool panel. The nullable form below is reachable only because
// this PR now collapses it into `type: "array"`, so it has to be safe too.
it("falls back to the JSON input for an array of object-union items", () => {
const objectBranch = (name: string): InspectorFormSchema => ({
type: "object",
properties: { type: { type: "string", const: name } },
required: ["type"],
});
const schema: InspectorFormSchema = {
type: "object",
properties: {
items: {
title: "Items",
type: "array",
items: { anyOf: [objectBranch("A"), objectBranch("B")] },
},
},
};
renderWithMantine(
<SchemaForm schema={schema} values={{}} onChange={vi.fn()} />,
);
expect(screen.getByLabelText(/Items/).tagName).toBe("TEXTAREA");
});
it("falls back to the JSON input for a nullable array of object-union items", () => {
const objectBranch = (name: string): InspectorFormSchema => ({
type: "object",
properties: { type: { type: "string", const: name } },
required: ["type"],
});
const schema: InspectorFormSchema = {
type: "object",
properties: {
items: {
title: "Items",
anyOf: [
{ type: "array", items: { anyOf: [objectBranch("A")] } },
{ type: "null" },
],
},
},
};
renderWithMantine(
<SchemaForm schema={schema} values={{}} onChange={vi.fn()} />,
);
expect(screen.getByLabelText(/Items/).tagName).toBe("TEXTAREA");
});
// Mantine's select is string-valued, so a numeric const would submit "1" for
// 1 — the same wrong-type-on-the-wire problem that keeps a numeric enum off
// the select path.
it("falls back to the JSON input for non-string anyOf consts", () => {
const schema: InspectorFormSchema = {
type: "object",
properties: {
items: {
title: "Items",
type: "array",
items: { anyOf: [{ const: 1 }, { const: 2 }] },
},
},
};
renderWithMantine(
<SchemaForm schema={schema} values={{}} onChange={vi.fn()} />,
);
expect(screen.getByLabelText(/Items/).tagName).toBe("TEXTAREA");
});
it("falls back to the JSON input when two anyOf branches share a const", () => {
const schema: InspectorFormSchema = {
type: "object",
properties: {
items: {
title: "Items",
type: "array",
items: { anyOf: [{ const: "dup" }, { const: "dup" }] },
},
},
};
renderWithMantine(
<SchemaForm schema={schema} values={{}} onChange={vi.fn()} />,
);
expect(screen.getByLabelText(/Items/).tagName).toBe("TEXTAREA");
});
it("falls back to a text input for a string oneOf with no consts", () => {
const schema: InspectorFormSchema = {
type: "object",
properties: {
mode: {
title: "Mode",
type: "string",
oneOf: [{ type: "string" }, { type: "string" }],
},
},
};
renderWithMantine(
<SchemaForm schema={schema} values={{}} onChange={vi.fn()} />,
);
expect(screen.getByLabelText(/Mode/).tagName).toBe("INPUT");
});
it("still renders a MultiSelect when every anyOf branch has a distinct const", () => {
const schema: InspectorFormSchema = {
type: "object",
properties: {
items: {
title: "Items",
type: "array",
items: { anyOf: [{ const: "a", title: "Alpha" }, { const: "b" }] },
},
},
};
renderWithMantine(
<SchemaForm schema={schema} values={{}} onChange={vi.fn()} />,
);
expect(screen.getByRole("textbox", { name: "Items" })).toBeInTheDocument();
});
// Reachable only since the collapse landed: the nullable wrapper had no
// top-level `type` before, so it fell to the JSON editor rather than into a
// string-valued MultiSelect that would submit ["1"] for [1].
it("falls back to the JSON input for a nullable array of numeric item enums", () => {
// Wire data again — a numeric `enum` is valid JSON Schema but outside
// `InspectorFormSchema`'s `string[]`, so it comes in through the narrow.
const schema = toFormSchema({
type: "object",
properties: {
levels: {
title: "Levels",
anyOf: [{ type: "array", items: { enum: [1, 2] } }, { type: "null" }],
},
},
});
renderWithMantine(
<SchemaForm schema={schema!} values={{}} onChange={vi.fn()} />,
);
expect(screen.getByLabelText(/Levels/).tagName).toBe("TEXTAREA");
});
// The sibling enum rules null out even though the type list names it, so the
// field must not get a clear button that emits a value the schema rejects.
it("offers no clear button when a sibling enum excludes null", () => {
const schema = toFormSchema({
type: "object",
properties: {
direction: {
title: "Direction",
type: ["string", "null"],
enum: ["envio", "recebimento"],
},
},
});
renderWithMantine(
<SchemaForm
schema={schema!}
values={{ direction: "envio" }}
onChange={vi.fn()}
/>,
);
expect(
screen.queryByRole("button", { hidden: true }),
).not.toBeInTheDocument();
});
it("still falls back to the JSON input for a union of two real types", () => {
const schema: InspectorFormSchema = {
type: "object",
properties: {
mixed: {
title: "Mixed",
anyOf: [{ type: "string" }, { type: "number" }],
},
},
};
renderWithMantine(
<SchemaForm schema={schema} values={{}} onChange={vi.fn()} />,
);
expect(screen.getByLabelText(/Mixed/).tagName).toBe("TEXTAREA");
});
});

View file

@ -11,7 +11,14 @@ import {
import { useState } from "react";
import { ClearButton } from "../../elements/ClearButton/ClearButton";
import { useValueChange } from "../../../hooks/useValueChange";
import type { InspectorFormSchema } from "../../../utils/jsonUtils";
import type {
InspectorFormSchema,
JsonSchemaConst,
} from "../../../utils/jsonUtils";
import {
isStringEnum,
normalizeNullableUnion,
} from "@inspector/core/json/nullableUnion.js";
const FieldLabel = Text.withProps({
fw: 500,
@ -51,6 +58,58 @@ function toEnumData(
return values;
}
/**
* Build `Select`/`MultiSelect` option data from a list of `oneOf`/`anyOf`
* branches that are expected to be constants.
*
* Returns `null` when the branches are not usable as options, which sends the
* field to the JSON fallback instead. Four ways that happens:
*
* - **A non-string `const`.** Mantine's select is string-valued, so
* `anyOf: [{ const: 1 }, { const: 2 }]` would submit `["1"]` where the
* schema says `[1]` — the same wrong-type-on-the-wire problem that keeps a
* numeric `enum` off the select path. An inspector must not misreport what
* it sends, so this stays on the JSON editor, where the value keeps its type.
* - **No `const` at all.** An `anyOf` of *object* schemas — what
* `z.array(z.union([z.object(…), z.object(…)]))` compiles to — has no
* top-level `const` on any branch, so every option would be the empty
* string. Mantine **throws** on duplicate option values, which greys out the
* whole tool panel rather than degrading (#2007).
* - **Duplicate values.** Two branches sharing a `const` throw the same way.
* - **An empty option value**, which Mantine cannot render as selectable.
*
* A union of object shapes has no faithful dropdown anyway, so the JSON editor
* is the honest widget for it rather than a lossy or crashing one.
*/
function toConstOptions(
branches: (InspectorFormSchema | JsonSchemaConst)[],
): { value: string; label: string }[] | null {
const options: { value: string; label: string }[] = [];
const seen = new Set<string>();
for (const branch of branches) {
if (typeof branch.const !== "string") {
return null;
}
const value = branch.const;
if (value === "" || seen.has(value)) {
return null;
}
seen.add(value);
options.push({ value, label: branch.title ?? value });
}
return options.length > 0 ? options : null;
}
/**
* Whether an enum `Select` should offer a clear affordance. Only a schema that
* admits `null` does: clearing emits `null`, and an enum without a null branch
* would reject it. Without this a nullable enum is a one-way door — once a
* value is picked there is no way back to "no answer".
*/
function isClearable(fieldSchema: InspectorFormSchema): boolean {
return fieldSchema.nullable === true;
}
/**
* Interpret whatever Mantine's `NumberInput` reported as the JSON value for the
* field. Anything that is not a finite number becomes `undefined`, which is how
@ -90,6 +149,110 @@ function toNumericValue(raw: string | number): number | undefined {
return Number.isSafeInteger(Math.trunc(parsed)) ? parsed : undefined;
}
/** Parse editor text, reporting `undefined` for anything that is not JSON yet. */
function parseJsonDraft(text: string): unknown {
if (text.trim() === "") {
return undefined;
}
try {
return JSON.parse(text);
} catch {
return undefined;
}
}
/** Structural equality for the draft/value re-sync, via canonical JSON. */
function isSameJson(a: unknown, b: unknown): boolean {
if (a === undefined || b === undefined) {
return a === b;
}
return serializeJson(a) === serializeJson(b);
}
interface SchemaJsonFieldProps {
label: string;
description?: string;
withAsterisk: boolean;
disabled: boolean;
value: unknown;
onChange: (value: unknown) => void;
}
/**
* A `JsonInput` that keeps the text the user is typing, not just the value it
* currently parses to.
*
* This field used to drive the box straight off the parent's value and, when
* the text failed `JSON.parse`, store the **raw text** back as the value — which
* the next render re-`JSON.stringify`d, adding a layer of escaping per
* keystroke. Typing `[` gave `"["`, then `"\"[\""`, and the field was unusable
* within a few characters. That compounding escape is the original #1928
* symptom, and it lived in this fallback rather than in the dispatch.
*
* Routing nullable fields to real widgets removed the common way of *landing*
* here, but it did not fix the editor — and #2007's fix deliberately sends a
* union of object shapes to it, so the editor itself has to be typeable. Hence
* the same split `SchemaNumberInput` uses: the raw text is the source of truth
* for what is *displayed*, while the parent only ever sees parsed JSON.
*
* While the text is mid-edit and does not parse, the parent is told
* `undefined` rather than handed the text. An inspector must not report a value
* it cannot send, and `undefined` is how "no value supplied" is spelled
* everywhere else in this form.
*
* That leaves invalid text and an empty field indistinguishable *to the
* parent*, so the field says so itself via `error`. A **required** field is
* already gated — its value is `undefined`, which `hasMissingRequiredFields`
* treats as missing — but an **optional** one would otherwise be dropped from
* the submission with no indication. Disabling Execute/Open/Submit on an
* invalid optional draft needs a validity channel out of this component and
* into all four callers, which is a wider change than this fix; tracked
* separately.
*/
function SchemaJsonField({
value,
onChange,
...inputProps
}: SchemaJsonFieldProps) {
const [draft, setDraft] = useState(() =>
value === undefined ? "" : serializeJson(value),
);
// Re-sync only when the parent's value genuinely diverges from what the draft
// parses to, which leaves an external reset (a cleared form, a loaded
// example) working while an in-progress `[` — whose parse is `undefined`,
// matching the `undefined` we just emitted — is left alone.
useValueChange(value, (next) => {
if (!isSameJson(parseJsonDraft(draft), next)) {
setDraft(next === undefined ? "" : serializeJson(next));
}
});
// Text that is present but does not parse yields no value, so the field would
// otherwise submit as absent while the user is still looking at what they
// typed. Saying so on the field is what keeps that from being silent; see the
// note on `SchemaJsonField` for the submit-gating half, which is not local to
// this component.
const hasInvalidDraft =
draft.trim() !== "" && parseJsonDraft(draft) === undefined;
return (
<SchemaJsonInput
{...inputProps}
value={draft}
error={
hasInvalidDraft
? "Not valid JSON — this field will be omitted"
: undefined
}
onChange={(text) => {
setDraft(text);
onChange(parseJsonDraft(text));
}}
/>
);
}
interface SchemaNumberInputProps {
label: string;
description?: string;
@ -207,7 +370,13 @@ export function SchemaForm({
onChange({ ...values, [fieldName]: fieldValue });
}
function renderField(fieldName: string, fieldSchema: InspectorFormSchema) {
function renderField(fieldName: string, rawSchema: InspectorFormSchema) {
// Flatten a nullable union (`anyOf: [X, {type:"null"}]`, `type: [X,"null"]`)
// before dispatching. Every branch below tests a single `type` string, so
// without this an "optional and explicitly nullable" field — what Zod's
// `.nullish()` emits — matches nothing and falls through to the raw-JSON
// fallback, which is unusable for a value the user has to type (#1928).
const fieldSchema = normalizeNullableUnion(rawSchema);
const isRequired = requiredFields.includes(fieldName);
const label = fieldSchema.title ?? fieldName;
const description = fieldSchema.description;
@ -223,6 +392,7 @@ export function SchemaForm({
withAsterisk={isRequired}
disabled={disabled}
data={toEnumData(fieldSchema.enum, fieldSchema.enumNames)}
clearable={isClearable(fieldSchema)}
value={(rawValue as string) ?? null}
onChange={(val) => handleFieldChange(fieldName, val)}
/>
@ -230,11 +400,11 @@ export function SchemaForm({
}
// string with oneOf
if (fieldSchema.type === "string" && fieldSchema.oneOf) {
const data = fieldSchema.oneOf.map((item) => ({
value: String(item.const ?? ""),
label: item.title ?? String(item.const ?? ""),
}));
const oneOfData = fieldSchema.oneOf
? toConstOptions(fieldSchema.oneOf)
: null;
if (fieldSchema.type === "string" && oneOfData) {
const data = oneOfData;
return (
<Select
key={fieldName}
@ -243,6 +413,7 @@ export function SchemaForm({
withAsterisk={isRequired}
disabled={disabled}
data={data}
clearable={isClearable(fieldSchema)}
value={(rawValue as string) ?? null}
onChange={(val) => handleFieldChange(fieldName, val)}
/>
@ -312,12 +483,14 @@ export function SchemaForm({
);
}
// array of enum values (multi-select)
if (fieldSchema.type === "array" && fieldSchema.items?.enum) {
const data = toEnumData(
fieldSchema.items.enum,
fieldSchema.items.enumNames,
);
// array of enum values (multi-select). Gated on the members being strings
// for the same reason `toConstOptions` is: `MultiSelect` is string-valued,
// so `items: { enum: [1, 2] }` would submit `["1"]` where the schema says
// `[1]`. Reachable from a nullable array since the collapse landed, which
// is what makes the guard load-bearing rather than theoretical.
const itemsEnum = fieldSchema.items?.enum;
if (fieldSchema.type === "array" && isStringEnum(itemsEnum)) {
const data = toEnumData(itemsEnum, fieldSchema.items?.enumNames);
return (
<MultiSelect
key={fieldName}
@ -333,11 +506,11 @@ export function SchemaForm({
}
// array with items having anyOf
if (fieldSchema.type === "array" && fieldSchema.items?.anyOf) {
const data = fieldSchema.items.anyOf.map((item) => ({
value: String(item.const ?? ""),
label: item.title ?? String(item.const ?? ""),
}));
const itemsAnyOfData = fieldSchema.items?.anyOf
? toConstOptions(fieldSchema.items.anyOf)
: null;
if (fieldSchema.type === "array" && itemsAnyOfData) {
const data = itemsAnyOfData;
return (
<MultiSelect
key={fieldName}
@ -376,20 +549,16 @@ export function SchemaForm({
// fallback: JsonInput for complex schemas
return (
<SchemaJsonInput
key={fieldName}
<SchemaJsonField
// Holds local draft state, so — like the number field — it has to be
// remounted when `resetKey` says the form moved to another entity.
key={resetKey === undefined ? fieldName : `${resetKey}:${fieldName}`}
label={label}
description={description}
withAsterisk={isRequired}
disabled={disabled}
value={rawValue !== undefined ? serializeJson(rawValue) : ""}
onChange={(val) => {
try {
handleFieldChange(fieldName, JSON.parse(val));
} catch {
handleFieldChange(fieldName, val);
}
}}
value={rawValue}
onChange={(val) => handleFieldChange(fieldName, val)}
/>
);
}