diff --git a/CHANGELOG.md b/CHANGELOG.md index 35119bc0e4..8fdd66f1da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,10 +28,12 @@ should change the heading of the (upcoming) version to include a major version b - [#2069](https://github.com/rjsf-team/react-jsonschema-form/issues/2069) - [#1661](https://github.com/rjsf-team/react-jsonschema-form/issues/1661) - And probably others +- Updated `ObjectField` to deal with additionalProperties with anyOf/oneOf, fixing [#2538](https://github.com/rjsf-team/react-jsonschema-form/issues/2538) # @rjsf/utils - Added new `getClosestMatchingOption()`, `getFirstMatchingOption()` and `sanitizeDataForNewSchema()` schema-based utility functions - Deprecated `getMatchingOption()` and updated all calls to it in other utility functions to use `getFirstMatchingOption()` +- Updated `stubExistingAdditionalProperties` to deal with additionalProperties with anyOf/oneOf, fixing [#2538](https://github.com/rjsf-team/react-jsonschema-form/issues/2538) ## Dev / docs / playground - Updated the playground to `onFormDataEdited()` to only change the formData in the state if the `JSON.stringify()` of the old and new values are different, partially fixing [#3236](https://github.com/rjsf-team/react-jsonschema-form/issues/3236) diff --git a/packages/core/src/components/fields/ObjectField.tsx b/packages/core/src/components/fields/ObjectField.tsx index ffb16f577f..3885a96096 100644 --- a/packages/core/src/components/fields/ObjectField.tsx +++ b/packages/core/src/components/fields/ObjectField.tsx @@ -13,6 +13,8 @@ import { ADDITIONAL_PROPERTY_FLAG, PROPERTIES_KEY, REF_KEY, + ANY_OF_KEY, + ONE_OF_KEY, } from "@rjsf/utils"; import get from "lodash/get"; import has from "lodash/has"; @@ -203,13 +205,17 @@ class ObjectField< let type: RJSFSchema["type"] = undefined; if (isObject(schema.additionalProperties)) { type = schema.additionalProperties.type; - if (REF_KEY in schema.additionalProperties) { + let apSchema = schema.additionalProperties; + if (REF_KEY in apSchema) { const { schemaUtils } = registry; - const refSchema = schemaUtils.retrieveSchema( - { $ref: schema.additionalProperties[REF_KEY] } as S, + apSchema = schemaUtils.retrieveSchema( + { $ref: apSchema[REF_KEY] } as S, formData ); - type = refSchema.type; + type = apSchema.type; + } + if (!type && (ANY_OF_KEY in apSchema || ONE_OF_KEY in apSchema)) { + type = "object"; } } diff --git a/packages/core/test/anyOf_test.js b/packages/core/test/anyOf_test.js index 21b7a82b93..baaad3cac7 100644 --- a/packages/core/test/anyOf_test.js +++ b/packages/core/test/anyOf_test.js @@ -839,6 +839,71 @@ describe("anyOf", () => { expect(options[1].firstChild.nodeValue).eql("Person"); }); + it("should select anyOf in additionalProperties with anyOf", () => { + const schema = { + type: "object", + properties: { + testProperty: { + description: "Any key name, fixed set of possible values", + type: "object", + minProperties: 1, + additionalProperties: { + anyOf: [ + { + title: "my choice 1", + type: "object", + properties: { + prop1: { + description: "prop1 description", + type: "string", + }, + }, + required: ["prop1"], + additionalProperties: false, + }, + { + title: "my choice 2", + type: "object", + properties: { + prop2: { + description: "prop2 description", + type: "string", + }, + }, + required: ["prop2"], + additionalProperties: false, + }, + ], + }, + }, + }, + required: ["testProperty"], + }; + + const { node, onChange } = createFormComponent({ + schema, + formData: { testProperty: { newKey: { prop2: "foo" } } }, + }); + + const $select = node.querySelector( + "select#root_testProperty_newKey__anyof_select" + ); + + expect($select.value).eql("1"); + + Simulate.change($select, { + target: { value: $select.options[0].value }, + }); + + expect($select.value).eql("0"); + + sinon.assert.calledWithMatch(onChange.lastCall, { + formData: { + testProperty: { newKey: { prop1: undefined, prop2: undefined } }, + }, + }); + }); + describe("Arrays", () => { it("should correctly render form inputs for anyOf inside array items", () => { const schema = { diff --git a/packages/core/test/oneOf_test.js b/packages/core/test/oneOf_test.js index f7af74819a..578897e0c3 100644 --- a/packages/core/test/oneOf_test.js +++ b/packages/core/test/oneOf_test.js @@ -665,6 +665,71 @@ describe("oneOf", () => { ); }); + it("should select oneOf in additionalProperties with oneOf", () => { + const schema = { + type: "object", + properties: { + testProperty: { + description: "Any key name, fixed set of possible values", + type: "object", + minProperties: 1, + additionalProperties: { + oneOf: [ + { + title: "my choice 1", + type: "object", + properties: { + prop1: { + description: "prop1 description", + type: "string", + }, + }, + required: ["prop1"], + additionalProperties: false, + }, + { + title: "my choice 2", + type: "object", + properties: { + prop2: { + description: "prop2 description", + type: "string", + }, + }, + required: ["prop2"], + additionalProperties: false, + }, + ], + }, + }, + }, + required: ["testProperty"], + }; + + const { node, onChange } = createFormComponent({ + schema, + formData: { testProperty: { newKey: { prop2: "foo" } } }, + }); + + const $select = node.querySelector( + "select#root_testProperty_newKey__oneof_select" + ); + + expect($select.value).eql("1"); + + Simulate.change($select, { + target: { value: $select.options[0].value }, + }); + + expect($select.value).eql("0"); + + sinon.assert.calledWithMatch(onChange.lastCall, { + formData: { + testProperty: { newKey: { prop1: undefined, prop2: undefined } }, + }, + }); + }); + describe("Arrays", () => { it("should correctly render mixed types for oneOf inside array items", () => { const schema = { diff --git a/packages/utils/src/schema/retrieveSchema.ts b/packages/utils/src/schema/retrieveSchema.ts index 0ba242a417..2868636603 100644 --- a/packages/utils/src/schema/retrieveSchema.ts +++ b/packages/utils/src/schema/retrieveSchema.ts @@ -6,7 +6,9 @@ import { ADDITIONAL_PROPERTIES_KEY, ADDITIONAL_PROPERTY_FLAG, ALL_OF_KEY, + ANY_OF_KEY, DEPENDENCIES_KEY, + ONE_OF_KEY, REF_KEY, } from "../constants"; import findSchemaDefinition, { @@ -205,6 +207,14 @@ export function stubExistingAdditionalProperties< ); } else if ("type" in schema.additionalProperties!) { additionalProperties = { ...schema.additionalProperties }; + } else if ( + ANY_OF_KEY in schema.additionalProperties! || + ONE_OF_KEY in schema.additionalProperties! + ) { + additionalProperties = { + type: "object", + ...schema.additionalProperties, + }; } else { additionalProperties = { type: guessType(get(formData, [key])) }; } diff --git a/packages/utils/test/schema/retrieveSchemaTest.ts b/packages/utils/test/schema/retrieveSchemaTest.ts index adbccfa2fc..c2b9685f79 100644 --- a/packages/utils/test/schema/retrieveSchemaTest.ts +++ b/packages/utils/test/schema/retrieveSchemaTest.ts @@ -171,6 +171,64 @@ export default function retrieveSchemaTest(testValidator: TestValidatorType) { }); }); + it("should `resolve` and stub out a schema which contains an `additionalProperties` with oneOf", () => { + const oneOf: RJSFSchema[] = [ + { + type: "string", + }, + { + type: "number", + }, + ]; + const schema: RJSFSchema = { + additionalProperties: { + oneOf, + }, + type: "object", + }; + + const formData = { newKey: {} }; + expect(retrieveSchema(testValidator, schema, {}, formData)).toEqual({ + ...schema, + properties: { + newKey: { + type: "object", + oneOf, + [ADDITIONAL_PROPERTY_FLAG]: true, + }, + }, + }); + }); + + it("should `resolve` and stub out a schema which contains an `additionalProperties` with anyOf", () => { + const anyOf: RJSFSchema[] = [ + { + type: "string", + }, + { + type: "number", + }, + ]; + const schema: RJSFSchema = { + additionalProperties: { + anyOf, + }, + type: "object", + }; + + const formData = { newKey: {} }; + expect(retrieveSchema(testValidator, schema, {}, formData)).toEqual({ + ...schema, + properties: { + newKey: { + type: "object", + anyOf, + [ADDITIONAL_PROPERTY_FLAG]: true, + }, + }, + }); + }); + it("should handle null formData for schema which contains additionalProperties", () => { const schema: RJSFSchema = { additionalProperties: {