From b1a7903f005f8e27e90c6857eb93aa02753da717 Mon Sep 17 00:00:00 2001 From: Abdallah Al-Soqatri Date: Fri, 23 Aug 2024 01:51:52 +0200 Subject: [PATCH 1/6] Fixed issue with dependencies computedDefaults --- .../utils/src/schema/getDefaultFormState.ts | 323 +++++++++--------- .../test/schema/getDefaultFormStateTest.ts | 87 +++++ 2 files changed, 252 insertions(+), 158 deletions(-) diff --git a/packages/utils/src/schema/getDefaultFormState.ts b/packages/utils/src/schema/getDefaultFormState.ts index 71c6ff89bf..320b6e589c 100644 --- a/packages/utils/src/schema/getDefaultFormState.ts +++ b/packages/utils/src/schema/getDefaultFormState.ts @@ -188,6 +188,166 @@ export function computeDefaults { + switch (getSchemaType(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 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); + + const emptyDefault = isSkipEmptyDefaults ? undefined : []; + + // 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[]; + } + + // 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[]; + } + } + + 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); + } + } + }; + if (isObject(defaults) && isObject(schema.default)) { // For object defaults, only override parent defaults that are defined in // schema.default. @@ -202,7 +362,9 @@ 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 dependency conditions are resolved based on it + const defaultFormData: T = { ...formData, ...getDefaultBasedOnSchemaType() }; + 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 +431,9 @@ 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 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); - - const emptyDefault = isSkipEmptyDefaults ? undefined : []; - - // 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[]; - } - - // 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[]; - } - } - - 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); - } - } + const defaultBasedOnSchemaType = getDefaultBasedOnSchemaType(); - return defaults; + return defaultBasedOnSchemaType ?? 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..7158012f2c 100644 --- a/packages/utils/test/schema/getDefaultFormStateTest.ts +++ b/packages/utils/test/schema/getDefaultFormStateTest.ts @@ -2660,6 +2660,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', From 47d9853e028773244cd576a95e30b19f522bbc55 Mon Sep 17 00:00:00 2001 From: Abdallah Al-Soqatri Date: Fri, 23 Aug 2024 02:32:41 +0200 Subject: [PATCH 2/6] update changelog --- CHANGELOG.md | 4 ++++ packages/utils/src/schema/getDefaultFormState.ts | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c810a447db..0830c1cb40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,10 @@ should change the heading of the (upcoming) version to include a major version b - Updated the peer dependencies to `5.20.x` due to types and API changes in `@rjsf/utils` +## @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.0 ## @rjsf/core diff --git a/packages/utils/src/schema/getDefaultFormState.ts b/packages/utils/src/schema/getDefaultFormState.ts index 320b6e589c..515083398d 100644 --- a/packages/utils/src/schema/getDefaultFormState.ts +++ b/packages/utils/src/schema/getDefaultFormState.ts @@ -362,7 +362,7 @@ export function computeDefaults(refName, rootSchema); } } else if (DEPENDENCIES_KEY in schema) { - // Get the default if set from properties to ensure the dependency conditions are resolved based on it + // Get the default if set from properties to ensure the dependencies conditions are resolved based on it const defaultFormData: T = { ...formData, ...getDefaultBasedOnSchemaType() }; const resolvedSchema = resolveDependencies(validator, schema, rootSchema, false, [], defaultFormData); schemaToCompute = resolvedSchema[0]; // pick the first element from resolve dependencies From add62e2b80e3911cb1dc99d59733f45e42e0b88d Mon Sep 17 00:00:00 2001 From: Abdallah Al-Soqatri Date: Sat, 31 Aug 2024 19:25:35 +0200 Subject: [PATCH 3/6] refactoring based on feedback --- .../utils/src/schema/getDefaultFormState.ts | 422 ++++++++++-------- 1 file changed, 247 insertions(+), 175 deletions(-) diff --git a/packages/utils/src/schema/getDefaultFormState.ts b/packages/utils/src/schema/getDefaultFormState.ts index 515083398d..794a898710 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. @@ -188,166 +190,6 @@ export function computeDefaults { - switch (getSchemaType(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 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); - - const emptyDefault = isSkipEmptyDefaults ? undefined : []; - - // 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[]; - } - - // 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[]; - } - } - - 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); - } - } - }; - if (isObject(defaults) && isObject(schema.default)) { // For object defaults, only override parent defaults that are defined in // schema.default. @@ -363,7 +205,10 @@ export function computeDefaults(validator, schema, rootSchema, false, [], defaultFormData); schemaToCompute = resolvedSchema[0]; // pick the first element from resolve dependencies } else if (isFixedItems(schema)) { @@ -431,11 +276,238 @@ export function computeDefaults( + validator: ValidatorType, + rawSchema: S, + defaults: T | T[] | undefined, + computeDefaultsProps: ComputeDefaultsProps = {} +): T | T[] | void { + const schema: S = isObject(rawSchema) ? rawSchema : ({} as S); + + switch (getSchemaType(schema)) { + // We need to recurse for object schema inner default values. + case 'object': { + return getObjectDefaults(validator, schema, defaults, computeDefaultsProps); + } + case 'array': { + return getArrayDefaults(validator, schema, defaults, computeDefaultsProps); + } + } +} + +/** 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 defaults - Optional props for this function + * @param {ComputeDefaultsProps} computeDefaultsProps - Optional props for this function + * @returns - The default value based on the schema type if they are defined for object or array schemas. + */ +function getObjectDefaults( + validator: ValidatorType, + rawSchema: S, + defaults: T | T[] | undefined, + { + rawFormData, + rootSchema = {} as S, + includeUndefinedValues = false, + _recurseList = [], + experimental_defaultFormStateBehavior = undefined, + required, + }: ComputeDefaultsProps = {} +): T { + { + const formData: T = (isObject(rawFormData) ? rawFormData : {}) as T; + const schema: S = isObject(rawSchema) ? rawSchema : ({} as S); + // 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 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; + } +} + +/** 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 defaults - Optional props for this function + * @param {ComputeDefaultsProps} computeDefaultsProps - Optional props for this function + * @returns - The default value based on the schema type if they are defined for object or array schemas. + */ +function getArrayDefaults( + validator: ValidatorType, + rawSchema: S, + defaults: T | T[] | undefined, + { + rawFormData, + rootSchema = {} as S, + _recurseList = [], + experimental_defaultFormStateBehavior = undefined, + required, + }: ComputeDefaultsProps = {} +): T | T[] | undefined { + const schema: S = isObject(rawSchema) ? rawSchema : ({} as S); + + 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 emptyDefault = isSkipEmptyDefaults ? undefined : []; + + // 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[]; + } + + // 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[]; + } + } + + 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); +} + /** Returns the superset of `formData` that includes the given set updated to include any missing fields that have * computed to have defaults provided in the `schema`. * From bb5bc6e7a9d0a47af8cfb5b1af9e072f395afbf3 Mon Sep 17 00:00:00 2001 From: Abdallah Al-Soqatri Date: Mon, 2 Sep 2024 01:35:27 +0200 Subject: [PATCH 4/6] created tests for the new created methods --- .../utils/src/schema/getDefaultFormState.ts | 44 +- .../test/schema/getDefaultFormStateTest.ts | 741 ++++++++++++++++++ 2 files changed, 764 insertions(+), 21 deletions(-) diff --git a/packages/utils/src/schema/getDefaultFormState.ts b/packages/utils/src/schema/getDefaultFormState.ts index 794a898710..6dc008d09c 100644 --- a/packages/utils/src/schema/getDefaultFormState.ts +++ b/packages/utils/src/schema/getDefaultFormState.ts @@ -207,7 +207,7 @@ export function computeDefaults(validator, schema, rootSchema, false, [], defaultFormData); schemaToCompute = resolvedSchema[0]; // pick the first element from resolve dependencies @@ -276,7 +276,7 @@ export function computeDefaults( +export function getDefaultBasedOnSchemaType< + T = any, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = any +>( validator: ValidatorType, rawSchema: S, - defaults: T | T[] | undefined, - computeDefaultsProps: ComputeDefaultsProps = {} + computeDefaultsProps: ComputeDefaultsProps = {}, + defaults?: T | T[] | undefined ): T | T[] | void { - const schema: S = isObject(rawSchema) ? rawSchema : ({} as S); - - switch (getSchemaType(schema)) { + switch (getSchemaType(rawSchema)) { // We need to recurse for object schema inner default values. case 'object': { - return getObjectDefaults(validator, schema, defaults, computeDefaultsProps); + return getObjectDefaults(validator, rawSchema, computeDefaultsProps, defaults); } case 'array': { - return getArrayDefaults(validator, schema, defaults, computeDefaultsProps); + return getArrayDefaults(validator, rawSchema, computeDefaultsProps, defaults); } } } @@ -312,14 +314,13 @@ function getDefaultBasedOnSchemaType( +export function getObjectDefaults( validator: ValidatorType, rawSchema: S, - defaults: T | T[] | undefined, { rawFormData, rootSchema = {} as S, @@ -327,11 +328,12 @@ function getObjectDefaults = {} + }: ComputeDefaultsProps = {}, + defaults?: T | T[] | undefined ): T { { const formData: T = (isObject(rawFormData) ? rawFormData : {}) as T; - const schema: S = isObject(rawSchema) ? rawSchema : ({} as S); + 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 = @@ -412,23 +414,23 @@ function getObjectDefaults( +export function getArrayDefaults( validator: ValidatorType, rawSchema: S, - defaults: T | T[] | undefined, { rawFormData, rootSchema = {} as S, _recurseList = [], experimental_defaultFormStateBehavior = undefined, required, - }: ComputeDefaultsProps = {} + }: ComputeDefaultsProps = {}, + defaults?: T | T[] | undefined ): T | T[] | undefined { - const schema: S = isObject(rawSchema) ? rawSchema : ({} as S); + const schema: S = rawSchema; const neverPopulate = experimental_defaultFormStateBehavior?.arrayMinItems?.populate === 'never'; const ignoreMinItemsFlagSet = experimental_defaultFormStateBehavior?.arrayMinItems?.populate === 'requiredOnly'; diff --git a/packages/utils/test/schema/getDefaultFormStateTest.ts b/packages/utils/test/schema/getDefaultFormStateTest.ts index 7158012f2c..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 = { From ac070c70b99c2141da96b7fcb70636731dde73cd Mon Sep 17 00:00:00 2001 From: Abdallah Al-Soqatri Date: Sat, 7 Sep 2024 20:48:35 +0200 Subject: [PATCH 5/6] organized file based on feedback --- CHANGELOG.md | 2 +- .../utils/src/schema/getDefaultFormState.ts | 58 +++++++++---------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0830c1cb40..7015096559 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ should change the heading of the (upcoming) version to include a major version b --> -# 5.20.1 +# 5.20.2 ## Dev / docs / playground diff --git a/packages/utils/src/schema/getDefaultFormState.ts b/packages/utils/src/schema/getDefaultFormState.ts index 6dc008d09c..b1f5a096c8 100644 --- a/packages/utils/src/schema/getDefaultFormState.ts +++ b/packages/utils/src/schema/getDefaultFormState.ts @@ -281,35 +281,6 @@ export function computeDefaults( - 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); - } - } -} - /** Computes the default value for objects. * * @param validator - an implementation of the `ValidatorType` interface that will be used when necessary @@ -510,6 +481,35 @@ export function getArrayDefaults( + 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 * computed to have defaults provided in the `schema`. * From 242e644bba42681939485450de1474a78475ee83 Mon Sep 17 00:00:00 2001 From: Heath C <51679588+heath-freenome@users.noreply.github.com> Date: Mon, 9 Sep 2024 10:08:28 -0700 Subject: [PATCH 6/6] Update CHANGELOG.md --- CHANGELOG.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7015096559..e649a01c0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,14 +18,16 @@ should change the heading of the (upcoming) version to include a major version b # 5.20.2 -## Dev / docs / playground - -- Updated the peer dependencies to `5.20.x` due to types and API changes in `@rjsf/utils` - ## @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 + +- Updated the peer dependencies to `5.20.x` due to types and API changes in `@rjsf/utils` + # 5.20.0 ## @rjsf/core