diff --git a/clients/web/src/components/groups/SchemaForm/SchemaForm.test.tsx b/clients/web/src/components/groups/SchemaForm/SchemaForm.test.tsx index 575c52d0..0d492714 100644 --- a/clients/web/src/components/groups/SchemaForm/SchemaForm.test.tsx +++ b/clients/web/src/components/groups/SchemaForm/SchemaForm.test.tsx @@ -2291,6 +2291,31 @@ describe("SchemaForm multiline strings (#2042)", () => { expect(onChange).not.toHaveBeenCalled(); }); + it("keeps a cleared root field cleared across a branch switch", async () => { + const user = userEvent.setup(); + const onChange = vi.fn(); + const schema: InspectorFormSchema = { + type: "object", + properties: { count: { type: "number", default: 7, title: "Count" } }, + anyOf: [ + { type: "object", title: "A", properties: { x: {} } }, + { type: "object", title: "B", properties: { y: {} } }, + ], + }; + renderWithMantine( + , + ); + await user.click(screen.getByRole("textbox", { name: /Variant/ })); + await user.click(screen.getByRole("option", { name: "B" })); + expect(onChange).toHaveBeenCalledWith({ count: undefined }); + }); + it("renders no picker for a single-branch union but still shows its fields", () => { const schema: InspectorFormSchema = { type: "object", diff --git a/clients/web/src/components/groups/SchemaForm/SchemaForm.tsx b/clients/web/src/components/groups/SchemaForm/SchemaForm.tsx index 8dfde16f..7cd2b174 100644 --- a/clients/web/src/components/groups/SchemaForm/SchemaForm.tsx +++ b/clients/web/src/components/groups/SchemaForm/SchemaForm.tsx @@ -799,8 +799,11 @@ export function SchemaForm({ Object.entries(nextProperties) .filter( ([name, fieldSchema]) => + // Own-property presence alone, `undefined` included: clearing a + // number or JSON field leaves the name present with no value, and + // that is the user's answer. Dropping it would let the defaults + // below put the field's default back and undo the clear. Object.hasOwn(values, name) && - values[name] !== undefined && fieldSchema.const === undefined && // Carried only where the incoming branch leaves the root's // declaration as it found it. Anything the incoming branch declares