diff --git a/CHANGELOG.md b/CHANGELOG.md index 542760ed22..2287d2fbdc 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. --> +# 5.8.3 + +## @rjsf/utils + +- Updated `getDefaultFormState()` to fix a bug where `experimental_defaultFormStateBehavior: { emptyObjectFields: 'populateRequiredDefaults' }` wasn't working for object properties with `$ref`s +- Experimental feature **breaking change**: + - Updated the `experimental_defaultFormStateBehavior.arrayMinItems` from simple flag to an object containing two optional fields, `populate` and `mergeExtraDefaults` + - The new `arrayMinItems.mergeExtraDefaults` flag, when "true", allows users to merge defaults onto the end of `formData` arrays when `minItems` is specified + - If you were previously passing `experimental_defaultFormStateBehavior` as `{ arrayMinItems = 'requiredOnly }` on the `Form`, now you would pass `{ arrayMinItems: { populate: 'requiredOnly' } }` +- Added a new, optional `mergeExtraArrayDefaults=false` flag to the `mergeDefaultWithFormData()` utility function to support the new `arrayMinItems.mergeExtraDefaults` experimental feature + +## Dev / docs / playground + +- Updated the `utility-functions` documentation to add the new `mergeExtraArrayDefaults` flag for the `mergeDefaultWithFormData()` function +- Updated the `form-props` documentation to update the `arrayMinItems` documentation for the new object behavior +- Updated the `playground` to add a checkbox for the new `arrayMinItems.mergeExtraDefaults` flag + # 5.8.2 ## @rjsf/validator-ajv8 diff --git a/packages/core/test/Form.test.jsx b/packages/core/test/Form.test.jsx index e440a12dbb..4a58eff3de 100644 --- a/packages/core/test/Form.test.jsx +++ b/packages/core/test/Form.test.jsx @@ -1416,7 +1416,7 @@ describeRepeated('Form common', (createFormComponent) => { formData: { albums: ['Until We Have Faces'], }, - experimental_defaultFormStateBehavior: { arrayMinItems: 'requiredOnly' }, + experimental_defaultFormStateBehavior: { arrayMinItems: { populate: 'requiredOnly' } }, }); submitForm(node); sinon.assert.calledWithMatch(onError.lastCall, [ diff --git a/packages/docs/docs/api-reference/form-props.md b/packages/docs/docs/api-reference/form-props.md index 242852942f..bec640a0f4 100644 --- a/packages/docs/docs/api-reference/form-props.md +++ b/packages/docs/docs/api-reference/form-props.md @@ -61,24 +61,41 @@ See [Validation](../usage/validation.md) for more information. ## experimental_defaultFormStateBehavior -Experimental features to specify different form state behavior. Currently, this only affects the handling of optional array fields where `minItems` is set. +Experimental features to specify different form state behavior. +Currently, this only affects the handling of optional array fields where `minItems` is set and handling of setting defaults based on the value of `emptyObjectFields`. -The following sub-sections represent the different keys in this object, with the tables explaining the values and their meanings. +The following subsections represent the different keys in this object, with the tables explaining the values and their meanings. ### `arrayMinItems` -| Flag Value | Description | -| -------------- | ---------------------------------------------------------------------------------------------------------------------------------- | -| `populate` | Legacy behavior - populate minItems entries with default values initially and include empty array when no values have been defined | -| `requiredOnly` | Ignore `minItems` on a field when calculating defaults unless the field is required | +This optional subsection is an object with two optional fields, `populate` and `mergeExtraDefaults`. +When not specified, it defaults to `{ populate: 'all', mergeExtraDefaults: false }`. + +#### `arrayMinItems.populate` + +Optional enumerated flag controlling how array minItems are populated, defaulting to `all`: + +| Flag Value | Description | +| -------------- | ----------------------------------------------------------------------------------------------------------------------------------- | +| `all` | Legacy behavior - populate minItems entries with default values initially and include empty array when no values have been defined. | +| `requiredOnly` | Ignore `minItems` on a field when calculating defaults unless the field is required. | + +#### `arrayMinItems.mergeExtraDefaults` + +Optional boolean flag, defaulting to `false` when not specified. +When `formData` is provided and does not contain `minItems` worth of data, this flag controls whether the extra data provided by the defaults is appended onto the existing `formData` items to ensure the `minItems` condition is met. +When `false`, only the `formData` provided is merged into the default form state, even if there are fewer than the `minItems`. +When `true`, the defaults are appended onto the end of the `formData` until the `minItems` condition is met. ### `emptyObjectFields` -| Flag Value | Description | -| -------------------------- | -------------------------------------------------------------------------------------------------------------------------- | -| `populateAllDefaults` | Legacy behavior - set default when there is a primitive value, an non-empty object field, or the field itself is required | -| `populateRequiredDefaults` | Only sets default when a value is an object and its parent field is required or it is a primitive value and it is required | -| `skipDefaults` | Does not set defaults | +Optional enumerated flag controlling how empty object fields are populated, defaulting to `populateAllDefaults`: + +| Flag Value | Description | +| -------------------------- | --------------------------------------------------------------------------------------------------------------------------- | +| `populateAllDefaults` | Legacy behavior - set default when there is a primitive value, an non-empty object field, or the field itself is required | +| `populateRequiredDefaults` | Only sets default when a value is an object and its parent field is required, or it is a primitive value and it is required | +| `skipDefaults` | Does not set defaults | ```tsx import { RJSFSchema } from '@rjsf/utils'; @@ -95,7 +112,7 @@ render( schema={schema} validator={validator} experimental_defaultFormStateBehavior={{ - arrayMinItems: 'requiredOnly', + arrayMinItems: { populate: 'requiredOnly' }, }} />, document.getElementById('app') diff --git a/packages/docs/docs/api-reference/utility-functions.md b/packages/docs/docs/api-reference/utility-functions.md index 4dccef9904..577d272a7c 100644 --- a/packages/docs/docs/api-reference/utility-functions.md +++ b/packages/docs/docs/api-reference/utility-functions.md @@ -516,7 +516,7 @@ When merging defaults and form data, we want to merge in this specific way: - objects are deeply merged - arrays are merged in such a way that: - - when the array is set in form data, only array entries set in form data are deeply merged; additional entries from the defaults are ignored + - when the array is set in form data, only array entries set in form data are deeply merged; additional entries from the defaults are ignored unless `mergeExtraArrayDefaults` is true, in which case the extras are appended onto the end of the form data - when the array is not set in form data, the default is copied over - scalars are overwritten/set by form data @@ -524,6 +524,7 @@ When merging defaults and form data, we want to merge in this specific way: - [defaults]: T | undefined - The defaults to merge - [formData]: T | undefined - The form data into which the defaults will be merged +- [mergeExtraArrayDefaults=false]: boolean - If true, any additional default array entries are appended onto the formData #### Returns diff --git a/packages/playground/src/components/Header.tsx b/packages/playground/src/components/Header.tsx index 12da577b7e..fe46917b93 100644 --- a/packages/playground/src/components/Header.tsx +++ b/packages/playground/src/components/Header.tsx @@ -79,21 +79,31 @@ const liveSettingsSelectSchema: RJSFSchema = { type: 'object', properties: { arrayMinItems: { - type: 'string', - title: 'minItems behavior for array field', - default: 'populate', - oneOf: [ - { + type: 'object', + properties: { + populate: { type: 'string', - title: 'Populate remaining minItems with default values (legacy behavior)', - enum: ['populate'], + default: 'populate', + title: 'Populate minItems in arrays', + oneOf: [ + { + type: 'string', + title: 'Populate remaining minItems with default values (legacy behavior)', + enum: ['all'], + }, + { + type: 'string', + title: 'Ignore minItems unless field is required', + enum: ['requiredOnly'], + }, + ], }, - { - type: 'string', - title: 'Ignore minItems unless field is required', - enum: ['requiredOnly'], + mergeExtraDefaults: { + title: 'Merge array defaults with formData', + type: 'boolean', + default: false, }, - ], + }, }, emptyObjectFields: { type: 'string', @@ -129,6 +139,11 @@ const liveSettingsSelectUiSchema: UiSchema = { 'ui:options': { label: false, }, + arrayMinItems: { + 'ui:options': { + label: false, + }, + }, }, }; diff --git a/packages/utils/src/mergeDefaultsWithFormData.ts b/packages/utils/src/mergeDefaultsWithFormData.ts index 7dbefa7fcd..e4ace80eff 100644 --- a/packages/utils/src/mergeDefaultsWithFormData.ts +++ b/packages/utils/src/mergeDefaultsWithFormData.ts @@ -9,29 +9,43 @@ import { GenericObjectType } from '../src'; * - objects are deeply merged * - arrays are merged in such a way that: * - when the array is set in form data, only array entries set in form data - * are deeply merged; additional entries from the defaults are ignored + * are deeply merged; additional entries from the defaults are ignored unless `mergeExtraArrayDefaults` is true, in + * which case the extras are appended onto the end of the form data * - when the array is not set in form data, the default is copied over * - scalars are overwritten/set by form data * * @param [defaults] - The defaults to merge * @param [formData] - The form data into which the defaults will be merged + * @param [mergeExtraArrayDefaults=false] - If true, any additional default array entries are appended onto the formData * @returns - The resulting merged form data with defaults */ -export default function mergeDefaultsWithFormData(defaults?: T, formData?: T): T | undefined { +export default function mergeDefaultsWithFormData( + defaults?: T, + formData?: T, + mergeExtraArrayDefaults = false +): T | undefined { if (Array.isArray(formData)) { const defaultsArray = Array.isArray(defaults) ? defaults : []; const mapped = formData.map((value, idx) => { if (defaultsArray[idx]) { - return mergeDefaultsWithFormData(defaultsArray[idx], value); + return mergeDefaultsWithFormData(defaultsArray[idx], value, mergeExtraArrayDefaults); } return value; }); + // Merge any extra defaults when mergeExtraArrayDefaults is true + if (mergeExtraArrayDefaults && mapped.length < defaultsArray.length) { + mapped.push(...defaultsArray.slice(mapped.length)); + } return mapped as unknown as T; } if (isObject(formData)) { const acc: { [key in keyof T]: any } = Object.assign({}, defaults); // Prevent mutation of source object. return Object.keys(formData as GenericObjectType).reduce((acc, key) => { - acc[key as keyof T] = mergeDefaultsWithFormData(defaults ? get(defaults, key) : {}, get(formData, key)); + acc[key as keyof T] = mergeDefaultsWithFormData( + defaults ? get(defaults, key) : {}, + get(formData, key), + mergeExtraArrayDefaults + ); return acc; }, acc); } diff --git a/packages/utils/src/schema/getDefaultFormState.ts b/packages/utils/src/schema/getDefaultFormState.ts index 07c48861b1..a2a393b4d5 100644 --- a/packages/utils/src/schema/getDefaultFormState.ts +++ b/packages/utils/src/schema/getDefaultFormState.ts @@ -89,7 +89,7 @@ function maybeAddDefaultToObject( key: string, computedDefault: T | T[] | undefined, includeUndefinedValues: boolean | 'excludeObjectChildren', - isParentRequired: boolean, + isParentRequired?: boolean, requiredFields: string[] = [], experimental_defaultFormStateBehavior: Experimental_DefaultFormStateBehavior = {} ) { @@ -98,12 +98,15 @@ function maybeAddDefaultToObject( obj[key] = computedDefault; } else if (emptyObjectFields !== 'skipDefaults') { if (isObject(computedDefault)) { + // If isParentRequired is undefined, then we are at the root level of the schema so defer to the requiredness of + // the field key itself in the `requiredField` list + const isSelfOrParentRequired = isParentRequired === undefined ? requiredFields.includes(key) : isParentRequired; // Store computedDefault if it's a non-empty object(e.g. not {}) and satisfies certain conditions // Condition 1: If computedDefault is not empty or if the key is a required field // Condition 2: If the parent object is required or emptyObjectFields is not 'populateRequiredDefaults' if ( (!isEmpty(computedDefault) || requiredFields.includes(key)) && - (isParentRequired || emptyObjectFields !== 'populateRequiredDefaults') + (isSelfOrParentRequired || emptyObjectFields !== 'populateRequiredDefaults') ) { obj[key] = computedDefault; } @@ -156,7 +159,7 @@ export function computeDefaults = {} ): T | T[] | undefined { const formData: T = (isObject(rawFormData) ? rawFormData : {}) as T; @@ -192,6 +195,7 @@ export function computeDefaults(defaults as T, formData); + return mergeDefaultsWithFormData(defaults as T, formData, mergeExtraDefaults); } if (Array.isArray(formData)) { - return mergeDefaultsWithFormData(defaults as T[], formData); + return mergeDefaultsWithFormData(defaults as T[], formData, mergeExtraDefaults); } return formData; } diff --git a/packages/utils/src/types.ts b/packages/utils/src/types.ts index c3a9658117..cd0b1f8704 100644 --- a/packages/utils/src/types.ts +++ b/packages/utils/src/types.ts @@ -31,12 +31,40 @@ export type RJSFSchema = StrictRJSFSchema & GenericObjectType; */ export type FormContextType = GenericObjectType; -/** Experimental features to specify different form state behavior. Currently, this affects the +/** Experimental feature that specifies the Array `minItems` default form state behavior + */ +export type Experimental_ArrayMinItems = { + /** Optional enumerated flag controlling how array minItems are populated, defaulting to `all`: + * - `all`: Legacy behavior, populate minItems entries with default values initially and include an empty array when + * no values have been defined. + * - `requiredOnly`: Ignore `minItems` on a field when calculating defaults unless the field is required. + */ + populate?: 'all' | 'requiredOnly'; + /** When `formData` is provided and does not contain `minItems` worth of data, this flag (`false` by default) controls + * whether the extra data provided by the defaults is appended onto the existing `formData` items to ensure the + * `minItems` condition is met. When false, only the `formData` provided is merged into the default form state, even + * if there are fewer than the `minItems`. When true, the defaults are appended onto the end of the `formData` until + * the `minItems` condition is met. + */ + mergeExtraDefaults?: boolean; +}; + +/** Experimental features to specify different default form state behaviors. Currently, this affects the * handling of optional array fields where `minItems` is set and handling of setting defaults based on the - * value of `emptyObjectFields` + * value of `emptyObjectFields`. */ export type Experimental_DefaultFormStateBehavior = { - arrayMinItems?: 'populate' | 'requiredOnly'; + /** Optional object, that controls how the default form state for arrays with `minItems` is handled. When not provided + * it defaults to `{ populate: 'all' }`. + */ + arrayMinItems?: Experimental_ArrayMinItems; + /** Optional enumerated flag controlling how empty object fields are populated, defaulting to `populateAllDefaults`: + * - `populateAllDefaults`: Legacy behavior - set default when there is a primitive value, an non-empty object field, + * or the field itself is required | + * - `populateRequiredDefaults`: Only sets default when a value is an object and its parent field is required, or it + * is a primitive value and it is required | + * - `skipDefaults`: Does not set defaults | + */ emptyObjectFields?: 'populateAllDefaults' | 'populateRequiredDefaults' | 'skipDefaults'; }; diff --git a/packages/utils/test/createSchemaUtils.test.ts b/packages/utils/test/createSchemaUtils.test.ts index c471fe0143..1858c6078f 100644 --- a/packages/utils/test/createSchemaUtils.test.ts +++ b/packages/utils/test/createSchemaUtils.test.ts @@ -10,7 +10,9 @@ import getTestValidator from './testUtils/getTestValidator'; describe('createSchemaUtils()', () => { const testValidator: ValidatorType = getTestValidator({}); const rootSchema: RJSFSchema = { type: 'object' }; - const defaultFormStateBehavior: Experimental_DefaultFormStateBehavior = { arrayMinItems: 'requiredOnly' }; + const defaultFormStateBehavior: Experimental_DefaultFormStateBehavior = { + arrayMinItems: { populate: 'requiredOnly' }, + }; const schemaUtils: SchemaUtilsType = createSchemaUtils(testValidator, rootSchema, defaultFormStateBehavior); it('getValidator()', () => { @@ -25,9 +27,9 @@ describe('createSchemaUtils()', () => { expect(schemaUtils.doesSchemaUtilsDiffer(testValidator, rootSchema)).toBe(false); }); it('returns true when passing different defaultFormStateBehavior', () => { - expect(schemaUtils.doesSchemaUtilsDiffer(testValidator, rootSchema, { arrayMinItems: 'requiredOnly' })).toBe( - true - ); + expect( + schemaUtils.doesSchemaUtilsDiffer(testValidator, rootSchema, { arrayMinItems: { populate: 'requiredOnly' } }) + ).toBe(true); }); }); @@ -52,7 +54,9 @@ describe('createSchemaUtils()', () => { expect(schemaUtils.doesSchemaUtilsDiffer(testValidator, {}, defaultFormStateBehavior)).toBe(true); }); it('returns true when passing different defaultFormStateBehavior', () => { - expect(schemaUtils.doesSchemaUtilsDiffer(testValidator, rootSchema, { arrayMinItems: 'populate' })).toBe(true); + expect( + schemaUtils.doesSchemaUtilsDiffer(testValidator, rootSchema, { arrayMinItems: { populate: 'all' } }) + ).toBe(true); }); }); }); diff --git a/packages/utils/test/mergeDefaultsWithFormData.test.ts b/packages/utils/test/mergeDefaultsWithFormData.test.ts index bccc01658f..919b94ef83 100644 --- a/packages/utils/test/mergeDefaultsWithFormData.test.ts +++ b/packages/utils/test/mergeDefaultsWithFormData.test.ts @@ -36,6 +36,10 @@ describe('mergeDefaultsWithFormData()', () => { expect(mergeDefaultsWithFormData([1, 2, 3], [4, 5])).toEqual([4, 5]); }); + it('should merge arrays using entries from second and extra from the first', () => { + expect(mergeDefaultsWithFormData([1, 2, 3], [4, 5], true)).toEqual([4, 5, 3]); + }); + it('should deeply merge arrays with overlapping entries', () => { expect(mergeDefaultsWithFormData([{ a: 1 }], [{ b: 2 }, { c: 3 }])).toEqual([{ a: 1, b: 2 }, { c: 3 }]); }); @@ -75,6 +79,41 @@ describe('mergeDefaultsWithFormData()', () => { expect(mergeDefaultsWithFormData(obj1, obj2)).toEqual(expected); }); + it('should recursively merge deeply nested objects, including extra array data', () => { + const obj1 = { + a: 1, + b: { + c: 3, + d: [1, 2, 3], + e: { f: { g: 1 } }, + h: [{ i: 1 }, { i: 2 }], + }, + c: 2, + }; + const obj2 = { + a: 1, + b: { + d: [3], + e: { f: { h: 2 } }, + g: 1, + h: [{ i: 3 }], + }, + c: 3, + }; + const expected = { + a: 1, + b: { + c: 3, + d: [3, 2, 3], + e: { f: { g: 1, h: 2 } }, + g: 1, + h: [{ i: 3 }, { i: 2 }], + }, + c: 3, + }; + expect(mergeDefaultsWithFormData(obj1, obj2, true)).toEqual(expected); + }); + it('should recursively merge File objects', () => { const file = new File(['test'], 'test.txt'); const obj1 = { diff --git a/packages/utils/test/schema/getDefaultFormStateTest.ts b/packages/utils/test/schema/getDefaultFormStateTest.ts index 6bdd84bac5..35f9b8de1d 100644 --- a/packages/utils/test/schema/getDefaultFormStateTest.ts +++ b/packages/utils/test/schema/getDefaultFormStateTest.ts @@ -329,7 +329,7 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType expect( computeDefaults(testValidator, schema, { rootSchema: schema, - experimental_defaultFormStateBehavior: { arrayMinItems: 'requiredOnly' }, + experimental_defaultFormStateBehavior: { arrayMinItems: { populate: 'requiredOnly' } }, }) ).toEqual({}); }); @@ -347,7 +347,7 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType computeDefaults(testValidator, schema, { rootSchema: schema, rawFormData: { optionalArray: [] }, - experimental_defaultFormStateBehavior: { arrayMinItems: 'requiredOnly' }, + experimental_defaultFormStateBehavior: { arrayMinItems: { populate: 'requiredOnly' } }, }) ).toEqual({ optionalArray: [] }); }); @@ -366,7 +366,7 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType expect( computeDefaults(testValidator, schema, { rootSchema: schema, - experimental_defaultFormStateBehavior: { arrayMinItems: 'requiredOnly' }, + experimental_defaultFormStateBehavior: { arrayMinItems: { populate: 'requiredOnly' } }, }) ).toEqual({ requiredArray: [undefined, undefined] }); }); @@ -386,12 +386,11 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType expect( computeDefaults(testValidator, schema, { rootSchema: schema, - experimental_defaultFormStateBehavior: { arrayMinItems: 'requiredOnly' }, + experimental_defaultFormStateBehavior: { arrayMinItems: { populate: 'requiredOnly' } }, }) ).toEqual({ requiredArray: ['default0', 'default1'] }); }); - // BUG: https://github.com/rjsf-team/react-jsonschema-form/issues/3602 - it.skip('should combine defaults with raw form data for a required array property with minItems', () => { + it('should combine defaults with raw form data for a required array property with minItems', () => { const schema: RJSFSchema = { type: 'object', properties: { @@ -407,9 +406,9 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType computeDefaults(testValidator, schema, { rootSchema: schema, rawFormData: { requiredArray: ['raw0'] }, - experimental_defaultFormStateBehavior: { arrayMinItems: 'requiredOnly' }, + experimental_defaultFormStateBehavior: { arrayMinItems: { populate: 'requiredOnly' } }, }) - ).toEqual({ requiredArray: ['raw0', 'default0'] }); + ).toEqual({ requiredArray: ['default0', 'default0'] }); }); }); describe('default form state behavior: emptyObjectFields = "populateRequiredDefaults"', () => { @@ -440,6 +439,69 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType }) ).toEqual({ requiredProperty: 'foo' }); }); + it('test an object with a nested required property in a ref', () => { + const schema: RJSFSchema = { + type: 'object', + definitions: { + nestedRequired: { + properties: { + nested: { + type: 'string', + default: 'foo', + }, + }, + required: ['nested'], + }, + }, + properties: { + nestedRequiredProperty: { + $ref: '#/definitions/nestedRequired', + }, + requiredProperty: { + type: 'string', + default: 'foo', + }, + }, + required: ['requiredProperty', 'nestedRequiredProperty'], + }; + expect( + computeDefaults(testValidator, schema, { + rootSchema: schema, + experimental_defaultFormStateBehavior: { emptyObjectFields: 'populateRequiredDefaults' }, + }) + ).toEqual({ requiredProperty: 'foo', nestedRequiredProperty: { nested: 'foo' } }); + }); + it('test an object with a nested optional property in a ref', () => { + const schema: RJSFSchema = { + type: 'object', + definitions: { + nestedOptional: { + properties: { + nested: { + type: 'string', + default: 'foo', + }, + }, + }, + }, + properties: { + nestedOptionalProperty: { + $ref: '#/definitions/nestedOptional', + }, + requiredProperty: { + type: 'string', + default: 'foo', + }, + }, + required: ['requiredProperty', 'nestedOptionalProperty'], + }; + expect( + computeDefaults(testValidator, schema, { + rootSchema: schema, + experimental_defaultFormStateBehavior: { emptyObjectFields: 'populateRequiredDefaults' }, + }) + ).toEqual({ requiredProperty: 'foo', nestedOptionalProperty: {} }); + }); it('test an object with an optional property that has a nested required property with default', () => { const schema: RJSFSchema = { type: 'object', @@ -1912,5 +1974,108 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType expect(getDefaultFormState(testValidator, schema, formData)).toEqual(result); }); }); + it('should return undefined defaults for a required array property with minItems', () => { + const schema: RJSFSchema = { + type: 'object', + properties: { + requiredArray: { + type: 'array', + items: { type: 'string' }, + minItems: 2, + }, + }, + }; + expect(getDefaultFormState(testValidator, schema, undefined, schema, false)).toEqual({ + requiredArray: [undefined, undefined], + }); + }); + it('should not combine defaults with raw form data for a required array property with minItems', () => { + const schema: RJSFSchema = { + type: 'object', + properties: { + requiredArray: { + type: 'array', + items: { type: 'string' }, + minItems: 2, + }, + }, + }; + expect(getDefaultFormState(testValidator, schema, { requiredArray: ['raw0'] }, schema, false)).toEqual({ + requiredArray: ['raw0'], + }); + }); + it('should combine ALL defaults with raw form data for a required array property with minItems', () => { + const schema: RJSFSchema = { + type: 'object', + properties: { + requiredArray: { + type: 'array', + items: { type: 'string' }, + minItems: 2, + }, + }, + }; + expect( + getDefaultFormState(testValidator, schema, { requiredArray: ['raw0'] }, schema, false, { + arrayMinItems: { mergeExtraDefaults: true }, + }) + ).toEqual({ + requiredArray: ['raw0', undefined], + }); + }); + it('should return given defaults for a required array property with minItems', () => { + const schema: RJSFSchema = { + type: 'object', + properties: { + requiredArray: { + type: 'array', + items: { type: 'string', default: 'default0' }, + minItems: 2, + }, + }, + required: ['requiredArray'], + }; + expect( + getDefaultFormState(testValidator, schema, undefined, schema, false, { + arrayMinItems: { populate: 'requiredOnly' }, + }) + ).toEqual({ requiredArray: ['default0', 'default0'] }); + }); + it('should not combine defaults with raw form data for a required array property with minItems', () => { + const schema: RJSFSchema = { + type: 'object', + properties: { + requiredArray: { + type: 'array', + items: { type: 'string', default: 'default0' }, + minItems: 2, + }, + }, + required: ['requiredArray'], + }; + expect( + getDefaultFormState(testValidator, schema, { requiredArray: ['raw0'] }, schema, false, { + arrayMinItems: { populate: 'requiredOnly' }, + }) + ).toEqual({ requiredArray: ['raw0'] }); + }); + it('should combine ALL defaults with raw form data for a required array property with minItems', () => { + const schema: RJSFSchema = { + type: 'object', + properties: { + requiredArray: { + type: 'array', + items: { type: 'string', default: 'default0' }, + minItems: 2, + }, + }, + required: ['requiredArray'], + }; + expect( + getDefaultFormState(testValidator, schema, { requiredArray: ['raw0'] }, schema, false, { + arrayMinItems: { populate: 'requiredOnly', mergeExtraDefaults: true }, + }) + ).toEqual({ requiredArray: ['raw0', 'default0'] }); + }); }); }