diff --git a/CHANGELOG.md b/CHANGELOG.md index 6707b2afbe..22131cadb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,23 @@ 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. --> +# 6.0.0-beta.21 + +## @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 + # 6.0.0-beta-20 ## @rjsf/antd diff --git a/packages/core/src/components/Form.tsx b/packages/core/src/components/Form.tsx index 2795895f3d..e2966e0ae0 100644 --- a/packages/core/src/components/Form.tsx +++ b/packages/core/src/components/Form.tsx @@ -269,6 +269,8 @@ export interface FormState; this.setState(state, () => onChange && onChange({ ...this.state, ...state })); diff --git a/packages/core/test/Form.test.jsx b/packages/core/test/Form.test.jsx index abb86270df..cb26f5cdf8 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, }); }); }); @@ -1979,6 +1980,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 73c0618672..17e20c8e24 100644 --- a/packages/core/test/ObjectField.test.jsx +++ b/packages/core/test/ObjectField.test.jsx @@ -1144,6 +1144,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('.rjsf-object-property-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('.rjsf-object-property-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 f602c86339..5cd65688b8 100644 --- a/packages/docs/docs/api-reference/utility-functions.md +++ b/packages/docs/docs/api-reference/utility-functions.md @@ -1065,6 +1065,7 @@ Returns the superset of `formData` that includes the given set updated to includ - [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 +- [initialDefaultsGenerated]: boolean - Optional flag, indicates whether or not initial defaults have been generated #### Returns diff --git a/packages/docs/docs/migration-guides/v6.x upgrade guide.md b/packages/docs/docs/migration-guides/v6.x upgrade guide.md index aced06797e..e797b610b2 100644 --- a/packages/docs/docs/migration-guides/v6.x upgrade guide.md +++ b/packages/docs/docs/migration-guides/v6.x upgrade guide.md @@ -699,6 +699,10 @@ Three new validator-based utility functions are available in `@rjsf/utils`: - `findSelectedOptionInXxxOf(validator: ValidatorType, rootSchema: S, schema: S, fallbackField: string,xxx: 'anyOf' | 'oneOf', formData?: T, experimental_customMergeAllOf?: Experimental_CustomMergeAllOf): S | undefined`: Finds the option that matches the selector field in the `schema` or undefined if nothing is selected - `getFromSchema(validator: ValidatorType, rootSchema: S, schema: S, path: string | string[], defaultValue: T | S, experimental_customMergeAllOf?: Experimental_CustomMergeAllOf): T | S`: Helper that acts like lodash's `get` but additionally retrieves `$ref`s as needed to get the path for schemas +### Changes to existing utility functions + +- `getDefaultFormState`: Added optional `initialDefaultsGenerated` boolean flag that indicates whether or not initial defaults have been generated + ### Dynamic UI Schema for Array Items RJSF 6.x introduces a new feature that allows dynamic UI schema generation for array items. diff --git a/packages/utils/src/createSchemaUtils.ts b/packages/utils/src/createSchemaUtils.ts index 994bd373e7..3cf181ee1c 100644 --- a/packages/utils/src/createSchemaUtils.ts +++ b/packages/utils/src/createSchemaUtils.ts @@ -165,12 +165,14 @@ class SchemaUtils( this.validator, @@ -180,6 +182,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 @@ -229,6 +231,7 @@ export function computeDefaults = {}, defaults?: T | T[], ): T { @@ -498,6 +503,7 @@ export function getObjectDefaults( @@ -515,7 +521,7 @@ export function getObjectDefaults( @@ -580,6 +587,7 @@ export function getArrayDefaults = {}, defaults?: T[], ): T[] | undefined { @@ -608,6 +616,7 @@ export function getArrayDefaults, + initialDefaultsGenerated?: boolean, ) { if (!isObject(theSchema)) { throw new Error('Invalid schema: ' + theSchema); @@ -757,6 +769,7 @@ export default function getDefaultFormState< experimental_customMergeAllOf, rawFormData: formData, shouldMergeDefaultsIntoFormData: true, + initialDefaultsGenerated, }); if (schema.type !== 'object' && isObject(schema.default)) { diff --git a/packages/utils/src/types.ts b/packages/utils/src/types.ts index e86bdacc29..ce368b5bcb 100644 --- a/packages/utils/src/types.ts +++ b/packages/utils/src/types.ts @@ -1194,12 +1194,14 @@ export interface SchemaUtilsType