fix: address Copilot review round 18 — empty-string constants, nested branch reset
- TUI missingRequiredFields treats an empty string as supplied when the property's own `const` pins it there. The one-option control cannot produce anything else, so reporting the seeded value as missing made such a branch permanently uncallable. - A nested SchemaForm is reset by the branch-aware key too. Two outer alternatives can both carry the same nested object field, and a nested form left mounted across an outer switch kept displaying the inner branch chosen for the other one while the newly seeded values described another shape. Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
95030cfdb6
commit
36cf58fe6c
4 changed files with 76 additions and 3 deletions
|
|
@ -837,6 +837,24 @@ describe("schemaToForm", () => {
|
|||
expect(missingRequiredFields(nullable, { a: null })).toEqual([]);
|
||||
});
|
||||
|
||||
it("accepts an empty string a const pins the field to", () => {
|
||||
// The one-option control cannot produce anything else, so reporting the
|
||||
// seeded value as missing would make the branch permanently uncallable.
|
||||
const pinnedEmpty = {
|
||||
type: "object",
|
||||
properties: { kind: { type: "string", const: "" } },
|
||||
required: ["kind"],
|
||||
};
|
||||
expect(missingRequiredFields(pinnedEmpty, { kind: "" })).toEqual([]);
|
||||
// An ordinary required string is still missing when left blank.
|
||||
const ordinary = {
|
||||
type: "object",
|
||||
properties: { kind: { type: "string" } },
|
||||
required: ["kind"],
|
||||
};
|
||||
expect(missingRequiredFields(ordinary, { kind: "" })).toEqual(["kind"]);
|
||||
});
|
||||
|
||||
it("checks the root's own required fields when there is no union", () => {
|
||||
const schema = {
|
||||
type: "object",
|
||||
|
|
|
|||
|
|
@ -481,6 +481,12 @@ export function missingRequiredFields(
|
|||
!admitsNull(property)
|
||||
);
|
||||
}
|
||||
return value === undefined || value === "";
|
||||
if (value === "") {
|
||||
// A branch may pin its discriminator to the empty string, and the
|
||||
// one-option control cannot produce anything else — reporting the seeded
|
||||
// value as missing would make that branch permanently uncallable.
|
||||
return constOf(properties[name]) !== "";
|
||||
}
|
||||
return value === undefined;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2215,6 +2215,51 @@ describe("SchemaForm multiline strings (#2042)", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("resets a nested form's own branch when the outer branch changes", async () => {
|
||||
const user = userEvent.setup();
|
||||
const nested: InspectorFormSchema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
config: {
|
||||
type: "object",
|
||||
title: "Config",
|
||||
properties: {},
|
||||
anyOf: [
|
||||
{
|
||||
type: "object",
|
||||
title: "Inner A",
|
||||
properties: { alpha: { type: "string", title: "Alpha" } },
|
||||
},
|
||||
{
|
||||
type: "object",
|
||||
title: "Inner B",
|
||||
properties: { beta: { type: "string", title: "Beta" } },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
anyOf: [
|
||||
{ type: "object", title: "Outer A", properties: { x: {} } },
|
||||
{ type: "object", title: "Outer B", properties: { y: {} } },
|
||||
],
|
||||
};
|
||||
renderWithMantine(
|
||||
<SchemaForm schema={nested} values={{}} onChange={vi.fn()} />,
|
||||
);
|
||||
const [outer, inner] = screen.getAllByRole("textbox", {
|
||||
name: /Variant/,
|
||||
});
|
||||
await user.click(inner!);
|
||||
await user.click(screen.getByRole("option", { name: "Inner B" }));
|
||||
expect(screen.getByRole("textbox", { name: /Beta/ })).toBeTruthy();
|
||||
|
||||
await user.click(outer!);
|
||||
await user.click(screen.getByRole("option", { name: "Outer B" }));
|
||||
// The nested form is still mounted, so only a changed reset key can stop
|
||||
// it displaying a branch the newly seeded values do not describe.
|
||||
expect(screen.getByRole("textbox", { name: /Alpha/ })).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders no picker for a single-branch union but still shows its fields", () => {
|
||||
const schema: InspectorFormSchema = {
|
||||
type: "object",
|
||||
|
|
|
|||
|
|
@ -972,8 +972,12 @@ export function SchemaForm({
|
|||
handleFieldChange(fieldName, nestedValues)
|
||||
}
|
||||
disabled={disabled}
|
||||
// Sub-fields belong to the same entity, so they reset with it.
|
||||
resetKey={resetKey}
|
||||
// Sub-fields belong to the same entity, so they reset with it —
|
||||
// and to the branch it is being edited under, since two outer
|
||||
// alternatives can both carry this field and a nested form left
|
||||
// mounted across the switch would keep displaying the nested
|
||||
// branch chosen for the other one.
|
||||
resetKey={draftKey}
|
||||
// A nested form's invalid draft is the outer form's invalid draft,
|
||||
// so it reports through the same channel under this field's name.
|
||||
onValidityChange={(nestedInvalid) =>
|
||||
|
|
|
|||
Loading…
Reference in a new issue