diff --git a/CHANGELOG.md b/CHANGELOG.md index c52a964bda..fd72039559 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,10 @@ should change the heading of the (upcoming) version to include a major version b - Fixed issue in BaseInputTemplate where input props were passed to `slotProps.htmlInput`, which does not work in MUI v5. +## @rjsf/utils + +- Fixed issue with schema combinators(allOf, anyOf, oneOf) could not be modified when defaults were set, fixing [#4555](https://github.com/rjsf-team/react-jsonschema-form/issues/4555) + ## Dev / docs / playground - Updated docs for ArrayFieldItemTemplate to include prop `onCopyIndexClick`, fixing [#4507](https://github.com/rjsf-team/react-jsonschema-form/issues/4507) diff --git a/packages/core/test/Form.test.jsx b/packages/core/test/Form.test.jsx index e1e5cc1f33..8f76fcd45e 100644 --- a/packages/core/test/Form.test.jsx +++ b/packages/core/test/Form.test.jsx @@ -1261,6 +1261,174 @@ describeRepeated('Form common', (createFormComponent) => { sinon.assert.callCount(onChange, 1); sinon.assert.callCount(secondOnChange, 1); }); + it('should modify an allOf field when the defaults are set', () => { + const schema = { + properties: { + all_of_field: { + allOf: [ + { + properties: { + first: { + type: 'string', + }, + }, + }, + { + properties: { + second: { + type: 'string', + }, + }, + }, + ], + default: { + second: 'second!', + }, + }, + }, + type: 'object', + }; + + const { node, onChange } = createFormComponent({ + schema, + }); + + const secondInputID = '#root_all_of_field_second'; + expect(node.querySelector(secondInputID).value).to.equal('second!'); + + act(() => { + fireEvent.change(node.querySelector(secondInputID), { + target: { value: 'changed!' }, + }); + }); + + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: { + all_of_field: { + second: 'changed!', + }, + }, + schema, + }, + 'root_all_of_field_second' + ); + + expect(node.querySelector(secondInputID).value).to.equal('changed!'); + }); + it('should modify an oneOf field when the defaults are set', () => { + const schema = { + properties: { + one_of_field: { + oneOf: [ + { + properties: { + first: { + type: 'string', + }, + }, + }, + { + properties: { + second: { + type: 'string', + }, + }, + }, + ], + default: { + second: 'second!', + }, + }, + }, + type: 'object', + }; + + const { node, onChange } = createFormComponent({ + schema, + }); + + const secondInputID = '#root_one_of_field_second'; + expect(node.querySelector(secondInputID).value).to.equal('second!'); + + act(() => { + fireEvent.change(node.querySelector(secondInputID), { + target: { value: 'changed!' }, + }); + }); + + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: { + one_of_field: { + second: 'changed!', + }, + }, + schema, + }, + 'root_one_of_field_second' + ); + + expect(node.querySelector(secondInputID).value).to.equal('changed!'); + }); + it('should modify an anyOf field when the defaults are set', () => { + const schema = { + properties: { + any_of_field: { + anyOf: [ + { + properties: { + first: { + type: 'string', + }, + }, + }, + { + properties: { + second: { + type: 'string', + }, + }, + }, + ], + default: { + second: 'second!', + }, + }, + }, + type: 'object', + }; + + const { node, onChange } = createFormComponent({ + schema, + }); + + const secondInputID = '#root_any_of_field_second'; + expect(node.querySelector(secondInputID).value).to.equal('second!'); + + act(() => { + fireEvent.change(node.querySelector(secondInputID), { + target: { value: 'changed!' }, + }); + }); + + sinon.assert.calledWithMatch( + onChange.lastCall, + { + formData: { + any_of_field: { + second: 'changed!', + }, + }, + schema, + }, + 'root_any_of_field_second' + ); + + expect(node.querySelector(secondInputID).value).to.equal('changed!'); + }); }); describe('Blur handler', () => { diff --git a/packages/utils/src/schema/getDefaultFormState.ts b/packages/utils/src/schema/getDefaultFormState.ts index af3de03a35..90145fadda 100644 --- a/packages/utils/src/schema/getDefaultFormState.ts +++ b/packages/utils/src/schema/getDefaultFormState.ts @@ -223,7 +223,9 @@ export function computeDefaults( validator, rootSchema, - rawFormData, + rawFormData ?? (schema.default as T), oneOf as S[], 0, discriminator, @@ -302,7 +304,7 @@ export function computeDefaults( validator, rootSchema, - rawFormData, + rawFormData ?? (schema.default as T), anyOf as S[], 0, discriminator, @@ -347,7 +349,9 @@ export function computeDefaults( defaultsWithFormData as T, matchingFormData as T, diff --git a/packages/utils/test/schema/getDefaultFormStateTest.ts b/packages/utils/test/schema/getDefaultFormStateTest.ts index 9f7c4a9f76..3f0d8eb7c6 100644 --- a/packages/utils/test/schema/getDefaultFormStateTest.ts +++ b/packages/utils/test/schema/getDefaultFormStateTest.ts @@ -2164,101 +2164,6 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType ).toEqual({ requiredArray: ['default0', 'default0'] }); }); }); - describe('default form state behaviour: allOf = "populateDefaults"', () => { - it('should populate default values correctly', () => { - const schema: RJSFSchema = { - title: 'Example', - type: 'object', - properties: { - animalInfo: { - properties: { - animal: { - type: 'string', - default: 'Cat', - enum: ['Cat', 'Fish'], - }, - }, - allOf: [ - { - if: { - properties: { - animal: { - const: 'Cat', - }, - }, - }, - then: { - properties: { - food: { - type: 'string', - default: 'meat', - enum: ['meat', 'grass', 'fish'], - }, - }, - required: ['food'], - }, - }, - ], - }, - }, - }; - - expect( - computeDefaults(testValidator, schema, { - rootSchema: schema, - experimental_defaultFormStateBehavior: { allOf: 'populateDefaults' }, - }) - ).toEqual({ animalInfo: { animal: 'Cat', food: 'meat' } }); - }); - }); - - describe('default form state behaviour: allOf = "skipDefaults"', () => { - it('should populate default values correctly', () => { - const schema: RJSFSchema = { - title: 'Example', - type: 'object', - properties: { - animalInfo: { - properties: { - animal: { - type: 'string', - default: 'Cat', - enum: ['Cat', 'Fish'], - }, - }, - allOf: [ - { - if: { - properties: { - animal: { - const: 'Cat', - }, - }, - }, - then: { - properties: { - food: { - type: 'string', - default: 'meat', - enum: ['meat', 'grass', 'fish'], - }, - }, - required: ['food'], - }, - }, - ], - }, - }, - }; - - expect( - computeDefaults(testValidator, schema, { - rootSchema: schema, - experimental_defaultFormStateBehavior: { allOf: 'skipDefaults' }, - }) - ).toEqual({ animalInfo: { animal: 'Cat' } }); - }); - }); describe('default form state behavior: arrayMinItems.populate = "never"', () => { it('should not be filled if minItems defined and required', () => { const schema: RJSFSchema = { @@ -3765,7 +3670,106 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType }); }); }); + describe('defaults with allOf', () => { + let schema: RJSFSchema; + + it('should populate root defaults for allOf', () => { + schema = { + allOf: [ + { + properties: { + first: { + title: 'First', + type: 'string', + }, + }, + }, + { + properties: { + second: { + title: 'Second', + type: 'string', + }, + }, + }, + ], + default: { + second: 'Second 2!', + }, + type: 'object', + }; + + expect(getDefaultFormState(testValidator, schema, {})).toEqual({ + second: 'Second 2!', + }); + }); + + describe('default form state behaviour: allOf = "populateDefaults"', () => { + it('should populate default values correctly', () => { + schema = { + title: 'Example', + type: 'object', + properties: { + animalInfo: { + properties: { + animal: { + type: 'string', + default: 'Cat', + enum: ['Cat', 'Fish'], + }, + }, + allOf: [ + { + if: { + properties: { + animal: { + const: 'Cat', + }, + }, + }, + then: { + properties: { + food: { + type: 'string', + default: 'meat', + enum: ['meat', 'grass', 'fish'], + }, + }, + required: ['food'], + }, + }, + ], + }, + }, + }; + + expect( + computeDefaults(testValidator, schema, { + rootSchema: schema, + experimental_defaultFormStateBehavior: { allOf: 'populateDefaults' }, + }) + ).toEqual({ animalInfo: { animal: 'Cat', food: 'meat' } }); + }); + }); + + describe('default form state behaviour: allOf = "skipDefaults"', () => { + it('should populate default values correctly', () => { + expect( + computeDefaults(testValidator, schema, { + rootSchema: schema, + experimental_defaultFormStateBehavior: { allOf: 'skipDefaults' }, + }) + ).toEqual({ animalInfo: { animal: 'Cat' } }); + }); + }); + }); describe('defaults with oneOf', () => { + afterEach(() => { + // Reset the testValidator + if (typeof testValidator.reset === 'function') { + testValidator?.reset(); + } + }); it('should not populate defaults for empty oneOf', () => { const schema: RJSFSchema = { type: 'object', @@ -3795,6 +3799,46 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType name: 'a', }); }); + it('should populate root defaults for oneOf', () => { + const schema: RJSFSchema = { + oneOf: [ + { + properties: { + first: { + title: 'First', + type: 'string', + }, + }, + }, + { + properties: { + second: { + title: 'Second', + type: 'string', + }, + }, + }, + ], + default: { + second: 'Second 2!', + }, + type: 'object', + }; + + // Mock isValid so that withExactlyOneSubschema works as expected + testValidator.setReturnValues({ + isValid: [ + false, // First oneOf... first === first + false, // Second oneOf... second !== first + false, // Third oneOf... second === second + true, // Fourth oneOf... second === second + ], + }); + + expect(getDefaultFormState(testValidator, schema)).toEqual({ + second: 'Second 2!', + }); + }); it('should populate defaults for oneOf when `type`: `object` is missing', () => { const schema: RJSFSchema = { type: 'object', @@ -3979,7 +4023,13 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType }); }); describe('defaults with anyOf', () => { - it('should not populate defaults for empty oneOf', () => { + afterEach(() => { + // Reset the testValidator + if (typeof testValidator.reset === 'function') { + testValidator?.reset(); + } + }); + it('should not populate defaults for empty anyOf', () => { const schema: RJSFSchema = { type: 'object', properties: { @@ -4008,6 +4058,46 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType name: 'a', }); }); + it('should populate root defaults for anyOf', () => { + const schema: RJSFSchema = { + anyOf: [ + { + properties: { + first: { + title: 'First', + type: 'string', + }, + }, + }, + { + properties: { + second: { + title: 'Second', + type: 'string', + }, + }, + }, + ], + default: { + second: 'Second 2!', + }, + type: 'object', + }; + + // Mock isValid so that withExactlyOneSubschema works as expected + testValidator.setReturnValues({ + isValid: [ + false, // First anyOf... first === first + false, // Second anyOf... second !== first + false, // Third anyOf... second === second + true, // Fourth anyOf... second === second + ], + }); + + expect(getDefaultFormState(testValidator, schema)).toEqual({ + second: 'Second 2!', + }); + }); it('should populate nested default values for anyOf', () => { const schema: RJSFSchema = { type: 'object',