From fc1c8c3317d6d711b6a67e595a6326a1e280e824 Mon Sep 17 00:00:00 2001 From: Appie Date: Mon, 9 Sep 2024 20:54:53 +0200 Subject: [PATCH] Bug: issue with dependencies computeDefaults (#4271) (#4282) * Fixed issue with dependencies computedDefaults * update changelog * refactoring based on feedback * created tests for the new created methods * organized file based on feedback * Update CHANGELOG.md --------- Co-authored-by: Abdallah Al-Soqatri Co-authored-by: Heath C <51679588+heath-freenome@users.noreply.github.com> --- CHANGELOG.md | 6 + .../utils/src/schema/getDefaultFormState.ts | 393 +++++---- .../test/schema/getDefaultFormStateTest.ts | 828 ++++++++++++++++++ 3 files changed, 1071 insertions(+), 156 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c810a447db..e649a01c0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,12 @@ should change the heading of the (upcoming) version to include a major version b --> +# 5.20.2 + +## @rjsf/utils + +- Fixes an issue with dependencies computeDefaults to ensure we can get the dependencies defaults [#4271](https://github.com/rjsf-team/react-jsonschema-form/issues/4271) + # 5.20.1 ## Dev / docs / playground diff --git a/packages/utils/src/schema/getDefaultFormState.ts b/packages/utils/src/schema/getDefaultFormState.ts index 71c6ff89bf..b1f5a096c8 100644 --- a/packages/utils/src/schema/getDefaultFormState.ts +++ b/packages/utils/src/schema/getDefaultFormState.ts @@ -141,12 +141,22 @@ function maybeAddDefaultToObject( } interface ComputeDefaultsProps { + /** Any defaults provided by the parent field in the schema */ parentDefaults?: T; + /** The options root schema, used to primarily to look up `$ref`s */ rootSchema?: S; + /** The current formData, if any, onto which to provide any missing defaults */ rawFormData?: T; + /** 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. + */ includeUndefinedValues?: boolean | 'excludeObjectChildren'; + /** The list of ref names currently being recursed, used to prevent infinite recursion */ _recurseList?: string[]; + /** Optional configuration object, if provided, allows users to override default form state behavior */ experimental_defaultFormStateBehavior?: Experimental_DefaultFormStateBehavior; + /** Optional flag, if true, indicates this schema was required in the parent schema. */ required?: boolean; } @@ -155,22 +165,15 @@ interface ComputeDefaultsProps * * @param validator - an implementation of the `ValidatorType` interface that will be used when necessary * @param rawSchema - The schema for which the default state is desired - * @param [props] - Optional props for this function - * @param [props.parentDefaults] - Any defaults provided by the parent field in the schema - * @param [props.rootSchema] - The options root schema, used to primarily to look up `$ref`s - * @param [props.rawFormData] - The current formData, if any, onto which to provide any missing defaults - * @param [props.includeUndefinedValues=false] - 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. - * @param [props._recurseList=[]] - The list of ref names currently being recursed, used to prevent infinite recursion - * @param [props.experimental_defaultFormStateBehavior] Optional configuration object, if provided, allows users to override default form state behavior - * @param [props.required] - Optional flag, if true, indicates this schema was required in the parent schema. + * @param {ComputeDefaultsProps} computeDefaultsProps - Optional props for this function * @returns - The resulting `formData` with all the defaults provided */ export function computeDefaults( validator: ValidatorType, rawSchema: S, - { + computeDefaultsProps: ComputeDefaultsProps = {} +): T | T[] | undefined { + const { parentDefaults, rawFormData, rootSchema = {} as S, @@ -178,8 +181,7 @@ export function computeDefaults = {} -): T | T[] | undefined { + } = computeDefaultsProps; const formData: T = (isObject(rawFormData) ? rawFormData : {}) as T; const schema: S = isObject(rawSchema) ? rawSchema : ({} as S); // Compute the defaults recursively: give highest priority to deepest nodes. @@ -202,7 +204,12 @@ export function computeDefaults(refName, rootSchema); } } else if (DEPENDENCIES_KEY in schema) { - const resolvedSchema = resolveDependencies(validator, schema, rootSchema, false, [], formData); + // Get the default if set from properties to ensure the dependencies conditions are resolved based on it + const defaultFormData: T = { + ...formData, + ...getDefaultBasedOnSchemaType(validator, schema, computeDefaultsProps, defaults), + }; + const resolvedSchema = resolveDependencies(validator, schema, rootSchema, false, [], defaultFormData); schemaToCompute = resolvedSchema[0]; // pick the first element from resolve dependencies } else if (isFixedItems(schema)) { defaults = (schema.items! as S[]).map((itemSchema: S, idx: number) => @@ -269,164 +276,238 @@ export function computeDefaults(schema)) { - // We need to recurse for object schema inner default values. - case 'object': { - // This is a custom addition that fixes this issue: - // https://github.com/rjsf-team/react-jsonschema-form/issues/3832 - const retrievedSchema = - experimental_defaultFormStateBehavior?.allOf === 'populateDefaults' && ALL_OF_KEY in schema - ? retrieveSchema(validator, schema, rootSchema, formData) - : schema; - const objectDefaults = Object.keys(retrievedSchema.properties || {}).reduce( - (acc: GenericObjectType, key: string) => { - // Compute the defaults for this node, with the parent defaults we might - // have from a previous run: defaults[key]. - const computedDefault = computeDefaults(validator, get(retrievedSchema, [PROPERTIES_KEY, key]), { - rootSchema, - _recurseList, - experimental_defaultFormStateBehavior, - includeUndefinedValues: includeUndefinedValues === true, - parentDefaults: get(defaults, [key]), - rawFormData: get(formData, [key]), - required: retrievedSchema.required?.includes(key), - }); - maybeAddDefaultToObject( - acc, - key, - computedDefault, - includeUndefinedValues, - required, - retrievedSchema.required, - experimental_defaultFormStateBehavior - ); - return acc; - }, - {} - ) as T; - if (retrievedSchema.additionalProperties) { - // as per spec additionalProperties may be either schema or boolean - const additionalPropertiesSchema = isObject(retrievedSchema.additionalProperties) - ? retrievedSchema.additionalProperties - : {}; + const defaultBasedOnSchemaType = getDefaultBasedOnSchemaType(validator, schema, computeDefaultsProps, defaults); - const keys = new Set(); - if (isObject(defaults)) { - Object.keys(defaults as GenericObjectType) - .filter((key) => !retrievedSchema.properties || !retrievedSchema.properties[key]) - .forEach((key) => keys.add(key)); - } - const formDataRequired: string[] = []; - Object.keys(formData as GenericObjectType) - .filter((key) => !retrievedSchema.properties || !retrievedSchema.properties[key]) - .forEach((key) => { - keys.add(key); - formDataRequired.push(key); - }); - keys.forEach((key) => { - const computedDefault = computeDefaults(validator, additionalPropertiesSchema as S, { - rootSchema, - _recurseList, - experimental_defaultFormStateBehavior, - includeUndefinedValues: includeUndefinedValues === true, - parentDefaults: get(defaults, [key]), - rawFormData: get(formData, [key]), - required: retrievedSchema.required?.includes(key), - }); - // Since these are additional properties we don't need to add the `experimental_defaultFormStateBehavior` prop - maybeAddDefaultToObject( - objectDefaults as GenericObjectType, - key, - computedDefault, - includeUndefinedValues, - required, - formDataRequired - ); - }); - } - return objectDefaults; - } - case 'array': { - const neverPopulate = experimental_defaultFormStateBehavior?.arrayMinItems?.populate === 'never'; - const ignoreMinItemsFlagSet = experimental_defaultFormStateBehavior?.arrayMinItems?.populate === 'requiredOnly'; - const isSkipEmptyDefaults = experimental_defaultFormStateBehavior?.emptyObjectFields === 'skipEmptyDefaults'; - const computeSkipPopulate = - experimental_defaultFormStateBehavior?.arrayMinItems?.computeSkipPopulate ?? (() => false); + return defaultBasedOnSchemaType ?? defaults; +} - const emptyDefault = isSkipEmptyDefaults ? undefined : []; +/** Computes the default value for objects. + * + * @param validator - an implementation of the `ValidatorType` interface that will be used when necessary + * @param rawSchema - The schema for which the default state is desired + * @param {ComputeDefaultsProps} computeDefaultsProps - Optional props for this function + * @param defaults - Optional props for this function + * @returns - The default value based on the schema type if they are defined for object or array schemas. + */ +export function getObjectDefaults( + validator: ValidatorType, + rawSchema: S, + { + rawFormData, + rootSchema = {} as S, + includeUndefinedValues = false, + _recurseList = [], + experimental_defaultFormStateBehavior = undefined, + required, + }: ComputeDefaultsProps = {}, + defaults?: T | T[] | undefined +): T { + { + const formData: T = (isObject(rawFormData) ? rawFormData : {}) as T; + const schema: S = rawSchema; + // This is a custom addition that fixes this issue: + // https://github.com/rjsf-team/react-jsonschema-form/issues/3832 + const retrievedSchema = + experimental_defaultFormStateBehavior?.allOf === 'populateDefaults' && ALL_OF_KEY in schema + ? retrieveSchema(validator, schema, rootSchema, formData) + : schema; + const objectDefaults = Object.keys(retrievedSchema.properties || {}).reduce( + (acc: GenericObjectType, key: string) => { + // Compute the defaults for this node, with the parent defaults we might + // have from a previous run: defaults[key]. + const computedDefault = computeDefaults(validator, get(retrievedSchema, [PROPERTIES_KEY, key]), { + rootSchema, + _recurseList, + experimental_defaultFormStateBehavior, + includeUndefinedValues: includeUndefinedValues === true, + parentDefaults: get(defaults, [key]), + rawFormData: get(formData, [key]), + required: retrievedSchema.required?.includes(key), + }); + maybeAddDefaultToObject( + acc, + key, + computedDefault, + includeUndefinedValues, + required, + retrievedSchema.required, + experimental_defaultFormStateBehavior + ); + return acc; + }, + {} + ) as T; + if (retrievedSchema.additionalProperties) { + // as per spec additionalProperties may be either schema or boolean + const additionalPropertiesSchema = isObject(retrievedSchema.additionalProperties) + ? retrievedSchema.additionalProperties + : {}; - // Inject defaults into existing array defaults - if (Array.isArray(defaults)) { - defaults = defaults.map((item, idx) => { - const schemaItem: S = getInnerSchemaForArrayItem(schema, AdditionalItemsHandling.Fallback, idx); - return computeDefaults(validator, schemaItem, { - rootSchema, - _recurseList, - experimental_defaultFormStateBehavior, - parentDefaults: item, - required, - }); - }) as T[]; + const keys = new Set(); + if (isObject(defaults)) { + Object.keys(defaults as GenericObjectType) + .filter((key) => !retrievedSchema.properties || !retrievedSchema.properties[key]) + .forEach((key) => keys.add(key)); } + const formDataRequired: string[] = []; + Object.keys(formData as GenericObjectType) + .filter((key) => !retrievedSchema.properties || !retrievedSchema.properties[key]) + .forEach((key) => { + keys.add(key); + formDataRequired.push(key); + }); + keys.forEach((key) => { + const computedDefault = computeDefaults(validator, additionalPropertiesSchema as S, { + rootSchema, + _recurseList, + experimental_defaultFormStateBehavior, + includeUndefinedValues: includeUndefinedValues === true, + parentDefaults: get(defaults, [key]), + rawFormData: get(formData, [key]), + required: retrievedSchema.required?.includes(key), + }); + // Since these are additional properties we don't need to add the `experimental_defaultFormStateBehavior` prop + maybeAddDefaultToObject( + objectDefaults as GenericObjectType, + key, + computedDefault, + includeUndefinedValues, + required, + formDataRequired + ); + }); + } + return objectDefaults; + } +} - // Deeply inject defaults into already existing form data - if (Array.isArray(rawFormData)) { - const schemaItem: S = getInnerSchemaForArrayItem(schema); - if (neverPopulate) { - defaults = rawFormData; - } else { - defaults = rawFormData.map((item: T, idx: number) => { - return computeDefaults(validator, schemaItem, { - rootSchema, - _recurseList, - experimental_defaultFormStateBehavior, - rawFormData: item, - parentDefaults: get(defaults, [idx]), - required, - }); - }) as T[]; - } - } +/** Computes the default value for arrays. + * + * @param validator - an implementation of the `ValidatorType` interface that will be used when necessary + * @param rawSchema - The schema for which the default state is desired + * @param {ComputeDefaultsProps} computeDefaultsProps - Optional props for this function + * @param defaults - Optional props for this function + * @returns - The default value based on the schema type if they are defined for object or array schemas. + */ +export function getArrayDefaults( + validator: ValidatorType, + rawSchema: S, + { + rawFormData, + rootSchema = {} as S, + _recurseList = [], + experimental_defaultFormStateBehavior = undefined, + required, + }: ComputeDefaultsProps = {}, + defaults?: T | T[] | undefined +): T | T[] | undefined { + const schema: S = rawSchema; - if (neverPopulate) { - return defaults ?? emptyDefault; - } - if (ignoreMinItemsFlagSet && !required) { - // If no form data exists or defaults are set leave the field empty/non-existent, otherwise - // return form data/defaults - return defaults ? defaults : undefined; - } + const neverPopulate = experimental_defaultFormStateBehavior?.arrayMinItems?.populate === 'never'; + const ignoreMinItemsFlagSet = experimental_defaultFormStateBehavior?.arrayMinItems?.populate === 'requiredOnly'; + const isSkipEmptyDefaults = experimental_defaultFormStateBehavior?.emptyObjectFields === 'skipEmptyDefaults'; + const computeSkipPopulate = + experimental_defaultFormStateBehavior?.arrayMinItems?.computeSkipPopulate ?? (() => false); - const defaultsLength = Array.isArray(defaults) ? defaults.length : 0; - if ( - !schema.minItems || - isMultiSelect(validator, schema, rootSchema) || - computeSkipPopulate(validator, schema, rootSchema) || - schema.minItems <= defaultsLength - ) { - return defaults ? defaults : emptyDefault; - } + const emptyDefault = isSkipEmptyDefaults ? undefined : []; - const defaultEntries: T[] = (defaults || []) as T[]; - const fillerSchema: S = getInnerSchemaForArrayItem(schema, AdditionalItemsHandling.Invert); - const fillerDefault = fillerSchema.default; + // Inject defaults into existing array defaults + if (Array.isArray(defaults)) { + defaults = defaults.map((item, idx) => { + const schemaItem: S = getInnerSchemaForArrayItem(schema, AdditionalItemsHandling.Fallback, idx); + return computeDefaults(validator, schemaItem, { + rootSchema, + _recurseList, + experimental_defaultFormStateBehavior, + parentDefaults: item, + required, + }); + }) as T[]; + } - // Calculate filler entries for remaining items (minItems - existing raw data/defaults) - const fillerEntries: T[] = new Array(schema.minItems - defaultsLength).fill( - computeDefaults(validator, fillerSchema, { - parentDefaults: fillerDefault, + // Deeply inject defaults into already existing form data + if (Array.isArray(rawFormData)) { + const schemaItem: S = getInnerSchemaForArrayItem(schema); + if (neverPopulate) { + defaults = rawFormData; + } else { + defaults = rawFormData.map((item: T, idx: number) => { + return computeDefaults(validator, schemaItem, { rootSchema, _recurseList, experimental_defaultFormStateBehavior, + rawFormData: item, + parentDefaults: get(defaults, [idx]), required, - }) - ) as T[]; - // then fill up the rest with either the item default or empty, up to minItems - return defaultEntries.concat(fillerEntries); + }); + }) as T[]; } } - return defaults; + if (neverPopulate) { + return defaults ?? emptyDefault; + } + if (ignoreMinItemsFlagSet && !required) { + // If no form data exists or defaults are set leave the field empty/non-existent, otherwise + // return form data/defaults + return defaults ? defaults : undefined; + } + + const defaultsLength = Array.isArray(defaults) ? defaults.length : 0; + if ( + !schema.minItems || + isMultiSelect(validator, schema, rootSchema) || + computeSkipPopulate(validator, schema, rootSchema) || + schema.minItems <= defaultsLength + ) { + return defaults ? defaults : emptyDefault; + } + + const defaultEntries: T[] = (defaults || []) as T[]; + const fillerSchema: S = getInnerSchemaForArrayItem(schema, AdditionalItemsHandling.Invert); + const fillerDefault = fillerSchema.default; + + // Calculate filler entries for remaining items (minItems - existing raw data/defaults) + const fillerEntries: T[] = new Array(schema.minItems - defaultsLength).fill( + computeDefaults(validator, fillerSchema, { + parentDefaults: fillerDefault, + rootSchema, + _recurseList, + experimental_defaultFormStateBehavior, + required, + }) + ) as T[]; + // then fill up the rest with either the item default or empty, up to minItems + return defaultEntries.concat(fillerEntries); +} + +/** Computes the default value based on the schema type. + * + * @param validator - an implementation of the `ValidatorType` interface that will be used when necessary + * @param rawSchema - The schema for which the default state is desired + * @param {ComputeDefaultsProps} computeDefaultsProps - Optional props for this function + * @param defaults - Optional props for this function + * @returns - The default value based on the schema type if they are defined for object or array schemas. + */ +export function getDefaultBasedOnSchemaType< + T = any, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = any +>( + validator: ValidatorType, + rawSchema: S, + computeDefaultsProps: ComputeDefaultsProps = {}, + defaults?: T | T[] | undefined +): T | T[] | void { + switch (getSchemaType(rawSchema)) { + // We need to recurse for object schema inner default values. + case 'object': { + return getObjectDefaults(validator, rawSchema, computeDefaultsProps, defaults); + } + case 'array': { + return getArrayDefaults(validator, rawSchema, computeDefaultsProps, defaults); + } + } } /** Returns the superset of `formData` that includes the given set updated to include any missing fields that have diff --git a/packages/utils/test/schema/getDefaultFormStateTest.ts b/packages/utils/test/schema/getDefaultFormStateTest.ts index 35b8d3718b..e773d91fc8 100644 --- a/packages/utils/test/schema/getDefaultFormStateTest.ts +++ b/packages/utils/test/schema/getDefaultFormStateTest.ts @@ -2,7 +2,10 @@ import { createSchemaUtils, getDefaultFormState, RJSFSchema } from '../../src'; import { AdditionalItemsHandling, computeDefaults, + getArrayDefaults, + getDefaultBasedOnSchemaType, getInnerSchemaForArrayItem, + getObjectDefaults, } from '../../src/schema/getDefaultFormState'; import { RECURSIVE_REF, RECURSIVE_REF_ALLOF } from '../testUtils/testData'; import { TestValidatorType } from './types'; @@ -357,6 +360,744 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType expect(computeDefaults(testValidator, schema)).toBe(undefined); }); }); + describe('getDefaultBasedOnSchemaType()', () => { + it('test an object with an optional property that has a nested required property', () => { + const schema: RJSFSchema = { + type: 'object', + properties: { + optionalProperty: { + type: 'object', + properties: { + nestedRequiredProperty: { + type: 'string', + }, + }, + required: ['nestedRequiredProperty'], + }, + requiredProperty: { + type: 'string', + default: 'foo', + }, + }, + required: ['requiredProperty'], + }; + + expect(getDefaultBasedOnSchemaType(testValidator, schema, { rootSchema: schema })).toEqual({ + requiredProperty: 'foo', + }); + }); + it('test an object with an optional property that has a nested required property with default', () => { + const schema: RJSFSchema = { + type: 'object', + properties: { + optionalProperty: { + type: 'object', + properties: { + nestedRequiredProperty: { + type: 'string', + default: '', + }, + }, + required: ['nestedRequiredProperty'], + }, + requiredProperty: { + type: 'string', + default: 'foo', + }, + }, + required: ['requiredProperty'], + }; + expect(getDefaultBasedOnSchemaType(testValidator, schema, { rootSchema: schema })).toEqual({ + requiredProperty: 'foo', + optionalProperty: { nestedRequiredProperty: '' }, + }); + }); + it('test an object with an optional property that has a nested required property and includeUndefinedValues', () => { + const schema: RJSFSchema = { + type: 'object', + properties: { + optionalProperty: { + type: 'object', + properties: { + nestedRequiredProperty: { + type: 'object', + properties: { + undefinedProperty: { + type: 'string', + }, + }, + }, + }, + required: ['nestedRequiredProperty'], + }, + requiredProperty: { + type: 'string', + default: 'foo', + }, + }, + required: ['requiredProperty'], + }; + expect( + getDefaultBasedOnSchemaType(testValidator, schema, { rootSchema: schema, includeUndefinedValues: true }) + ).toEqual({ + optionalProperty: { + nestedRequiredProperty: { + undefinedProperty: undefined, + }, + }, + requiredProperty: 'foo', + }); + }); + it("test an object with an optional property that has a nested required property and includeUndefinedValues is 'excludeObjectChildren'", () => { + const schema: RJSFSchema = { + type: 'object', + properties: { + optionalNumberProperty: { + type: 'number', + }, + optionalObjectProperty: { + type: 'object', + properties: { + nestedRequiredProperty: { + type: 'object', + properties: { + undefinedProperty: { + type: 'string', + }, + }, + }, + }, + required: ['nestedRequiredProperty'], + }, + requiredProperty: { + type: 'string', + default: 'foo', + }, + }, + required: ['requiredProperty'], + }; + expect( + getDefaultBasedOnSchemaType(testValidator, schema, { + rootSchema: schema, + includeUndefinedValues: 'excludeObjectChildren', + }) + ).toEqual({ + optionalNumberProperty: undefined, + optionalObjectProperty: { + nestedRequiredProperty: {}, + }, + requiredProperty: 'foo', + }); + }); + it('test an object with an additionalProperties', () => { + const schema: RJSFSchema = { + type: 'object', + properties: { + requiredProperty: { + type: 'string', + default: 'foo', + }, + }, + additionalProperties: true, + required: ['requiredProperty'], + default: { + foo: 'bar', + }, + }; + expect(getDefaultBasedOnSchemaType(testValidator, schema, { rootSchema: schema }, { foo: 'bar' })).toEqual({ + requiredProperty: 'foo', + foo: 'bar', + }); + }); + it('test an object with an additionalProperties and includeUndefinedValues', () => { + const schema: RJSFSchema = { + type: 'object', + properties: { + requiredProperty: { + type: 'string', + default: 'foo', + }, + }, + additionalProperties: { + type: 'string', + }, + required: ['requiredProperty'], + default: { + foo: 'bar', + }, + }; + expect( + getDefaultBasedOnSchemaType( + testValidator, + schema, + { rootSchema: schema, includeUndefinedValues: true }, + { foo: 'bar' } + ) + ).toEqual({ + requiredProperty: 'foo', + foo: 'bar', + }); + }); + it('test an object with additionalProperties type object with defaults and formdata', () => { + const schema: RJSFSchema = { + type: 'object', + properties: { + test: { + title: 'Test', + type: 'object', + properties: { + foo: { + type: 'string', + }, + }, + additionalProperties: { + type: 'object', + properties: { + host: { + title: 'Host', + type: 'string', + default: 'localhost', + }, + port: { + title: 'Port', + type: 'integer', + default: 389, + }, + }, + }, + }, + }, + }; + expect( + getDefaultBasedOnSchemaType(testValidator, schema, { + rootSchema: schema, + rawFormData: { test: { foo: 'x', newKey: {} } }, + }) + ).toEqual({ + test: { + newKey: { + host: 'localhost', + port: 389, + }, + }, + }); + }); + it('test an object with additionalProperties type object with no defaults and formdata', () => { + const schema: RJSFSchema = { + type: 'object', + properties: { + test: { + title: 'Test', + type: 'object', + properties: { + foo: { + type: 'string', + }, + }, + additionalProperties: { + type: 'object', + properties: { + host: { + title: 'Host', + type: 'string', + }, + port: { + title: 'Port', + type: 'integer', + }, + }, + }, + }, + }, + }; + expect( + getDefaultBasedOnSchemaType(testValidator, schema, { + rootSchema: schema, + rawFormData: { test: { foo: 'x', newKey: {} } }, + }) + ).toEqual({ + test: { + newKey: {}, + }, + }); + }); + it('test an object with additionalProperties type object with no defaults and non-object formdata', () => { + const schema: RJSFSchema = { + type: 'object', + properties: { + test: { + title: 'Test', + type: 'object', + properties: { + foo: { + type: 'string', + }, + }, + additionalProperties: { + type: 'object', + properties: { + host: { + title: 'Host', + type: 'string', + }, + port: { + title: 'Port', + type: 'integer', + }, + }, + }, + }, + }, + }; + expect( + getDefaultBasedOnSchemaType(testValidator, schema, { + rootSchema: schema, + rawFormData: {}, + }) + ).toEqual({}); + }); + it('test an array with defaults', () => { + const schema: RJSFSchema = { + type: 'array', + minItems: 4, + default: ['Raphael', 'Michaelangelo'], + items: { + type: 'string', + default: 'Unknown', + }, + }; + + expect( + getDefaultBasedOnSchemaType( + testValidator, + schema, + { + rootSchema: schema, + includeUndefinedValues: 'excludeObjectChildren', + }, + ['Raphael', 'Michaelangelo'] + ) + ).toEqual(['Raphael', 'Michaelangelo', 'Unknown', 'Unknown']); + }); + it('test an array with no defaults', () => { + const schema: RJSFSchema = { + type: 'array', + minItems: 4, + items: { + type: 'string', + }, + }; + + expect( + getArrayDefaults(testValidator, schema, { + rootSchema: schema, + includeUndefinedValues: 'excludeObjectChildren', + }) + ).toEqual([]); + }); + it('test computeDefaults handles an invalid property schema', () => { + const schema: RJSFSchema = { + type: 'object', + properties: { + invalidProperty: 'not a valid property value', + }, + } as RJSFSchema; + expect( + getDefaultBasedOnSchemaType(testValidator, schema, { + rootSchema: schema, + includeUndefinedValues: 'excludeObjectChildren', + }) + ).toEqual({}); + }); + it('test with a recursive allof schema', () => { + expect( + getDefaultBasedOnSchemaType(testValidator, RECURSIVE_REF_ALLOF, { rootSchema: RECURSIVE_REF_ALLOF }) + ).toEqual({ + value: [undefined], + }); + }); + it('test computeDefaults returns undefined with simple schema and no optional args', () => { + const schema: RJSFSchema = { type: 'string' }; + expect(getDefaultBasedOnSchemaType(testValidator, schema)).toBe(undefined); + }); + }); + describe('getObjectDefaults()', () => { + it('test an object with an optional property that has a nested required property', () => { + const schema: RJSFSchema = { + type: 'object', + properties: { + optionalProperty: { + type: 'object', + properties: { + nestedRequiredProperty: { + type: 'string', + }, + }, + required: ['nestedRequiredProperty'], + }, + requiredProperty: { + type: 'string', + default: 'foo', + }, + }, + required: ['requiredProperty'], + }; + + expect(getObjectDefaults(testValidator, schema, { rootSchema: schema })).toEqual({ + requiredProperty: 'foo', + }); + }); + it('test an object with an optional property that has a nested required property with default', () => { + const schema: RJSFSchema = { + type: 'object', + properties: { + optionalProperty: { + type: 'object', + properties: { + nestedRequiredProperty: { + type: 'string', + default: '', + }, + }, + required: ['nestedRequiredProperty'], + }, + requiredProperty: { + type: 'string', + default: 'foo', + }, + }, + required: ['requiredProperty'], + }; + expect(getObjectDefaults(testValidator, schema, { rootSchema: schema })).toEqual({ + requiredProperty: 'foo', + optionalProperty: { nestedRequiredProperty: '' }, + }); + }); + it('test an object with an optional property that has a nested required property and includeUndefinedValues', () => { + const schema: RJSFSchema = { + type: 'object', + properties: { + optionalProperty: { + type: 'object', + properties: { + nestedRequiredProperty: { + type: 'object', + properties: { + undefinedProperty: { + type: 'string', + }, + }, + }, + }, + required: ['nestedRequiredProperty'], + }, + requiredProperty: { + type: 'string', + default: 'foo', + }, + }, + required: ['requiredProperty'], + }; + expect(getObjectDefaults(testValidator, schema, { rootSchema: schema, includeUndefinedValues: true })).toEqual({ + optionalProperty: { + nestedRequiredProperty: { + undefinedProperty: undefined, + }, + }, + requiredProperty: 'foo', + }); + }); + it("test an object with an optional property that has a nested required property and includeUndefinedValues is 'excludeObjectChildren'", () => { + const schema: RJSFSchema = { + type: 'object', + properties: { + optionalNumberProperty: { + type: 'number', + }, + optionalObjectProperty: { + type: 'object', + properties: { + nestedRequiredProperty: { + type: 'object', + properties: { + undefinedProperty: { + type: 'string', + }, + }, + }, + }, + required: ['nestedRequiredProperty'], + }, + requiredProperty: { + type: 'string', + default: 'foo', + }, + }, + required: ['requiredProperty'], + }; + expect( + getObjectDefaults(testValidator, schema, { + rootSchema: schema, + includeUndefinedValues: 'excludeObjectChildren', + }) + ).toEqual({ + optionalNumberProperty: undefined, + optionalObjectProperty: { + nestedRequiredProperty: {}, + }, + requiredProperty: 'foo', + }); + }); + it('test an object with an additionalProperties', () => { + const schema: RJSFSchema = { + type: 'object', + properties: { + requiredProperty: { + type: 'string', + default: 'foo', + }, + }, + additionalProperties: true, + required: ['requiredProperty'], + default: { + foo: 'bar', + }, + }; + expect(getObjectDefaults(testValidator, schema, { rootSchema: schema }, { foo: 'bar' })).toEqual({ + requiredProperty: 'foo', + foo: 'bar', + }); + }); + it('test an object with an additionalProperties and includeUndefinedValues', () => { + const schema: RJSFSchema = { + type: 'object', + properties: { + requiredProperty: { + type: 'string', + default: 'foo', + }, + }, + additionalProperties: { + type: 'string', + }, + required: ['requiredProperty'], + default: { + foo: 'bar', + }, + }; + expect( + getObjectDefaults( + testValidator, + schema, + { + rootSchema: schema, + includeUndefinedValues: true, + }, + { + foo: 'bar', + } + ) + ).toEqual({ requiredProperty: 'foo', foo: 'bar' }); + }); + it('test an object with additionalProperties type object with defaults and formdata', () => { + const schema: RJSFSchema = { + type: 'object', + properties: { + test: { + title: 'Test', + type: 'object', + properties: { + foo: { + type: 'string', + }, + }, + additionalProperties: { + type: 'object', + properties: { + host: { + title: 'Host', + type: 'string', + default: 'localhost', + }, + port: { + title: 'Port', + type: 'integer', + default: 389, + }, + }, + }, + }, + }, + }; + expect( + getObjectDefaults(testValidator, schema, { + rootSchema: schema, + rawFormData: { test: { foo: 'x', newKey: {} } }, + }) + ).toEqual({ + test: { + newKey: { + host: 'localhost', + port: 389, + }, + }, + }); + }); + it('test an object with additionalProperties type object with no defaults and formdata', () => { + const schema: RJSFSchema = { + type: 'object', + properties: { + test: { + title: 'Test', + type: 'object', + properties: { + foo: { + type: 'string', + }, + }, + additionalProperties: { + type: 'object', + properties: { + host: { + title: 'Host', + type: 'string', + }, + port: { + title: 'Port', + type: 'integer', + }, + }, + }, + }, + }, + }; + expect( + getObjectDefaults(testValidator, schema, { + rootSchema: schema, + rawFormData: { test: { foo: 'x', newKey: {} } }, + }) + ).toEqual({ + test: { + newKey: {}, + }, + }); + }); + it('test an object with additionalProperties type object with no defaults and non-object formdata', () => { + const schema: RJSFSchema = { + type: 'object', + properties: { + test: { + title: 'Test', + type: 'object', + properties: { + foo: { + type: 'string', + }, + }, + additionalProperties: { + type: 'object', + properties: { + host: { + title: 'Host', + type: 'string', + }, + port: { + title: 'Port', + type: 'integer', + }, + }, + }, + }, + }, + }; + expect( + getObjectDefaults(testValidator, schema, { + rootSchema: schema, + rawFormData: {}, + }) + ).toEqual({}); + }); + it('test computeDefaults handles an invalid property schema', () => { + const schema: RJSFSchema = { + type: 'object', + properties: { + invalidProperty: 'not a valid property value', + }, + } as RJSFSchema; + expect( + getObjectDefaults(testValidator, schema, { + rootSchema: schema, + includeUndefinedValues: 'excludeObjectChildren', + }) + ).toEqual({}); + }); + it('test with a recursive allof schema', () => { + expect(getObjectDefaults(testValidator, RECURSIVE_REF_ALLOF, { rootSchema: RECURSIVE_REF_ALLOF })).toEqual({ + value: [undefined], + }); + }); + it('test computeDefaults returns undefined with simple schema and no optional args', () => { + const schema: RJSFSchema = { type: 'object' }; + expect(getObjectDefaults(testValidator, schema)).toStrictEqual({}); + }); + }); + describe('getArrayDefaults()', () => { + it('test an array with defaults', () => { + const schema: RJSFSchema = { + type: 'array', + minItems: 4, + default: ['Raphael', 'Michaelangelo'], + items: { + type: 'string', + default: 'Unknown', + }, + }; + + expect( + getArrayDefaults( + testValidator, + schema, + { + rootSchema: schema, + includeUndefinedValues: 'excludeObjectChildren', + }, + ['Raphael', 'Michaelangelo'] + ) + ).toEqual(['Raphael', 'Michaelangelo', 'Unknown', 'Unknown']); + }); + it('test an array with no defaults', () => { + const schema: RJSFSchema = { + type: 'array', + minItems: 4, + items: { + type: 'string', + }, + }; + + expect( + getArrayDefaults(testValidator, schema, { + rootSchema: schema, + includeUndefinedValues: 'excludeObjectChildren', + }) + ).toEqual([]); + }); + it('test computeDefaults handles an invalid array schema', () => { + const schema: RJSFSchema = { + type: 'array', + items: 'not a valid item value', + } as RJSFSchema; + expect( + getArrayDefaults(testValidator, schema, { + rootSchema: schema, + includeUndefinedValues: 'excludeObjectChildren', + }) + ).toEqual([]); + }); + it('test computeDefaults returns undefined with simple schema and no optional args', () => { + const schema: RJSFSchema = { type: 'array' }; + expect(getArrayDefaults(testValidator, schema)).toStrictEqual([]); + }); + }); describe('default form state behavior: ignore min items unless required', () => { it('should return empty data for an optional array property with minItems', () => { const schema: RJSFSchema = { @@ -2660,6 +3401,93 @@ export default function getDefaultFormStateTest(testValidator: TestValidatorType }, }); }); + it('should populate defaults for properties to ensure the dependencies conditions are resolved based on it', () => { + const schema: RJSFSchema = { + type: 'object', + required: ['authentication'], + properties: { + authentication: { + title: 'Authentication', + type: 'object', + properties: { + credentialType: { + title: 'Credential type', + type: 'string', + default: 'username', + oneOf: [ + { + const: 'username', + title: 'Username and password', + }, + { + const: 'secret', + title: 'SSO', + }, + ], + }, + }, + dependencies: { + credentialType: { + allOf: [ + { + if: { + properties: { + credentialType: { + const: 'username', + }, + }, + }, + then: { + properties: { + usernameAndPassword: { + type: 'object', + properties: { + username: { + type: 'string', + title: 'Username', + }, + password: { + type: 'string', + title: 'Password', + }, + }, + required: ['username', 'password'], + }, + }, + required: ['usernameAndPassword'], + }, + }, + { + if: { + properties: { + credentialType: { + const: 'secret', + }, + }, + }, + then: { + properties: { + sso: { + type: 'string', + title: 'SSO', + }, + }, + required: ['sso'], + }, + }, + ], + }, + }, + }, + }, + }; + expect(getDefaultFormState(testValidator, schema)).toEqual({ + authentication: { + credentialType: 'username', + usernameAndPassword: {}, + }, + }); + }); it('should populate defaults for nested dependencies when formData passed to computeDefaults is undefined', () => { const schema: RJSFSchema = { type: 'object',