diff --git a/CHANGELOG.md b/CHANGELOG.md index b819d00e67..624a916211 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,30 @@ it according to semantic versioning. For example, if your PR adds a breaking cha should change the heading of the (upcoming) version to include a major version bump. --> +# 5.24.4 + +## @rjsf/core + +- Added `initialDefaultsGenerated` flag to state, which indicates whether the initial generation of defaults has been completed +- Added `ObjectField` tests for additionalProperties with defaults + +## @rjsf/utils + +- Updated `getDefaultFormState` to add a new `initialDefaultsGenerated` prop flag, along with type definitions, fixing uneditable & permanent defaults with additional properties [3759](https://github.com/rjsf-team/react-jsonschema-form/issues/3759) +- Updated `createSchemaUtils` definition to reflect addition of `initialDefaultsGenerated` +- Updated existing tests where `getDefaultFormState` is used to reflect addition of `initialDefaultsGenerated` + +## @rjsf/docs +- Updated docs for `getDefaultFormState` to reflect addition of `initialDefaultsGenerated` prop + +## @rjsf/validator-ajv6 + +- Updated `getDefaultFormState` calls to reflect addition of `initialDefaultsGenerated` + +## @rjsf/validator-ajv8 + +- Updated `getDefaultFormState` calls to reflect addition of `initialDefaultsGenerated` + # 5.24.3 ## @rjsf/utils diff --git a/packages/core/src/components/Form.tsx b/packages/core/src/components/Form.tsx index 5a5d7a800c..37979dfd57 100644 --- a/packages/core/src/components/Form.tsx +++ b/packages/core/src/components/Form.tsx @@ -252,6 +252,8 @@ export interface FormState; this.setState(state, () => onChange && onChange({ ...this.state, ...state })); diff --git a/packages/core/src/components/fields/MultiSchemaField.tsx b/packages/core/src/components/fields/MultiSchemaField.tsx index 498713038c..db4b38649e 100644 --- a/packages/core/src/components/fields/MultiSchemaField.tsx +++ b/packages/core/src/components/fields/MultiSchemaField.tsx @@ -126,7 +126,7 @@ class AnyOfField { diff --git a/packages/core/test/Form.test.jsx b/packages/core/test/Form.test.jsx index b774049fd9..d3872d3d94 100644 --- a/packages/core/test/Form.test.jsx +++ b/packages/core/test/Form.test.jsx @@ -147,6 +147,7 @@ describeRepeated('Form common', (createFormComponent) => { schemaValidationErrorSchema: undefined, schemaUtils: sinon.match.object, retrievedSchema: schema, + initialDefaultsGenerated: true, }); }); }); @@ -1512,6 +1513,7 @@ describeRepeated('Form common', (createFormComponent) => { schemaValidationErrorSchema: undefined, schemaUtils: sinon.match.object, retrievedSchema: formProps.schema, + initialDefaultsGenerated: true, }); }); }); diff --git a/packages/core/test/ObjectField.test.jsx b/packages/core/test/ObjectField.test.jsx index 5bc7ec8a5d..b20a63aeed 100644 --- a/packages/core/test/ObjectField.test.jsx +++ b/packages/core/test/ObjectField.test.jsx @@ -1142,6 +1142,165 @@ describe('ObjectField', () => { }); }); + it('should generate the specified default key and value inputs if default is provided outside of additionalProperties schema', () => { + const customSchema = { + ...schema, + default: { + defaultKey: 'defaultValue', + }, + }; + const { onChange } = createFormComponent({ + schema: customSchema, + formData: {}, + }); + + sinon.assert.calledWithMatch(onChange.lastCall, { + formData: { + defaultKey: 'defaultValue', + }, + }); + }); + + it('should generate the specified default key/value input with custom formData provided', () => { + const customSchema = { + ...schema, + default: { + defaultKey: 'defaultValue', + }, + }; + const { onChange } = createFormComponent({ + schema: customSchema, + formData: { + someData: 'someValue', + }, + }); + + sinon.assert.calledWithMatch(onChange.lastCall, { + formData: { + defaultKey: 'defaultValue', + someData: 'someValue', + }, + }); + }); + + it('should edit the specified default key without duplicating', () => { + const customSchema = { + ...schema, + default: { + defaultKey: 'defaultValue', + }, + }; + const { node, onChange } = createFormComponent({ + schema: customSchema, + formData: {}, + }); + + fireEvent.blur(node.querySelector('#root_defaultKey-key'), { target: { value: 'newDefaultKey' } }); + + sinon.assert.calledWithMatch(onChange.lastCall, { + formData: { + newDefaultKey: 'defaultValue', + }, + }); + }); + + it('should remove the specified default key/value input item', () => { + const customSchema = { + ...schema, + default: { + defaultKey: 'defaultValue', + }, + }; + const { node, onChange } = createFormComponent({ + schema: customSchema, + formData: {}, + }); + + fireEvent.click(node.querySelector('.array-item-remove')); + + sinon.assert.calledWithMatch(onChange.lastCall, { + formData: {}, + }); + }); + + it('should handle nested additional property default key/value input generation', () => { + const customSchema = { + ...schema, + default: { + defaultKey: 'defaultValue', + }, + properties: { + nested: { + type: 'object', + properties: { + bar: { + type: 'object', + additionalProperties: { + type: 'string', + }, + default: { + baz: 'value', + }, + }, + }, + }, + }, + }; + + const { onChange } = createFormComponent({ + schema: customSchema, + formData: {}, + }); + + sinon.assert.calledWithMatch(onChange.lastCall, { + formData: { + defaultKey: 'defaultValue', + nested: { + bar: { + baz: 'value', + }, + }, + }, + }); + }); + + it('should remove nested additional property default key/value input', () => { + const customSchema = { + ...schema, + properties: { + nested: { + type: 'object', + properties: { + bar: { + type: 'object', + additionalProperties: { + type: 'string', + }, + default: { + baz: 'value', + }, + }, + }, + }, + }, + }; + + const { node, onChange } = createFormComponent({ + schema: customSchema, + formData: {}, + }); + + fireEvent.click(node.querySelector('.array-item-remove')); + + sinon.assert.calledWithMatch(onChange.lastCall, { + formData: { + nested: { + bar: {}, + }, + }, + }); + }); + it('should not provide an expand button if length equals maxProperties', () => { const { node } = createFormComponent({ schema: { maxProperties: 1, ...schema }, diff --git a/packages/docs/docs/api-reference/utility-functions.md b/packages/docs/docs/api-reference/utility-functions.md index 93fa954646..c8749c8b1b 100644 --- a/packages/docs/docs/api-reference/utility-functions.md +++ b/packages/docs/docs/api-reference/utility-functions.md @@ -921,6 +921,7 @@ Returns the superset of `formData` that includes the given set updated to includ - theSchema: S - The schema for which the default state is desired - [formData]: T | undefined - The current formData, if any, onto which to provide any missing defaults - [rootSchema]: S | undefined - The root schema, used to primarily to look up `$ref`s +- [initialDefaultsGenerated]: boolean - Flag indicating whether the initial form defaults have been generated - [includeUndefinedValues=false]: boolean | "excludeObjectChildren" - Optional flag, if true, cause undefined values to be added as defaults. If "excludeObjectChildren", cause undefined values for this object and pass `includeUndefinedValues` as false when computing defaults for any nested object properties. - [experimental_defaultFormStateBehavior]: Experimental_DefaultFormStateBehavior - See `Form` documentation for the [experimental_defaultFormStateBehavior](./form-props.md#experimental_defaultFormStateBehavior) prop - [experimental_customMergeAllOf]: Experimental_CustomMergeAllOf<S> - See `Form` documentation for the [experimental_customMergeAllOf](./form-props.md#experimental_custommergeallof) prop diff --git a/packages/utils/src/createSchemaUtils.ts b/packages/utils/src/createSchemaUtils.ts index 153dd9a000..fcef861955 100644 --- a/packages/utils/src/createSchemaUtils.ts +++ b/packages/utils/src/createSchemaUtils.ts @@ -103,6 +103,7 @@ class SchemaUtils( @@ -118,6 +120,7 @@ class SchemaUtils * The formData should take precedence unless it's not valid. This is useful when for example the value from formData does not exist in the schema 'enum' property, in such cases we take the value from the defaults because the value from the formData is not valid. */ shouldMergeDefaultsIntoFormData?: boolean; + /** Indicates whether initial defaults have been generated */ + initialDefaultsGenerated?: boolean; } /** Computes the defaults for the current `schema` given the `rawFormData` and `parentDefaults` if any. This drills into @@ -203,6 +205,7 @@ export function computeDefaults = {}, defaults?: T | T[] | undefined ): T { @@ -452,6 +457,7 @@ export function getObjectDefaults( acc, @@ -467,7 +473,7 @@ export function getObjectDefaults( @@ -532,6 +539,7 @@ export function getArrayDefaults = {}, defaults?: T | T[] | undefined ): T | T[] | undefined { @@ -560,6 +568,7 @@ export function getArrayDefaults @@ -706,6 +719,7 @@ export default function getDefaultFormState< experimental_customMergeAllOf, rawFormData: formData, shouldMergeDefaultsIntoFormData: true, + initialDefaultsGenerated, }); // If the formData is an object or an array, add additional properties from formData and override formData with diff --git a/packages/utils/src/types.ts b/packages/utils/src/types.ts index 2d6c8335c3..7e73b9514a 100644 --- a/packages/utils/src/types.ts +++ b/packages/utils/src/types.ts @@ -1074,6 +1074,7 @@ export interface SchemaUtilsType { - expect(getDefaultFormState(testValidator, schema, undefined, schema, includeUndefinedValues)).toEqual( + expect(getDefaultFormState(testValidator, schema, undefined, schema, false, includeUndefinedValues)).toEqual( expected ); }); @@ -458,7 +459,7 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType }; test('getDefaultFormState', () => { - expect(getDefaultFormState(testValidator, schema, undefined, schema, includeUndefinedValues)).toEqual( + expect(getDefaultFormState(testValidator, schema, undefined, schema, false, includeUndefinedValues)).toEqual( expected ); }); @@ -794,7 +795,7 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType test('getDefaultFormState', () => { expect( - getDefaultFormState(testValidator, schema, rawFormData, schema, false, { + getDefaultFormState(testValidator, schema, rawFormData, schema, false, false, { emptyObjectFields: 'populateAllDefaults', allOf: 'skipDefaults', arrayMinItems: { @@ -873,9 +874,9 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType const expected = {}; test('getDefaultFormState', () => { - expect(getDefaultFormState(testValidator, schema, undefined, schema, includeUndefinedValues)).toEqual( - expected - ); + expect( + getDefaultFormState(testValidator, schema, undefined, schema, false, includeUndefinedValues) + ).toEqual(expected); }); test('computeDefaults', () => { @@ -1050,6 +1051,7 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType rawFormData, schema, includeUndefinedValues, + false, experimental_defaultFormStateBehavior ) ).toEqual({ @@ -1131,6 +1133,7 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType rawFormData, schema, includeUndefinedValues, + false, experimental_defaultFormStateBehavior ) ).toEqual({ @@ -1298,6 +1301,7 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType rawFormData, schema, undefined, + false, experimental_defaultFormStateBehavior ) ).toEqual(expected); @@ -1380,6 +1384,7 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType undefined, schema, undefined, + false, experimental_defaultFormStateBehavior ) ).toEqual(expected); @@ -1427,6 +1432,7 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType undefined, schema, undefined, + false, experimental_defaultFormStateBehavior ) ).toEqual(expected); @@ -1474,6 +1480,7 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType undefined, schema, undefined, + false, experimental_defaultFormStateBehavior ) ).toEqual(expected); @@ -1536,6 +1543,7 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType rawFormData, schema, undefined, + false, experimental_defaultFormStateBehavior ) ).toEqual(expected); @@ -1588,7 +1596,7 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType const expected = ['Raphael', 'Michaelangelo', 'Unknown', 'Unknown']; test('getDefaultFormState', () => { - expect(getDefaultFormState(testValidator, schema, undefined, schema, includeUndefinedValues)).toEqual( + expect(getDefaultFormState(testValidator, schema, undefined, schema, false, includeUndefinedValues)).toEqual( expected ); }); @@ -1647,6 +1655,7 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType schema, undefined, schema, + false, includeUndefinedValues, experimental_defaultFormStateBehavior ) @@ -1710,7 +1719,7 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType const expected: undefined[] = [undefined, undefined, undefined, undefined]; test('getDefaultFormState', () => { - expect(getDefaultFormState(testValidator, schema, undefined, schema, includeUndefinedValues)).toEqual( + expect(getDefaultFormState(testValidator, schema, undefined, schema, false, includeUndefinedValues)).toEqual( expected ); }); @@ -1757,7 +1766,7 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType const expected = ['ConstFromRoot', 'ConstFromRoot', 'Constant', 'Constant']; test('getDefaultFormState', () => { - expect(getDefaultFormState(testValidator, schema, undefined, schema, includeUndefinedValues)).toEqual( + expect(getDefaultFormState(testValidator, schema, undefined, schema, false, includeUndefinedValues)).toEqual( expected ); }); @@ -1809,7 +1818,7 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType const expected: never[] = []; test('getDefaultFormState', () => { - expect(getDefaultFormState(testValidator, schema, undefined, schema, includeUndefinedValues)).toEqual( + expect(getDefaultFormState(testValidator, schema, undefined, schema, false, includeUndefinedValues)).toEqual( expected ); }); @@ -3855,7 +3864,7 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType }, }; expect( - getDefaultFormState(testValidator, schema, {}, undefined, undefined, { + getDefaultFormState(testValidator, schema, {}, undefined, undefined, false, { emptyObjectFields: 'populateRequiredDefaults', }) ).toEqual({ name: {} }); @@ -3886,7 +3895,7 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType }, }; expect( - getDefaultFormState(testValidator, schema, {}, undefined, undefined, { + getDefaultFormState(testValidator, schema, {}, undefined, undefined, false, { emptyObjectFields: 'populateRequiredDefaults', }) ).toEqual({ @@ -3920,7 +3929,7 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType ], }; expect( - getDefaultFormState(testValidator, schema, {}, undefined, undefined, { + getDefaultFormState(testValidator, schema, {}, undefined, undefined, false, { emptyObjectFields: 'populateRequiredDefaults', }) ).toEqual({ foo: 'fooVal', baz: 'bazIsRequired' }); @@ -4052,7 +4061,7 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType }, }; expect( - getDefaultFormState(testValidator, schema, {}, undefined, undefined, { + getDefaultFormState(testValidator, schema, {}, undefined, undefined, false, { emptyObjectFields: 'populateRequiredDefaults', }) ).toEqual({ name: {} }); @@ -4083,7 +4092,7 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType }, }; expect( - getDefaultFormState(testValidator, schema, {}, undefined, undefined, { + getDefaultFormState(testValidator, schema, {}, undefined, undefined, false, { emptyObjectFields: 'populateRequiredDefaults', }) ).toEqual({ @@ -4117,7 +4126,7 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType ], }; expect( - getDefaultFormState(testValidator, schema, {}, undefined, undefined, { + getDefaultFormState(testValidator, schema, {}, undefined, undefined, false, { emptyObjectFields: 'populateRequiredDefaults', }) ).toEqual({ foo: 'fooVal', baz: 'bazIsRequired' }); @@ -4635,7 +4644,7 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType it('returns field value of default when formData has undefined for field and `useDefaultIfFormDataUndefined`', () => { const formData = { field: undefined }; expect( - getDefaultFormState(testValidator, schema, formData, undefined, undefined, { + getDefaultFormState(testValidator, schema, formData, undefined, undefined, false, { mergeDefaultsIntoFormData: 'useDefaultIfFormDataUndefined', }) ).toEqual(defaultedFormData); @@ -4683,7 +4692,7 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType }, }; expect( - getDefaultFormState(testValidator, schema, { requiredArray: ['raw0'] }, schema, false, { + getDefaultFormState(testValidator, schema, { requiredArray: ['raw0'] }, schema, false, false, { arrayMinItems: { mergeExtraDefaults: true }, }) ).toEqual({ @@ -4703,7 +4712,7 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType required: ['requiredArray'], }; expect( - getDefaultFormState(testValidator, schema, undefined, schema, false, { + getDefaultFormState(testValidator, schema, undefined, schema, false, false, { arrayMinItems: { populate: 'requiredOnly' }, }) ).toEqual({ requiredArray: ['default0', 'default0'] }); @@ -4721,7 +4730,7 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType required: ['requiredArray'], }; expect( - getDefaultFormState(testValidator, schema, { requiredArray: ['raw0'] }, schema, false, { + getDefaultFormState(testValidator, schema, { requiredArray: ['raw0'] }, schema, false, false, { arrayMinItems: { populate: 'requiredOnly' }, }) ).toEqual({ requiredArray: ['raw0'] }); @@ -4739,7 +4748,7 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType required: ['requiredArray'], }; expect( - getDefaultFormState(testValidator, schema, { requiredArray: ['raw0'] }, schema, false, { + getDefaultFormState(testValidator, schema, { requiredArray: ['raw0'] }, schema, false, false, { arrayMinItems: { populate: 'requiredOnly', mergeExtraDefaults: true }, }) ).toEqual({ requiredArray: ['raw0', 'default0'] }); @@ -4762,7 +4771,7 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType required: ['stringArray', 'numberArray'], }; expect( - getDefaultFormState(testValidator, schema, {}, schema, false, { + getDefaultFormState(testValidator, schema, {}, schema, false, false, { arrayMinItems: { computeSkipPopulate: (_, schema) => !Array.isArray(schema?.items) && typeof schema?.items !== 'boolean' && schema?.items?.type === 'number', diff --git a/packages/validator-ajv6/src/validator.ts b/packages/validator-ajv6/src/validator.ts index 79c221a246..5b8f0bcd6b 100644 --- a/packages/validator-ajv6/src/validator.ts +++ b/packages/validator-ajv6/src/validator.ts @@ -152,7 +152,7 @@ export default class AJV6Validator(this, schema, formData, rootSchema, true) as T; + const newFormData = getDefaultFormState(this, schema, formData, rootSchema, false, true) as T; const errorHandler = customValidate(newFormData, createErrorHandler(newFormData), uiSchema); const userErrorSchema = unwrapErrorHandler(errorHandler); diff --git a/packages/validator-ajv8/src/processRawValidationErrors.ts b/packages/validator-ajv8/src/processRawValidationErrors.ts index 52dfc29913..0c0d22b591 100644 --- a/packages/validator-ajv8/src/processRawValidationErrors.ts +++ b/packages/validator-ajv8/src/processRawValidationErrors.ts @@ -150,7 +150,7 @@ export default function processRawValidationErrors< } // Include form data with undefined values, which is required for custom validation. - const newFormData = getDefaultFormState(validator, schema, formData, schema, true) as T; + const newFormData = getDefaultFormState(validator, schema, formData, schema, false, true) as T; const errorHandler = customValidate(newFormData, createErrorHandler(newFormData), uiSchema); const userErrorSchema = unwrapErrorHandler(errorHandler);